diff --git a/.gitignore b/.gitignore index f90c93da9..97eb7c9e0 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,8 @@ gradle-app.setting logs/ /velocity.toml /forwarding.secret +forwarding.secret +velocity.toml server-icon.png /bin/ run/ diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 7176ec2a6..000000000 --- a/Jenkinsfile +++ /dev/null @@ -1,38 +0,0 @@ -pipeline { - agent none - options { - disableConcurrentBuilds() - } - stages { - stage('Build') { - agent { - docker { - image 'velocitypowered/openjdk8-plus-git:slim' - args '-v gradle-cache:/root/.gradle:rw' - } - } - steps { - sh './gradlew build --no-daemon' - archiveArtifacts 'proxy/build/libs/*-all.jar,api/build/libs/*-all.jar' - } - } - stage('Deploy') { - when { - expression { - GIT_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() - return GIT_BRANCH == 'master' - } - } - agent { - docker { - image 'velocitypowered/openjdk8-plus-git:slim' - args '-v gradle-cache:/root/.gradle:rw -v maven-repo:/maven-repo:rw -v javadoc:/javadoc:rw' - } - } - steps { - sh 'export MAVEN_DEPLOYMENT=true; ./gradlew publish --no-daemon' - sh 'rsync -av --delete ./api/build/docs/javadoc/ /javadoc' - } - } - } -} \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 7bc15ca1b..0e23c91a2 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { `java-library` `maven-publish` + id("velocity-publish") } java { @@ -38,6 +39,8 @@ dependencies { api("net.kyori:adventure-text-serializer-legacy") api("net.kyori:adventure-text-serializer-plain") api("net.kyori:adventure-text-minimessage") + api("net.kyori:adventure-text-logger-slf4j") + api("net.kyori:adventure-text-serializer-ansi") api("org.slf4j:slf4j-api:$slf4jVersion") api("com.google.inject:guice:$guiceVersion") diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java index 671a7adcd..98bcb60d3 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerPreConnectEvent.java @@ -47,7 +47,7 @@ public final class ServerPreConnectEvent implements * * @param player the player who is connecting to a server * @param originalServer the server the player was trying to connect to - * @param previousServer the server the player ís connected to + * @param previousServer the server the player is connected to */ public ServerPreConnectEvent(Player player, RegisteredServer originalServer, @Nullable RegisteredServer previousServer) { diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 62d09a32d..ef7024efe 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -61,7 +61,9 @@ public enum ProtocolVersion { MINECRAFT_1_19(759, "1.19"), MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), MINECRAFT_1_19_3(761, "1.19.3"), - MINECRAFT_1_19_4(762, "1.19.4"); + MINECRAFT_1_19_4(762, "1.19.4"), + MINECRAFT_1_20(763, "1.20", "1.20.1"), + MINECRAFT_1_20_2(764, "1.20.2"); private static final int SNAPSHOT_BIT = 30; diff --git a/api/src/main/java/com/velocitypowered/api/permission/Tristate.java b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java index 189cc2789..ddbe26a86 100644 --- a/api/src/main/java/com/velocitypowered/api/permission/Tristate.java +++ b/api/src/main/java/com/velocitypowered/api/permission/Tristate.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018-2022 Velocity Contributors + * Copyright (C) 2018-2023 Velocity Contributors * * The Velocity API is licensed under the terms of the MIT License. For more details, * reference the LICENSE file in the api top-level directory. @@ -7,6 +7,7 @@ package com.velocitypowered.api.permission; +import java.util.Optional; import net.kyori.adventure.util.TriState; import org.checkerframework.checker.nullness.qual.Nullable; @@ -66,6 +67,21 @@ public enum Tristate { return val ? TRUE : FALSE; } + /** + * Returns a {@link Tristate} from an {@link Optional}. + * + *

Unlike {@link #fromBoolean(boolean)}, this method returns {@link #UNDEFINED} + * if the value is empty.

+ * + * @param val the optional boolean value + * @return {@link #UNDEFINED}, {@link #TRUE} or {@link #FALSE}, if the value is empty, + * true or false, respectively. + */ + public static Tristate fromOptionalBoolean(Optional val) { + return val.map(Tristate::fromBoolean).orElse(UNDEFINED); + } + + private final boolean booleanValue; Tristate(boolean booleanValue) { diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index b99c427c8..99bf09da0 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -147,10 +147,17 @@ public interface Player extends /** * Clears the tab list header and footer for the player. * - * @deprecated Use {@link TabList#clearHeaderAndFooter()}. + * @deprecated Use {@link Player#clearPlayerListHeaderAndFooter()}. */ @Deprecated - void clearHeaderAndFooter(); + default void clearHeaderAndFooter() { + clearPlayerListHeaderAndFooter(); + } + + /** + * Clears the player list header and footer. + */ + void clearPlayerListHeaderAndFooter(); /** * Returns the player's player list header. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index dca3182ab..fdd6d0a11 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -41,6 +41,12 @@ public interface ProxyServer extends Audience { */ void shutdown(); + /** + * Closes all listening endpoints for this server. + * This includes the main minecraft listener and query channel. + */ + void closeListeners(); + /** * Retrieves the player currently connected to this proxy by their Minecraft username. The search * is case-insensitive. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java index bceed260f..4d03d3a87 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabList.java @@ -43,6 +43,28 @@ public interface TabList { */ void addEntry(TabListEntry entry); + /** + * Adds a {@link Iterable} of {@link TabListEntry}'s to the {@link Player}'s tab list. + * + * @param entries to add to the tab list + */ + default void addEntries(Iterable entries) { + for (TabListEntry entry : entries) { + addEntry(entry); + } + } + + /** + * Adds an array of {@link TabListEntry}'s to the {@link Player}'s tab list. + * + * @param entries to add to the tab list + */ + default void addEntries(TabListEntry... entries) { + for (TabListEntry entry : entries) { + addEntry(entry); + } + } + /** * Removes the {@link TabListEntry} from the tab list with the {@link GameProfile} identified with * the specified {@link UUID}. diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts new file mode 100644 index 000000000..26af27624 --- /dev/null +++ b/build-logic/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + `kotlin-dsl` + alias(libs.plugins.spotless) +} + +dependencies { + // this is OK as long as the same version catalog is used in the main build and build-logic + // see https://github.com/gradle/gradle/issues/15383#issuecomment-779893192 + implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + implementation("com.diffplug.spotless:spotless-plugin-gradle:${libs.plugins.spotless.get().version}") +} + +spotless { + kotlin { + licenseHeaderFile(rootProject.file("../HEADER.txt")) + } +} diff --git a/buildSrc/settings.gradle.kts b/build-logic/settings.gradle.kts similarity index 79% rename from buildSrc/settings.gradle.kts rename to build-logic/settings.gradle.kts index 0ecd59401..853b9f8c4 100644 --- a/buildSrc/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("UnstableApiUsage") + dependencyResolutionManagement { repositories { mavenCentral() @@ -9,3 +11,5 @@ dependencyResolutionManagement { } } } + +rootProject.name = "build-logic" diff --git a/build-logic/src/main/kotlin/LibsAccessor.kt b/build-logic/src/main/kotlin/LibsAccessor.kt new file mode 100644 index 000000000..1214d4912 --- /dev/null +++ b/build-logic/src/main/kotlin/LibsAccessor.kt @@ -0,0 +1,6 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +val Project.libs: LibrariesForLibs + get() = rootProject.extensions.getByType() diff --git a/build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts b/build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts new file mode 100644 index 000000000..4ac506002 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-checkstyle.gradle.kts @@ -0,0 +1,10 @@ +plugins { + checkstyle +} + +extensions.configure { + configFile = rootProject.file("config/checkstyle/checkstyle.xml") + maxErrors = 0 + maxWarnings = 0 + toolVersion = libs.checkstyle.get().version.toString() +} diff --git a/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts b/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts new file mode 100644 index 000000000..1901430f5 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-init-manifest.gradle.kts @@ -0,0 +1,29 @@ +import org.gradle.jvm.tasks.Jar +import org.gradle.kotlin.dsl.withType +import java.io.ByteArrayOutputStream + +val currentShortRevision = ByteArrayOutputStream().use { + exec { + executable = "git" + args = listOf("rev-parse", "HEAD") + standardOutput = it + } + it.toString().trim().substring(0, 8) +} + +tasks.withType { + manifest { + val buildNumber = System.getenv("BUILD_NUMBER") + val velocityHumanVersion: String = + if (project.version.toString().endsWith("-SNAPSHOT")) { + if (buildNumber == null) { + "${project.version} (git-$currentShortRevision)" + } else { + "${project.version} (git-$currentShortRevision-b$buildNumber)" + } + } else { + archiveVersion.get() + } + attributes["Implementation-Version"] = velocityHumanVersion + } +} diff --git a/build-logic/src/main/kotlin/velocity-publish.gradle.kts b/build-logic/src/main/kotlin/velocity-publish.gradle.kts new file mode 100644 index 000000000..51de5c672 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-publish.gradle.kts @@ -0,0 +1,33 @@ +plugins { + java + `maven-publish` +} + +extensions.configure { + repositories { + maven { + credentials(PasswordCredentials::class.java) + + name = "paper" + val base = "https://repo.papermc.io/repository/maven" + val releasesRepoUrl = "$base-releases/" + val snapshotsRepoUrl = "$base-snapshots/" + setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) + } + } + publications { + create("maven") { + from(components["java"]) + pom { + name.set("Velocity") + description.set("The modern, next-generation Minecraft server proxy") + url.set("https://papermc.io/software/velocity") + scm { + url.set("https://github.com/PaperMC/Velocity") + connection.set("scm:git:https://github.com/PaperMC/Velocity.git") + developerConnection.set("scm:git:https://github.com/PaperMC/Velocity.git") + } + } + } + } +} diff --git a/build-logic/src/main/kotlin/velocity-spotless.gradle.kts b/build-logic/src/main/kotlin/velocity-spotless.gradle.kts new file mode 100644 index 000000000..cbd3f3d46 --- /dev/null +++ b/build-logic/src/main/kotlin/velocity-spotless.gradle.kts @@ -0,0 +1,15 @@ +import com.diffplug.gradle.spotless.SpotlessExtension +import com.diffplug.gradle.spotless.SpotlessPlugin + +apply() + +extensions.configure { + java { + if (project.name == "velocity-api") { + licenseHeaderFile(file("HEADER.txt")) + } else { + licenseHeaderFile(rootProject.file("HEADER.txt")) + } + removeUnusedImports() + } +} diff --git a/build.gradle.kts b/build.gradle.kts index f1a64c850..3ec6b160c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,17 +1,14 @@ -import com.velocitypowered.script.VelocityCheckstylePlugin -import com.velocitypowered.script.VelocityPublishPlugin -import com.velocitypowered.script.VelocitySpotlessPlugin - plugins { `java-library` + id("velocity-checkstyle") apply false + id("velocity-spotless") apply false } subprojects { apply() - apply() - apply() - apply() + apply(plugin = "velocity-checkstyle") + apply(plugin = "velocity-spotless") java { toolchain { @@ -19,12 +16,6 @@ subprojects { } } - repositories { - mavenCentral() - maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // adventure - maven("https://repo.papermc.io/repository/maven-public/") - } - dependencies { testImplementation(rootProject.libs.junit) } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index e65c26037..000000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - `kotlin-dsl` - checkstyle - alias(libs.plugins.indra.publishing) - alias(libs.plugins.spotless) -} - -dependencies { - implementation("com.diffplug.spotless:spotless-plugin-gradle:${libs.plugins.spotless.get().version}") -} - -gradlePlugin { - plugins { - register("set-manifest-impl-version") { - id = "set-manifest-impl-version" - implementationClass = "com.velocitypowered.script.SetManifestImplVersionPlugin" - } - register("velocity-checkstyle") { - id = "velocity-checkstyle" - implementationClass = "com.velocitypowered.script.VelocityCheckstylePlugin" - } - register("velocity-spotless") { - id = "velocity-spotless" - implementationClass = "com.velocitypowered.script.VelocitySpotlessPlugin" - } - register("velocity-publish") { - id = "velocity-publish" - implementationClass = "com.velocitypowered.script.VelocityPublishPlugin" - } - } -} - -spotless { - kotlin { - licenseHeaderFile(project.rootProject.file("../HEADER.txt")) - } -} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt deleted file mode 100644 index dc0a66d51..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/SetManifestImplVersionPlugin.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.script - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.jvm.tasks.Jar -import org.gradle.kotlin.dsl.withType -import java.io.ByteArrayOutputStream - -class SetManifestImplVersionPlugin : Plugin { - override fun apply(target: Project) = target.afterEvaluate { configure() } - private fun Project.configure() { - val currentShortRevision = ByteArrayOutputStream().use { - exec { - executable = "git" - args = listOf("rev-parse", "HEAD") - standardOutput = it - } - it.toString().trim().substring(0, 8) - } - tasks.withType { - manifest { - val buildNumber = System.getenv("BUILD_NUMBER") - var velocityHumanVersion: String - if (project.version.toString().endsWith("-SNAPSHOT")) { - if (buildNumber != null) { - velocityHumanVersion = "${project.version} (git-$currentShortRevision-b$buildNumber)" - } else { - velocityHumanVersion = "${project.version} (git-$currentShortRevision)" - } - } else { - velocityHumanVersion = project.version.toString() - } - attributes["Implementation-Version"] = velocityHumanVersion - } - } - } -} diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt deleted file mode 100644 index 73d5e7bcd..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityCheckstylePlugin.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.script - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.plugins.quality.CheckstyleExtension -import org.gradle.api.plugins.quality.CheckstylePlugin -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure - -class VelocityCheckstylePlugin : Plugin { - override fun apply(target: Project) = target.configure() - private fun Project.configure() { - apply() - extensions.configure { - configFile = project.rootProject.file("config/checkstyle/checkstyle.xml") - maxErrors = 0 - maxWarnings = 0 - toolVersion = "10.6.0" - } - } -} diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt deleted file mode 100644 index 8aa18b048..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocityPublishPlugin.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.script - -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.artifacts.repositories.PasswordCredentials -import org.gradle.api.plugins.JavaBasePlugin -import org.gradle.api.plugins.JavaPluginExtension -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.api.publish.maven.plugins.MavenPublishPlugin -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure -import org.gradle.kotlin.dsl.create -import org.gradle.kotlin.dsl.get -import org.gradle.kotlin.dsl.getByType - -class VelocityPublishPlugin : Plugin { - override fun apply(target: Project) = target.afterEvaluate { - if (target.name != "velocity-proxy") { - configure() - } - } - private fun Project.configure() { - apply() - apply() - extensions.configure { - repositories { - maven { - credentials(PasswordCredentials::class.java) - - name = "paper" - val base = "https://papermc.io/repo/repository/maven" - val releasesRepoUrl = "$base-releases/" - val snapshotsRepoUrl = "$base-snapshots/" - setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) - } - } - publications { - create("maven") { - from(components["java"]) - pom { - name.set("Velocity") - description.set("The modern, next-generation Minecraft server proxy") - url.set("https://www.velocitypowered.com") - scm { - url.set("https://github.com/PaperMC/Velocity") - connection.set("scm:git:https://github.com/PaperMC/Velocity.git") - developerConnection.set("scm:git:https://github.com/PaperMC/Velocity.git") - } - } - } - } - } - } -} diff --git a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt b/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt deleted file mode 100644 index 1641f2c8a..000000000 --- a/buildSrc/src/main/kotlin/com/velocitypowered/script/VelocitySpotlessPlugin.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.script - -import com.diffplug.gradle.spotless.SpotlessExtension -import com.diffplug.gradle.spotless.SpotlessPlugin -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.apply -import org.gradle.kotlin.dsl.configure -import java.io.File - -class VelocitySpotlessPlugin : Plugin { - override fun apply(target: Project) = target.configure() - - private fun Project.configure() { - apply() - - extensions.configure { - java { - if (project.name == "velocity-api") { - licenseHeaderFile(project.file("HEADER.txt")) - } else { - licenseHeaderFile(project.rootProject.file("HEADER.txt")) - } - - removeUnusedImports() - } - } - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e2e762c01..8b1f9a817 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ configurate = "3.7.3" flare = "2.0.1" log4j = "2.20.0" -netty = "4.1.90.Final" +netty = "4.1.100.Final" [plugins] indra-publishing = "net.kyori.indra.publishing:2.0.6" @@ -10,13 +10,15 @@ shadow = "com.github.johnrengelman.shadow:8.1.0" spotless = "com.diffplug.spotless:6.12.0" [libraries] -adventure-bom = "net.kyori:adventure-bom:4.13.1" +adventure-bom = "net.kyori:adventure-bom:4.14.0" adventure-facet = "net.kyori:adventure-platform-facet:4.3.0" +asm = "org.ow2.asm:asm:9.5" asynchttpclient = "org.asynchttpclient:async-http-client:2.12.3" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" bstats = "org.bstats:bstats-base:3.0.1" caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.5" checker-qual = "org.checkerframework:checker-qual:3.28.0" +checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" completablefutures = "com.spotify:completable-futures:0.3.5" configurate-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate" } configurate-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate" } @@ -28,13 +30,14 @@ flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = jline = "org.jline:jline-terminal-jansi:3.23.0" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" junit = "org.junit.jupiter:junit-jupiter:5.9.0" +kyori-ansi = "net.kyori:ansi:1.0.3" guava = "com.google.guava:guava:25.1-jre" gson = "com.google.code.gson:gson:2.10.1" -guice = "com.google.inject:guice:5.1.0" +guice = "com.google.inject:guice:6.0.0" lmbda = "org.lanternpowered:lmbda:2.0.0" log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } -log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j-impl", version.ref = "log4j" } +log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } mockito = "org.mockito:mockito-core:5.2.0" @@ -44,7 +47,7 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } nightconfig = "com.electronwill.night-config:toml:3.6.6" -slf4j = "org.slf4j:slf4j-api:1.7.30" +slf4j = "org.slf4j:slf4j-api:2.0.7" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" diff --git a/native/build.gradle.kts b/native/build.gradle.kts index 165cb30f9..5ec8673cc 100644 --- a/native/build.gradle.kts +++ b/native/build.gradle.kts @@ -1,6 +1,6 @@ plugins { `java-library` - `maven-publish` + id("velocity-publish") } dependencies { diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 997ddd467..322888a27 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCach plugins { application - `set-manifest-impl-version` + id("velocity-init-manifest") alias(libs.plugins.shadow) } @@ -93,6 +93,7 @@ dependencies { implementation(project(":velocity-native")) implementation(libs.bundles.log4j) + implementation(libs.kyori.ansi) implementation(libs.netty.codec) implementation(libs.netty.codec.haproxy) implementation(libs.netty.codec.http) @@ -114,6 +115,7 @@ dependencies { implementation(libs.nightconfig) implementation(libs.bstats) implementation(libs.lmbda) + implementation(libs.asm) implementation(libs.bundles.flare) compileOnly(libs.spotbugs.annotations) testImplementation(libs.mockito) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index c59c225fd..912e5ecb2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -568,6 +568,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { shutdown(true); } + @Override + public void closeListeners() { + this.cm.closeEndpoints(false); + } + public AsyncHttpClient getAsyncHttpClient() { return cm.getHttpClient(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index 9145ff133..bb0c4db72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -36,6 +36,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler; +import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; @@ -46,6 +47,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; @@ -60,6 +62,9 @@ import io.netty.util.ReferenceCountUtil; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -78,7 +83,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private final Channel channel; private SocketAddress remoteAddress; private StateRegistry state; - private @Nullable MinecraftSessionHandler sessionHandler; + private Map sessionHandlers; + private @Nullable MinecraftSessionHandler activeSessionHandler; private ProtocolVersion protocolVersion; private @Nullable MinecraftConnectionAssociation association; public final VelocityServer server; @@ -96,12 +102,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.remoteAddress = channel.remoteAddress(); this.server = server; this.state = StateRegistry.HANDSHAKE; + + this.sessionHandlers = new HashMap<>(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.connected(); + if (activeSessionHandler != null) { + activeSessionHandler.connected(); } if (association != null && server.getConfiguration().isLogPlayerConnections()) { @@ -111,12 +119,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.disconnected(); + if (activeSessionHandler != null) { + activeSessionHandler.disconnected(); } if (association != null && !knownDisconnect - && !(sessionHandler instanceof StatusSessionHandler) + && !(activeSessionHandler instanceof StatusSessionHandler) && server.getConfiguration().isLogPlayerConnections()) { logger.info("{} has disconnected", association); } @@ -125,12 +133,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { - if (sessionHandler == null) { + if (activeSessionHandler == null) { // No session handler available, do nothing return; } - if (sessionHandler.beforeHandle()) { + if (activeSessionHandler.beforeHandle()) { return; } @@ -140,15 +148,15 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (msg instanceof MinecraftPacket) { MinecraftPacket pkt = (MinecraftPacket) msg; - if (!pkt.handle(sessionHandler)) { - sessionHandler.handleGeneric((MinecraftPacket) msg); + if (!pkt.handle(activeSessionHandler)) { + activeSessionHandler.handleGeneric((MinecraftPacket) msg); } } else if (msg instanceof HAProxyMessage) { HAProxyMessage proxyMessage = (HAProxyMessage) msg; this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); } else if (msg instanceof ByteBuf) { - sessionHandler.handleUnknown((ByteBuf) msg); + activeSessionHandler.handleUnknown((ByteBuf) msg); } } finally { ReferenceCountUtil.release(msg); @@ -157,20 +165,21 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.readCompleted(); + if (activeSessionHandler != null) { + activeSessionHandler.readCompleted(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (ctx.channel().isActive()) { - if (sessionHandler != null) { + if (activeSessionHandler != null) { try { - sessionHandler.exception(cause); + activeSessionHandler.exception(cause); } catch (Exception ex) { logger.error("{}: exception handling exception in {}", - (association != null ? association : channel.remoteAddress()), sessionHandler, cause); + (association != null ? association : channel.remoteAddress()), activeSessionHandler, + cause); } } @@ -178,13 +187,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (cause instanceof ReadTimeoutException) { logger.error("{}: read timed out", association); } else { - boolean frontlineHandler = sessionHandler instanceof InitialLoginSessionHandler - || sessionHandler instanceof HandshakeSessionHandler - || sessionHandler instanceof StatusSessionHandler; + boolean frontlineHandler = activeSessionHandler instanceof InitialLoginSessionHandler + || activeSessionHandler instanceof HandshakeSessionHandler + || activeSessionHandler instanceof StatusSessionHandler; boolean isQuietDecoderException = cause instanceof QuietDecoderException; boolean willLog = !isQuietDecoderException && !frontlineHandler; if (willLog) { - logger.error("{}: exception encountered in {}", association, sessionHandler, cause); + logger.error("{}: exception encountered in {}", association, activeSessionHandler, + cause); } else { knownDisconnect = true; } @@ -197,8 +207,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.writabilityChanged(); + if (activeSessionHandler != null) { + activeSessionHandler.writabilityChanged(); } } @@ -323,7 +333,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } /** - * Determines whether or not the channel should continue reading data automaticaly. + * Determines whether or not the channel should continue reading data automatically. * * @param autoReading whether or not we should read data automatically */ @@ -341,10 +351,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } } + // Ideally only used by the state switch + /** - * Changes the state of the Minecraft connection. + * Sets the new state for the connection. * - * @param state the new state + * @param state the state to use */ public void setState(StateRegistry state) { ensureInEventLoop(); @@ -352,6 +364,25 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.state = state; this.channel.pipeline().get(MinecraftEncoder.class).setState(state); this.channel.pipeline().get(MinecraftDecoder.class).setState(state); + + if (state == StateRegistry.CONFIG) { + // Activate the play packet queue + addPlayPacketQueueHandler(); + } else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) { + // Remove the queue + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE); + } + } + + /** + * Adds the play packet queue handler. + */ + public void addPlayPacketQueueHandler() { + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, + new PlayPacketQueueHandler(this.protocolVersion, + channel.pipeline().get(MinecraftEncoder.class).getDirection())); + } } public ProtocolVersion getProtocolVersion() { @@ -382,32 +413,81 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } } - public @Nullable MinecraftSessionHandler getSessionHandler() { - return sessionHandler; + public @Nullable MinecraftSessionHandler getActiveSessionHandler() { + return activeSessionHandler; + } + + public @Nullable MinecraftSessionHandler getSessionHandlerForRegistry(StateRegistry registry) { + return this.sessionHandlers.getOrDefault(registry, null); } /** * Sets the session handler for this connection. * + * @param registry the registry of the handler * @param sessionHandler the handler to use */ - public void setSessionHandler(MinecraftSessionHandler sessionHandler) { + public void setActiveSessionHandler(StateRegistry registry, + MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); ensureInEventLoop(); - if (this.sessionHandler != null) { - this.sessionHandler.deactivated(); + if (this.activeSessionHandler != null) { + this.activeSessionHandler.deactivated(); } - this.sessionHandler = sessionHandler; + this.sessionHandlers.put(registry, sessionHandler); + this.activeSessionHandler = sessionHandler; + setState(registry); sessionHandler.activated(); } + /** + * Switches the active session handler to the respective registry one. + * + * @param registry the registry of the handler + * @return true if successful and handler is present + */ + public boolean setActiveSessionHandler(StateRegistry registry) { + Preconditions.checkNotNull(registry); + ensureInEventLoop(); + + MinecraftSessionHandler handler = getSessionHandlerForRegistry(registry); + if (handler != null) { + boolean flag = true; + if (this.activeSessionHandler != null + && (flag = !Objects.equals(handler, this.activeSessionHandler))) { + this.activeSessionHandler.deactivated(); + } + this.activeSessionHandler = handler; + setState(registry); + if (flag) { + handler.activated(); + } + } + return handler != null; + } + + /** + * Adds a secondary session handler for this connection. + * + * @param registry the registry of the handler + * @param sessionHandler the handler to use + */ + public void addSessionHandler(StateRegistry registry, MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); + Preconditions.checkArgument(registry != state, "Handler would overwrite handler"); + ensureInEventLoop(); + + this.sessionHandlers.put(registry, sessionHandler); + } + private void ensureOpen() { Preconditions.checkState(!isClosed(), "Connection is closed."); } /** - * Sets the compression threshold on the connection. You are responsible for sending - * {@link com.velocitypowered.proxy.protocol.packet.SetCompression} beforehand. + * Sets the compression threshold on the connection. You are responsible for sending {@link + * com.velocitypowered.proxy.protocol.packet.SetCompression} beforehand. * * @param threshold the compression threshold to use */ @@ -497,5 +577,4 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void setType(ConnectionType connectionType) { this.connectionType = connectionType; } - } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index ef4c362f4..189820e04 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -31,8 +31,10 @@ import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyHandshake; import com.velocitypowered.proxy.protocol.packet.LegacyPing; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; +import com.velocitypowered.proxy.protocol.packet.PingIdentify; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; @@ -48,6 +50,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgement; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletion; import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; @@ -55,6 +58,11 @@ import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.ActiveFeatures; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -279,4 +287,36 @@ public interface MinecraftSessionHandler { default boolean handle(UpsertPlayerInfo packet) { return false; } + + default boolean handle(LoginAcknowledged packet) { + return false; + } + + default boolean handle(ActiveFeatures packet) { + return false; + } + + default boolean handle(FinishedUpdate packet) { + return false; + } + + default boolean handle(RegistrySync packet) { + return false; + } + + default boolean handle(TagsUpdate packet) { + return false; + } + + default boolean handle(StartUpdate packet) { + return false; + } + + default boolean handle(PingIdentify pingIdentify) { + return false; + } + + default boolean handle(ChatAcknowledgement chatAcknowledgement) { + return false; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 3adf6ba25..c21ae6ecd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -39,8 +39,11 @@ import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; @@ -51,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -68,10 +72,10 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class); - private static final boolean BACKPRESSURE_LOG = Boolean - .getBoolean("velocity.log-server-backpressure"); - private static final int MAXIMUM_PACKETS_TO_FLUSH = Integer - .getInteger("velocity.max-packets-per-flush", 8192); + private static final boolean BACKPRESSURE_LOG = + Boolean.getBoolean("velocity.log-server-backpressure"); + private static final int MAXIMUM_PACKETS_TO_FLUSH = + Integer.getInteger("velocity.max-packets-per-flush", 8192); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -86,7 +90,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { this.serverConn = serverConn; this.playerConnection = serverConn.getPlayer().getConnection(); - MinecraftSessionHandler psh = playerConnection.getSessionHandler(); + MinecraftSessionHandler psh = playerConnection.getActiveSessionHandler(); if (!(psh instanceof ClientPlaySessionHandler)) { throw new IllegalStateException( "Initializing BackendPlaySessionHandler with no backing client play session handler!"); @@ -101,12 +105,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { public void activated() { serverConn.getServer().addPlayer(serverConn.getPlayer()); + MinecraftConnection serverMc = serverConn.ensureConnected(); if (server.getConfiguration().isBungeePluginChannelEnabled()) { - MinecraftConnection serverMc = serverConn.ensureConnected(); serverMc.write(PluginMessageUtil.constructChannelsPacket(serverMc.getProtocolVersion(), ImmutableList.of(getBungeeCordChannel(serverMc.getProtocolVersion())) )); } + } @Override @@ -119,12 +124,28 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return false; } + @Override + public boolean handle(StartUpdate packet) { + MinecraftConnection smc = serverConn.ensureConnected(); + smc.setAutoReading(false); + // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.CONFIG); + serverConn.getPlayer().switchToConfigState(); + return true; + } + @Override public boolean handle(KeepAlive packet) { serverConn.getPendingPings().put(packet.getRandomId(), System.currentTimeMillis()); return false; // forwards on } + @Override + public boolean handle(ClientSettings packet) { + serverConn.ensureConnected().write(packet); + return true; + } + @Override public boolean handle(Disconnect packet) { serverConn.disconnect(); @@ -198,13 +219,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return true; } - // We need to specially handle REGISTER and UNREGISTER packets. Later on, we'll write them to - // the client. - if (PluginMessageUtil.isRegister(packet)) { - serverConn.getPlayer().getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); - return false; - } else if (PluginMessageUtil.isUnregister(packet)) { - serverConn.getPlayer().getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); + // Register and unregister packets are simply forwarded to the server as-is. + if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) { return false; } @@ -226,20 +242,16 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } byte[] copy = ByteBufUtil.getBytes(packet.content()); - PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, - copy); - server.getEventManager().fire(event) - .thenAcceptAsync(pme -> { - if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { - PluginMessage copied = new PluginMessage(packet.getChannel(), - Unpooled.wrappedBuffer(copy)); - playerConnection.write(copied); - } - }, playerConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling plugin message {}", packet, ex); - return null; - }); + PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy); + server.getEventManager().fire(event).thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { + PluginMessage copied = new PluginMessage(packet.getChannel(), Unpooled.wrappedBuffer(copy)); + playerConnection.write(copied); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message {}", packet, ex); + return null; + }); return true; } @@ -288,18 +300,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerData packet) { - server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()) - .thenComposeAsync( - ping -> server.getEventManager() - .fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), - playerConnection.eventLoop() - ) - .thenAcceptAsync(pingEvent -> - this.playerConnection.write( - new ServerData(pingEvent.getPing().getDescriptionComponent(), - pingEvent.getPing().getFavicon().orElse(null), - packet.isSecureChatEnforced()) - ), playerConnection.eventLoop()); + server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()).thenComposeAsync( + ping -> server.getEventManager() + .fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), + playerConnection.eventLoop()).thenAcceptAsync(pingEvent -> this.playerConnection.write( + new ServerData(pingEvent.getPing().getDescriptionComponent(), + pingEvent.getPing().getFavicon().orElse(null), packet.isSecureChatEnforced())), + playerConnection.eventLoop()); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java new file mode 100644 index 000000000..7374f7985 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.util.ConnectionMessages; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.packet.Disconnect; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; +import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; +import net.kyori.adventure.text.Component; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A special session handler that catches "last minute" disconnects. This version is to accommodate + * 1.20.2+ switching. Yes, some of this is exceptionally stupid. + */ +public class ConfigSessionHandler implements MinecraftSessionHandler { + + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); + private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class); + private final VelocityServer server; + private final VelocityServerConnection serverConn; + private final CompletableFuture resultFuture; + + private ResourcePackInfo resourcePackToApply; + + private State state; + + /** + * Creates the new transition handler. + * + * @param server the Velocity server instance + * @param serverConn the server connection + * @param resultFuture the result future + */ + ConfigSessionHandler(VelocityServer server, VelocityServerConnection serverConn, + CompletableFuture resultFuture) { + this.server = server; + this.serverConn = serverConn; + this.resultFuture = resultFuture; + this.state = State.START; + } + + @Override + public void activated() { + resourcePackToApply = serverConn.getPlayer().getAppliedResourcePack(); + serverConn.getPlayer().clearAppliedResourcePack(); + } + + @Override + public boolean beforeHandle() { + if (!serverConn.isActive()) { + // Obsolete connection + serverConn.disconnect(); + return true; + } + return false; + } + + @Override + public boolean handle(StartUpdate packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(TagsUpdate packet) { + serverConn.getPlayer().getConnection().write(packet); + return true; + } + + @Override + public boolean handle(KeepAlive packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(ResourcePackRequest packet) { + final MinecraftConnection playerConnection = serverConn.getPlayer().getConnection(); + + ServerResourcePackSendEvent event = + new ServerResourcePackSendEvent(packet.toServerPromptedPack(), this.serverConn); + + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackSendEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackSendEvent.getResult().isAllowed()) { + ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); + if (toSend != serverResourcePackSendEvent.getReceivedResourcePack()) { + ((VelocityResourcePackInfo) toSend).setOriginalOrigin( + ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + } + + resourcePackToApply = null; + serverConn.getPlayer().queueResourcePack(toSend); + } else if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse(packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse(packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + } + logger.error("Exception while handling resource pack send for {}", playerConnection, ex); + return null; + }); + + return true; + } + + @Override + public boolean handle(FinishedUpdate packet) { + MinecraftConnection smc = serverConn.ensureConnected(); + ConnectedPlayer player = serverConn.getPlayer(); + ClientConfigSessionHandler configHandler = + (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); + + smc.setAutoReading(false); + // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); + configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { + if (serverConn == player.getConnectedServer()) { + smc.setActiveSessionHandler(StateRegistry.PLAY); + player.sendPlayerListHeaderAndFooter( + player.getPlayerListHeader(), player.getPlayerListFooter()); + // The client cleared the tab list. TODO: Restore changes done via TabList API + player.getTabList().clearAllSilent(); + } else { + smc.setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + } + if (player.getAppliedResourcePack() == null && resourcePackToApply != null) { + player.queueResourcePack(resourcePackToApply); + } + smc.setAutoReading(true); + }, smc.eventLoop()); + return true; + } + + @Override + public boolean handle(Disconnect packet) { + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer())); + return true; + } + + @Override + public boolean handle(PluginMessage packet) { + if (PluginMessageUtil.isMcBrand(packet)) { + serverConn.getPlayer().getConnection().write( + PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), + serverConn.getPlayer().getProtocolVersion())); + } else { + // TODO: Change this so its usable for mod loaders + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect( + Component.translatable("multiplayer.disconnect.missing_tags"), serverConn.getServer())); + } + return true; + } + + @Override + public boolean handle(RegistrySync packet) { + serverConn.getPlayer().getConnection().write(packet.retain()); + return true; + } + + @Override + public void disconnected() { + resultFuture.completeExceptionally( + new IOException("Unexpectedly disconnected from remote server")); + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + serverConn.getPlayer().getConnection().write(packet); + } + + private void switchFailure(Throwable cause) { + logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), + serverConn.getPlayer().getUsername(), cause); + serverConn.getPlayer().disconnect(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + resultFuture.completeExceptionally(cause); + } + + /** + * Represents the state of the configuration stage. + */ + public static enum State { + START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index 90a9a1300..10079db72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -27,6 +27,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -34,6 +35,7 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; @@ -59,8 +61,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); - private static final Component MODERN_IP_FORWARDING_FAILURE = Component - .translatable("velocity.error.modern-forwarding-failed"); + private static final Component MODERN_IP_FORWARDING_FAILURE = + Component.translatable("velocity.error.modern-forwarding-failed"); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -150,10 +152,26 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // Move into the PLAY phase. MinecraftConnection smc = serverConn.ensureConnected(); - smc.setState(StateRegistry.PLAY); + if (smc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + smc.setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + } else { + smc.write(new LoginAcknowledged()); + smc.setActiveSessionHandler(StateRegistry.CONFIG, + new ConfigSessionHandler(server, serverConn, resultFuture)); + ConnectedPlayer player = serverConn.getPlayer(); + if (player.getClientSettingsPacket() != null) { + smc.write(player.getClientSettingsPacket()); + } + if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + smc.setAutoReading(false); + ((ClientPlaySessionHandler) player.getConnection() + .getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> { + smc.setAutoReading(true); + }, smc.eventLoop()); + } + } - // Switch to the transition handler. - smc.setSessionHandler(new TransitionSessionHandler(server, serverConn, resultFuture)); return true; } @@ -165,12 +183,12 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public void disconnected() { if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) { - resultFuture.completeExceptionally( - new QuietRuntimeException("The connection to the remote server was unexpectedly closed.\n" - + "This is usually because the remote server does not have BungeeCord IP forwarding " + resultFuture.completeExceptionally(new QuietRuntimeException( + "The connection to the remote server was unexpectedly closed.\n" + + "This is usually because the remote server " + + "does not have BungeeCord IP forwarding " + "correctly enabled.\nSee https://velocitypowered.com/wiki/users/forwarding/ " - + "for instructions on how to configure player info forwarding correctly.") - ); + + "for instructions on how to configure player info forwarding correctly.")); } else { resultFuture.completeExceptionally( new QuietRuntimeException("The connection to the remote server was unexpectedly closed.") diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 25cbbef63..0f32fbefb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -22,6 +22,7 @@ import static com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeHands import com.velocitypowered.api.event.player.ServerConnectedEvent; import com.velocitypowered.api.event.player.ServerPostConnectEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.ConnectionTypes; @@ -32,11 +33,11 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import java.io.IOException; import java.util.concurrent.CompletableFuture; import org.apache.logging.log4j.LogManager; @@ -104,7 +105,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { player.sendKeepAlive(); // Reset Tablist header and footer to prevent desync - player.clearHeaderAndFooter(); + player.clearPlayerListHeaderAndFooter(); } // The goods are in hand! We got JoinGame. Let's transition completely to the new state. @@ -121,17 +122,21 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Change the client to use the ClientPlaySessionHandler if required. ClientPlaySessionHandler playHandler; - if (player.getConnection().getSessionHandler() instanceof ClientPlaySessionHandler) { - playHandler = (ClientPlaySessionHandler) player.getConnection().getSessionHandler(); + if (player.getConnection() + .getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + playHandler = + (ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler(); } else { playHandler = new ClientPlaySessionHandler(server, player); - player.getConnection().setSessionHandler(playHandler); + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler); } + assert playHandler != null; playHandler.handleBackendJoinGame(packet, serverConn); // Set the new play session handler for the server. We will have nothing more to do // with this connection once this task finishes up. - smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn)); + smc.setActiveSessionHandler(StateRegistry.PLAY, + new BackendPlaySessionHandler(server, serverConn)); // Clean up disabling auto-read while the connected event was being processed. smc.setAutoReading(true); @@ -139,12 +144,17 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Now set the connected server. serverConn.getPlayer().setConnectedServer(serverConn); + // Send client settings. In 1.20.2+ this is done in the config state. + if (smc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0 + && player.getClientSettingsPacket() != null) { + serverConn.ensureConnected().write(player.getClientSettingsPacket()); + } + // We're done! :) server.getEventManager().fireAndForget(new ServerPostConnectEvent(player, previousServer)); resultFuture.complete(ConnectionRequestResults.successful(serverConn.getServer())); - }, smc.eventLoop()) - .exceptionally(exc -> { + }, smc.eventLoop()).exceptionally(exc -> { logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), player.getUsername(), exc); @@ -180,12 +190,6 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { return true; } - if (PluginMessageUtil.isRegister(packet)) { - serverConn.getPlayer().getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); - } else if (PluginMessageUtil.isUnregister(packet)) { - serverConn.getPlayer().getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); - } - // We always need to handle plugin messages, for Forge compatibility. if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) { // Handled, but check the server connection phase. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index ab5fd0b4d..a0cecd1b9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -93,8 +94,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, /** * Connects to the server. * - * @return a {@link com.velocitypowered.api.proxy.ConnectionRequestBuilder.Result} representing - * whether or not the connect succeeded + * @return a {@link com.velocitypowered.api.proxy.ConnectionRequestBuilder.Result} + * representing whether the connection succeeded */ public CompletableFuture connect() { CompletableFuture result = new CompletableFuture<>(); @@ -111,15 +112,21 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, future.channel().pipeline().addLast(HANDLER, connection); // Kick off the connection process - connection.setSessionHandler( - new LoginSessionHandler(server, VelocityServerConnection.this, result)); + if (!connection.setActiveSessionHandler(StateRegistry.HANDSHAKE)) { + MinecraftSessionHandler handler = + new LoginSessionHandler(server, VelocityServerConnection.this, result); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler); + connection.addSessionHandler(StateRegistry.LOGIN, handler); + } - // Set the connection phase, which may, for future forge (or whatever), be determined + // Set the connection phase, which may, for future forge (or whatever), be + // determined // at this point already connectionPhase = connection.getType().getInitialBackendPhase(); startHandshake(); } else { - // Complete the result immediately. ConnectedPlayer will reset the in-flight connection. + // Complete the result immediately. ConnectedPlayer will reset the in-flight + // connection. result.completeExceptionally(future.cause()); } }); @@ -174,12 +181,10 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private String createBungeeGuardForwardingAddress(byte[] forwardingSecret) { // Append forwarding secret as a BungeeGuard token. - Property property = new Property("bungeeguard-token", - new String(forwardingSecret, StandardCharsets.UTF_8), ""); - return createLegacyForwardingAddress(properties -> ImmutableList.builder() - .addAll(properties) - .add(property) - .build()); + Property property = + new Property("bungeeguard-token", new String(forwardingSecret, StandardCharsets.UTF_8), ""); + return createLegacyForwardingAddress( + properties -> ImmutableList.builder().addAll(properties).add(property).build()); } private void startHandshake() { @@ -209,7 +214,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, mc.delayedWrite(handshake); mc.setProtocolVersion(protocolVersion); - mc.setState(StateRegistry.LOGIN); + mc.setActiveSessionHandler(StateRegistry.LOGIN); if (proxyPlayer.getIdentifiedKey() == null && proxyPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getUniqueId())); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index f68d3bb59..603cf686a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -38,6 +39,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; import io.netty.buffer.ByteBuf; @@ -64,6 +66,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private GameProfile profile; private @MonotonicNonNull ConnectedPlayer connectedPlayer; private final boolean onlineMode; + private State loginState = State.START; // 1.20.2+ AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode) { @@ -95,8 +98,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { inbound.getIdentifiedKey()); this.connectedPlayer = player; if (!server.canRegisterConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy", - NamedTextColor.RED), true); + player.disconnect0( + Component.translatable("velocity.error.already-connected-proxy", NamedTextColor.RED), + true); return CompletableFuture.completedFuture(null); } @@ -109,16 +113,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler { // wait for permissions to load, then set the players permission function final PermissionFunction function = event.createFunction(player); if (function == null) { - logger.error( - "A plugin permission provider {} provided an invalid permission function" - + " for player {}. This is a bug in the plugin, not in Velocity. Falling" - + " back to the default permission function.", - event.getProvider().getClass().getName(), - player.getUsername()); + logger.error("A plugin permission provider {} provided an invalid permission " + + "function for player {}. This is a bug in the plugin, not in " + + "Velocity. Falling back to the default permission function.", + event.getProvider().getClass().getName(), player.getUsername()); } else { player.setPermissionFunction(function); } - completeLoginProtocolPhaseAndInitialize(player); + startLoginCompletion(player); } }, mcConnection.eventLoop()); }, mcConnection.eventLoop()).exceptionally((ex) -> { @@ -127,7 +129,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { }); } - private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { + private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { mcConnection.write(new SetCompression(threshold)); @@ -165,64 +167,87 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } } - ServerLoginSuccess success = new ServerLoginSuccess(); - success.setUsername(player.getUsername()); - success.setProperties(player.getGameProfileProperties()); - success.setUuid(playerUniqueId); - mcConnection.write(success); + completeLoginProtocolPhaseAndInitialize(player); + } + @Override + public boolean handle(LoginAcknowledged packet) { + if (loginState != State.SUCCESS_SENT) { + inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); + } else { + loginState = State.ACKNOWLEDGED; + mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, + new ClientConfigSessionHandler(server, connectedPlayer)); + + server.getEventManager().fire(new PostLoginEvent(connectedPlayer)) + .thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); + return null; + }); + } + return true; + } + + private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { mcConnection.setAssociation(player); - mcConnection.setState(StateRegistry.PLAY); - server.getEventManager().fire(new LoginEvent(player)) - .thenAcceptAsync(event -> { - if (mcConnection.isClosed()) { - // The player was disconnected - server.getEventManager().fireAndForget(new DisconnectEvent(player, - DisconnectEvent.LoginStatus.CANCELLED_BY_USER_BEFORE_COMPLETE)); - return; - } + server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> { + if (mcConnection.isClosed()) { + // The player was disconnected + server.getEventManager().fireAndForget(new DisconnectEvent(player, + DisconnectEvent.LoginStatus.CANCELLED_BY_USER_BEFORE_COMPLETE)); + return; + } - Optional reason = event.getResult().getReasonComponent(); - if (reason.isPresent()) { - player.disconnect0(reason.get(), true); - } else { - if (!server.registerConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), - true); - return; - } + Optional reason = event.getResult().getReasonComponent(); + if (reason.isPresent()) { + player.disconnect0(reason.get(), true); + } else { + if (!server.registerConnection(player)) { + player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), + true); + return; + } - mcConnection.setSessionHandler(new InitialConnectSessionHandler(player, server)); - server.getEventManager().fire(new PostLoginEvent(player)) - .thenCompose((ignored) -> connectToInitialServer(player)) - .exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", player, ex); - return null; - }); - } - }, mcConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while completing login initialisation phase for {}", player, ex); - return null; - }); + ServerLoginSuccess success = new ServerLoginSuccess(); + success.setUsername(player.getUsername()); + success.setProperties(player.getGameProfileProperties()); + success.setUuid(player.getUniqueId()); + mcConnection.write(success); + + loginState = State.SUCCESS_SENT; + if (inbound.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + loginState = State.ACKNOWLEDGED; + mcConnection.setActiveSessionHandler(StateRegistry.PLAY, + new InitialConnectSessionHandler(player, server)); + server.getEventManager().fire(new PostLoginEvent(player)) + .thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); + } + } + }, mcConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while completing login initialisation phase for {}", player, ex); + return null; + }); } private CompletableFuture connectToInitialServer(ConnectedPlayer player) { Optional initialFromConfig = player.getNextServerToTry(); - PlayerChooseInitialServerEvent event = new PlayerChooseInitialServerEvent(player, - initialFromConfig.orElse(null)); + PlayerChooseInitialServerEvent event = + new PlayerChooseInitialServerEvent(player, initialFromConfig.orElse(null)); - return server.getEventManager().fire(event) - .thenRunAsync(() -> { - Optional toTry = event.getInitialServer(); - if (!toTry.isPresent()) { - player.disconnect0(Component.translatable("velocity.error.no-available-servers", - NamedTextColor.RED), true); - return; - } - player.createConnectionRequest(toTry.get()).fireAndForget(); - }, mcConnection.eventLoop()); + return server.getEventManager().fire(event).thenRunAsync(() -> { + Optional toTry = event.getInitialServer(); + if (!toTry.isPresent()) { + player.disconnect0( + Component.translatable("velocity.error.no-available-servers", NamedTextColor.RED), + true); + return; + } + player.createConnectionRequest(toTry.get()).fireAndForget(); + }, mcConnection.eventLoop()); } @Override @@ -237,4 +262,8 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } this.inbound.cleanup(); } + + static enum State { + START, SUCCESS_SENT, ACKNOWLEDGED + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java new file mode 100644 index 000000000..83d9fe91b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.client; + +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import io.netty.buffer.ByteBuf; +import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Handles the client config stage. + */ +public class ClientConfigSessionHandler implements MinecraftSessionHandler { + + private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class); + private final VelocityServer server; + private final ConnectedPlayer player; + + private CompletableFuture configSwitchFuture; + + /** + * Constructs a client config session handler. + * + * @param server the Velocity server instance + * @param player the player + */ + public ClientConfigSessionHandler(VelocityServer server, ConnectedPlayer player) { + this.server = server; + this.player = player; + } + + @Override + public void activated() { + configSwitchFuture = new CompletableFuture<>(); + } + + @Override + public void deactivated() { + } + + @Override + public boolean handle(KeepAlive packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + player.setPing(System.currentTimeMillis() - sentTime); + smc.write(packet); + } + } + } + return true; + } + + @Override + public boolean handle(ClientSettings packet) { + player.setClientSettingsPacket(packet); + return true; + } + + @Override + public boolean handle(ResourcePackResponse packet) { + if (player.getConnectionInFlight() != null) { + player.getConnectionInFlight().ensureConnected().write(packet); + } + return player.onResourcePackResponse(packet.getStatus()); + } + + @Override + public boolean handle(FinishedUpdate packet) { + player.getConnection() + .setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); + + configSwitchFuture.complete(null); + return true; + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && serverConnection.getPhase().consideredComplete()) { + if (packet instanceof PluginMessage) { + ((PluginMessage) packet).retain(); + } + smc.write(packet); + } + } + + @Override + public void handleUnknown(ByteBuf buf) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { + smc.write(buf.retain()); + } + } + + @Override + public void disconnected() { + player.teardown(); + } + + @Override + public void exception(Throwable throwable) { + player.disconnect( + Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); + } + + /** + * Handles the backend finishing the config stage. + * + * @param serverConn the server connection + * @return a future that completes when the config stage is finished + */ + public CompletableFuture handleBackendFinishUpdate(VelocityServerConnection serverConn) { + player.getConnection().write(new FinishedUpdate()); + serverConn.ensureConnected().write(new FinishedUpdate()); + return configSwitchFuture; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index bc041d8ab..6004cf834 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -17,9 +17,6 @@ package com.velocitypowered.proxy.connection.client; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; -import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; import com.google.common.collect.ImmutableList; @@ -67,6 +64,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionChatHandler import com.velocitypowered.proxy.protocol.packet.chat.session.SessionCommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; @@ -80,6 +78,7 @@ import java.util.Collection; import java.util.List; import java.util.Queue; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -105,6 +104,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final CommandHandler commandHandler; private final ChatTimeKeeper timeKeeper = new ChatTimeKeeper(); + private CompletableFuture configSwitchFuture; + /** * Constructs a client play session handler. * @@ -151,12 +152,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { - Collection channels = server.getChannelRegistrar() - .getChannelsForProtocol(player.getProtocolVersion()); + configSwitchFuture = new CompletableFuture<>(); + Collection channels = + server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels); player.getConnection().write(register); - player.getKnownChannels().addAll(channels); } } @@ -186,7 +187,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ClientSettings packet) { player.setPlayerSettings(packet); - return false; // will forward onto the server + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return true; + } + player.getConnectedServer().ensureConnected().write(packet); + return true; // will forward onto the server } @Override @@ -289,13 +296,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null; if (serverConn != null && backendConn != null) { if (backendConn.getState() != StateRegistry.PLAY) { - logger.warn( - "A plugin message was received while the backend server was not " - + "ready. Channel: {}. Packet discarded.", - packet.getChannel()); + logger.warn("A plugin message was received while the backend server was not " + + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); - player.getKnownChannels().addAll(channels); List channelIdentifiers = new ArrayList<>(); for (String channel : channels) { try { @@ -309,7 +313,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isUnregister(packet)) { - player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); backendConn.write(packet.retain()); } else if (PluginMessageUtil.isMcBrand(packet)) { String brand = PluginMessageUtil.readBrandMessage(packet.content()); @@ -380,6 +383,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return player.onResourcePackResponse(packet.getStatus()); } + @Override + public boolean handle(FinishedUpdate packet) { + // Complete client switch + player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + MinecraftConnection smc = serverConnection.ensureConnected(); + CompletableFuture.runAsync(() -> { + smc.write(packet); + smc.setActiveSessionHandler(StateRegistry.CONFIG); + smc.setAutoReading(true); + }, smc.eventLoop()).exceptionally((ex) -> { + logger.error("Error forwarding config state acknowledgement to server:", ex); + return null; + }); + } + configSwitchFuture.complete(null); + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); @@ -443,6 +466,34 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } + /** + * Handles switching stages for swapping between servers. + * + * @return a future that completes when the switch is complete + */ + public CompletableFuture doSwitch() { + VelocityServerConnection existingConnection = player.getConnectedServer(); + + if (existingConnection != null) { + // Shut down the existing server connection. + player.setConnectedServer(null); + existingConnection.disconnect(); + + // Send keep alive to try to avoid timeouts + player.sendKeepAlive(); + + // Config state clears everything in the client. No need to clear later. + spawned = false; + serverBossBars.clear(); + player.clearPlayerListHeaderAndFooterSilent(); + player.getTabList().clearAllSilent(); + } + + player.switchToConfigState(); + + return configSwitchFuture; + } + /** * Handles the {@code JoinGame} packet. This function is responsible for handling the client-side * switching servers in Velocity. @@ -485,10 +536,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } serverBossBars.clear(); - // Tell the server about this client's plugin message channels. + // Tell the server about the proxy's plugin message channels. ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - if (!player.getKnownChannels().isEmpty()) { - serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getKnownChannels())); + final Collection channels = server.getChannelRegistrar() + .getChannelsForProtocol(serverMc.getProtocolVersion()); + if (!channels.isEmpty()) { + serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); } // If we had plugin messages queued during login/FML handshake, send them now. @@ -498,7 +551,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } // Clear any title from the previous server. - if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { player.getConnection().delayedWrite( GenericTitlePacket.constructTitlePacket(GenericTitlePacket.ActionType.RESET, player.getProtocolVersion())); @@ -521,7 +574,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // improving compatibility with mods. final Respawn respawn = Respawn.fromJoinGame(joinGame); - if (player.getProtocolVersion().compareTo(MINECRAFT_1_16) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_16) < 0) { // Before Minecraft 1.16, we could not switch to the same dimension without sending an // additional respawn. On older versions of Minecraft this forces the client to perform // garbage collection which adds additional latency. @@ -563,7 +616,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String commandLabel = command.substring(0, commandEndPosition); if (!server.getCommandManager().hasCommand(commandLabel)) { - if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide // additional tab completion support. outstandingTabComplete = packet; @@ -605,7 +658,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } private boolean handleRegularTabComplete(TabCompleteRequest packet) { - if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide // additional tab completion support. outstandingTabComplete = packet; @@ -636,7 +689,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String command = request.getCommand().substring(1); server.getCommandManager().offerBrigadierSuggestions(player, command) .thenAcceptAsync(offers -> { - boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0; + boolean legacy = + player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0; try { for (Suggestion suggestion : offers.getList()) { String offer = suggestion.getText(); @@ -660,9 +714,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } }, player.getConnection().eventLoop()).exceptionally((ex) -> { logger.error( - "Exception while finishing command tab completion, with request {} and response {}", - request, - response, ex); + "Exception while finishing command tab completion," + + " with request {} and response {}", + request, response, ex); return null; }); } @@ -681,9 +735,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getConnection().write(response); }, player.getConnection().eventLoop()).exceptionally((ex) -> { logger.error( - "Exception while finishing regular tab completion, with request {} and response{}", - request, - response, ex); + "Exception while finishing regular tab completion," + + " with request {} and response{}", + request, response, ex); return null; }); } @@ -703,5 +757,4 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } } - } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 15a960e8d..8c3699402 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -59,6 +59,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; @@ -69,6 +70,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatType; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.InternalTabList; @@ -77,13 +79,12 @@ import com.velocitypowered.proxy.tablist.VelocityTabList; import com.velocitypowered.proxy.tablist.VelocityTabListLegacy; import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.DurationUtils; -import com.velocitypowered.proxy.util.collect.CappedSet; +import com.velocitypowered.proxy.util.TranslatableMapper; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.ArrayDeque; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; @@ -101,9 +102,6 @@ import net.kyori.adventure.platform.facet.FacetPointers; import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.KeybindComponent; -import net.kyori.adventure.text.TranslatableComponent; -import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -126,12 +124,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = - PlainTextComponentSerializer.builder() - .flattener(ComponentFlattener.basic().toBuilder() - .mapper(KeybindComponent.class, c -> "") - .mapper(TranslatableComponent.class, TranslatableComponent::key) - .build()) - .build(); + PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class); @@ -156,24 +149,23 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final InternalTabList tabList; private final VelocityServer server; private ClientConnectionPhase connectionPhase; - private final Collection knownChannels; private final CompletableFuture teardownFuture = new CompletableFuture<>(); private @MonotonicNonNull List serversToTry = null; private @MonotonicNonNull Boolean previousResourceResponse; private final Queue outstandingResourcePacks = new ArrayDeque<>(); private @Nullable ResourcePackInfo pendingResourcePack; private @Nullable ResourcePackInfo appliedResourcePack; - private final @NotNull Pointers pointers = Player.super.pointers().toBuilder() - .withDynamic(Identity.UUID, this::getUniqueId) - .withDynamic(Identity.NAME, this::getUsername) - .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) - .withDynamic(Identity.LOCALE, this::getEffectiveLocale) - .withStatic(PermissionChecker.POINTER, getPermissionChecker()) - .withStatic(FacetPointers.TYPE, Type.PLAYER) - .build(); + private final @NotNull Pointers pointers = + Player.super.pointers().toBuilder().withDynamic(Identity.UUID, this::getUniqueId) + .withDynamic(Identity.NAME, this::getUsername) + .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) + .withDynamic(Identity.LOCALE, this::getEffectiveLocale) + .withStatic(PermissionChecker.POINTER, getPermissionChecker()) + .withStatic(FacetPointers.TYPE, Type.PLAYER).build(); private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; private @Nullable IdentifiedKey playerKey; + private @Nullable ClientSettings clientSettingsPacket; private final ChatQueue chatQueue; private final ChatBuilderFactory chatBuilderFactory; @@ -186,7 +178,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.virtualHost = virtualHost; this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.connectionPhase = connection.getType().getInitialClientPhase(); - this.knownChannels = CappedSet.create(MAX_PLUGIN_CHANNELS); this.onlineMode = onlineMode; if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { @@ -283,11 +274,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return settings == null ? ClientSettingsWrapper.DEFAULT : this.settings; } + public ClientSettings getClientSettingsPacket() { + return clientSettingsPacket; + } + @Override public boolean hasSentPlayerSettings() { return settings != null; } + public void setClientSettingsPacket(ClientSettings clientSettingsPacket) { + this.clientSettingsPacket = clientSettingsPacket; + } + void setPlayerSettings(ClientSettings settings) { ClientSettingsWrapper cs = new ClientSettingsWrapper(settings); this.settings = cs; @@ -542,8 +541,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } @Override - public void clearHeaderAndFooter() { - tabList.clearHeaderAndFooter(); + public void clearPlayerListHeaderAndFooter() { + clearPlayerListHeaderAndFooterSilent(); + if (this.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { + this.connection.write(HeaderAndFooter.reset()); + } + } + + public void clearPlayerListHeaderAndFooterSilent() { + this.playerListHeader = Component.empty(); + this.playerListFooter = Component.empty(); } @Override @@ -679,8 +686,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, ServerKickResult result; if (kickedFromCurrent) { Optional next = getNextServerToTry(rs); - result = next.map(RedirectPlayer::create) - .orElseGet(() -> DisconnectPlayer.create(friendlyReason)); + result = + next.map(RedirectPlayer::create).orElseGet(() -> DisconnectPlayer.create(friendlyReason)); } else { // If we were kicked by going to another server, the connection should not be in flight if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) { @@ -694,86 +701,83 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason, - boolean kickedFromCurrent) { - server.getEventManager().fire(originalEvent) - .thenAcceptAsync(event -> { - // There can't be any connection in flight now. - connectionInFlight = null; + boolean kickedFromCurrent) { + server.getEventManager().fire(originalEvent).thenAcceptAsync(event -> { + // There can't be any connection in flight now. + connectionInFlight = null; - // Make sure we clear the current connected server as the connection is invalid. - VelocityServerConnection previousConnection = connectedServer; - if (kickedFromCurrent) { - connectedServer = null; - } + // Make sure we clear the current connected server as the connection is invalid. + VelocityServerConnection previousConnection = connectedServer; + if (kickedFromCurrent) { + connectedServer = null; + } - if (!isActive()) { - // If the connection is no longer active, it makes no sense to try and recover it. - return; - } + if (!isActive()) { + // If the connection is no longer active, it makes no sense to try and recover it. + return; + } - if (event.getResult() instanceof DisconnectPlayer) { - DisconnectPlayer res = (DisconnectPlayer) event.getResult(); - disconnect(res.getReasonComponent()); - } else if (event.getResult() instanceof RedirectPlayer) { - RedirectPlayer res = (RedirectPlayer) event.getResult(); - createConnectionRequest(res.getServer(), previousConnection) - .connect() - .whenCompleteAsync((status, throwable) -> { - if (throwable != null) { - handleConnectionException(status != null ? status.getAttemptedConnection() - : res.getServer(), throwable, true); - return; + if (event.getResult() instanceof DisconnectPlayer) { + DisconnectPlayer res = (DisconnectPlayer) event.getResult(); + disconnect(res.getReasonComponent()); + } else if (event.getResult() instanceof RedirectPlayer) { + RedirectPlayer res = (RedirectPlayer) event.getResult(); + createConnectionRequest(res.getServer(), previousConnection).connect() + .whenCompleteAsync((status, throwable) -> { + if (throwable != null) { + handleConnectionException( + status != null ? status.getAttemptedConnection() : res.getServer(), throwable, + true); + return; + } + + switch (status.getStatus()) { + // Impossible/nonsensical cases + case ALREADY_CONNECTED: + logger.error("{}: already connected to {}", this, + status.getAttemptedConnection().getServerInfo().getName()); + break; + case CONNECTION_IN_PROGRESS: + // Fatal case + case CONNECTION_CANCELLED: + Component fallbackMsg = res.getMessageComponent(); + if (fallbackMsg == null) { + fallbackMsg = friendlyReason; } - - switch (status.getStatus()) { - // Impossible/nonsensical cases - case ALREADY_CONNECTED: - logger.error("{}: already connected to {}", - this, - status.getAttemptedConnection().getServerInfo().getName() - ); - break; - case CONNECTION_IN_PROGRESS: - // Fatal case - case CONNECTION_CANCELLED: - Component fallbackMsg = res.getMessageComponent(); - if (fallbackMsg == null) { - fallbackMsg = friendlyReason; - } - disconnect(status.getReasonComponent().orElse(fallbackMsg)); - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); - handleConnectionException(res.getServer(), Disconnect.create(reason, - getProtocolVersion()), ((Impl) status).isSafe()); - break; - case SUCCESS: - Component requestedMessage = res.getMessageComponent(); - if (requestedMessage == null) { - requestedMessage = friendlyReason; - } - if (requestedMessage != Component.empty()) { - sendMessage(requestedMessage); - } - break; - default: - // The only remaining value is successful (no need to do anything!) - break; + disconnect(status.getReasonComponent().orElse(fallbackMsg)); + break; + case SERVER_DISCONNECTED: + Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + handleConnectionException(res.getServer(), + Disconnect.create(reason, getProtocolVersion()), ((Impl) status).isSafe()); + break; + case SUCCESS: + Component requestedMessage = res.getMessageComponent(); + if (requestedMessage == null) { + requestedMessage = friendlyReason; } - }, connection.eventLoop()); - } else if (event.getResult() instanceof Notify) { - Notify res = (Notify) event.getResult(); - if (event.kickedDuringServerConnect() && previousConnection != null) { - sendMessage(Identity.nil(), res.getMessageComponent()); - } else { - disconnect(res.getMessageComponent()); - } - } else { - // In case someone gets creative, assume we want to disconnect the player. - disconnect(friendlyReason); - } - }, connection.eventLoop()); + if (requestedMessage != Component.empty()) { + sendMessage(requestedMessage); + } + break; + default: + // The only remaining value is successful (no need to do anything!) + break; + } + }, connection.eventLoop()); + } else if (event.getResult() instanceof Notify) { + Notify res = (Notify) event.getResult(); + if (event.kickedDuringServerConnect() && previousConnection != null) { + sendMessage(Identity.nil(), res.getMessageComponent()); + } else { + disconnect(res.getMessageComponent()); + } + } else { + // In case someone gets creative, assume we want to disconnect the player. + disconnect(friendlyReason); + } + }, connection.eventLoop()); } /** @@ -1026,6 +1030,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return pendingResourcePack; } + /** + * Clears the applied resource pack field. + */ + public void clearAppliedResourcePack() { + appliedResourcePack = null; + } + /** * Processes a client response to a sent resource-pack. */ @@ -1073,18 +1084,42 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; } + /** + * Gives an indication about the previous resource pack responses. + */ + public @Nullable Boolean getPreviousResourceResponse() { + return previousResourceResponse; + } + /** * Sends a {@link KeepAlive} packet to the player with a random ID. The response will be ignored * by Velocity as it will not match the ID last sent by the server. */ public void sendKeepAlive() { - if (connection.getState() == StateRegistry.PLAY) { + if (connection.getState() == StateRegistry.PLAY + || connection.getState() == StateRegistry.CONFIG) { KeepAlive keepAlive = new KeepAlive(); keepAlive.setRandomId(ThreadLocalRandom.current().nextLong()); connection.write(keepAlive); } } + /** + * Switches the connection to the client into config state. + */ + public void switchToConfigState() { + CompletableFuture.runAsync(() -> { + connection.write(new StartUpdate()); + connection.getChannel().pipeline() + .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); + // Make sure we don't send any play packets to the player after update start + connection.addPlayPacketQueueHandler(); + }, connection.eventLoop()).exceptionally((ex) -> { + logger.error("Error switching player connection to config state:", ex); + return null; + }); + } + /** * Gets the current "phase" of the connection, mostly used for tracking modded negotiation for * legacy forge servers and provides methods for performing phase specific actions. @@ -1104,15 +1139,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, this.connectionPhase = connectionPhase; } - /** - * Return all the plugin message channels "known" to the client. - * - * @return the channels - */ - public Collection getKnownChannels() { - return knownChannels; - } - @Override public @Nullable IdentifiedKey getIdentifiedKey() { return playerKey; @@ -1161,37 +1187,34 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } private CompletableFuture internalConnect() { - return this.getInitialStatus() - .thenCompose(initialCheck -> { - if (initialCheck.isPresent()) { - return completedFuture(plainResult(initialCheck.get(), toConnect)); - } + return this.getInitialStatus().thenCompose(initialCheck -> { + if (initialCheck.isPresent()) { + return completedFuture(plainResult(initialCheck.get(), toConnect)); + } - ServerPreConnectEvent event = new ServerPreConnectEvent(ConnectedPlayer.this, - toConnect, previousServer); - return server.getEventManager().fire(event) - .thenComposeAsync(newEvent -> { - Optional newDest = newEvent.getResult().getServer(); - if (!newDest.isPresent()) { - return completedFuture( - plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect) - ); - } + ServerPreConnectEvent event = + new ServerPreConnectEvent(ConnectedPlayer.this, toConnect, previousServer); + return server.getEventManager().fire(event).thenComposeAsync(newEvent -> { + Optional newDest = newEvent.getResult().getServer(); + if (!newDest.isPresent()) { + return completedFuture( + plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect)); + } - RegisteredServer realDestination = newDest.get(); - Optional check = checkServer(realDestination); - if (check.isPresent()) { - return completedFuture(plainResult(check.get(), realDestination)); - } + RegisteredServer realDestination = newDest.get(); + Optional check = checkServer(realDestination); + if (check.isPresent()) { + return completedFuture(plainResult(check.get(), realDestination)); + } - VelocityRegisteredServer vrs = (VelocityRegisteredServer) realDestination; - VelocityServerConnection con = new VelocityServerConnection(vrs, - previousServer, ConnectedPlayer.this, server); - connectionInFlight = con; - return con.connect().whenCompleteAsync( - (result, exception) -> this.resetIfInFlightIs(con), connection.eventLoop()); - }, connection.eventLoop()); - }); + VelocityRegisteredServer vrs = (VelocityRegisteredServer) realDestination; + VelocityServerConnection con = + new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server); + connectionInFlight = con; + return con.connect().whenCompleteAsync((result, exception) -> this.resetIfInFlightIs(con), + connection.eventLoop()); + }, connection.eventLoop()); + }); } private void resetIfInFlightIs(VelocityServerConnection establishedConnection) { @@ -1202,50 +1225,46 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, @Override public CompletableFuture connect() { - return this.internalConnect() - .whenCompleteAsync((status, throwable) -> { - if (status != null && !status.isSuccessful()) { - if (!status.isSafe()) { - handleConnectionException(status.getAttemptedConnection(), throwable, false); - } - } - }, connection.eventLoop()) - .thenApply(x -> x); + return this.internalConnect().whenCompleteAsync((status, throwable) -> { + if (status != null && !status.isSuccessful()) { + if (!status.isSafe()) { + handleConnectionException(status.getAttemptedConnection(), throwable, false); + } + } + }, connection.eventLoop()).thenApply(x -> x); } @Override public CompletableFuture connectWithIndication() { - return internalConnect() - .whenCompleteAsync((status, throwable) -> { - if (throwable != null) { - // TODO: The exception handling from this is not very good. Find a better way. - handleConnectionException(status != null ? status.getAttemptedConnection() - : toConnect, throwable, true); - return; - } + return internalConnect().whenCompleteAsync((status, throwable) -> { + if (throwable != null) { + // TODO: The exception handling from this is not very good. Find a better way. + handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect, + throwable, true); + return; + } - switch (status.getStatus()) { - case ALREADY_CONNECTED: - sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); - break; - case CONNECTION_IN_PROGRESS: - sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); - break; - case CONNECTION_CANCELLED: - // Ignored; the plugin probably already handled this. - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); - handleConnectionException(toConnect, Disconnect.create(reason, - getProtocolVersion()), status.isSafe()); - break; - default: - // The only remaining value is successful (no need to do anything!) - break; - } - }, connection.eventLoop()) - .thenApply(Result::isSuccessful); + switch (status.getStatus()) { + case ALREADY_CONNECTED: + sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); + break; + case CONNECTION_IN_PROGRESS: + sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); + break; + case CONNECTION_CANCELLED: + // Ignored; the plugin probably already handled this. + break; + case SERVER_DISCONNECTED: + Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + handleConnectionException(toConnect, Disconnect.create(reason, getProtocolVersion()), + status.isSafe()); + break; + default: + // The only remaining value is successful (no need to do anything!) + break; + } + }, connection.eventLoop()).thenApply(Result::isSuccessful); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index e814efaa3..079ce035f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -47,8 +47,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** * The initial handler used when a connection is established to the proxy. This will either - * transition to {@link StatusSessionHandler} or {@link InitialLoginSessionHandler} as soon - * as the handshake packet is received. + * transition to {@link StatusSessionHandler} or {@link InitialLoginSessionHandler} as soon as the + * handshake packet is received. */ public class HandshakeSessionHandler implements MinecraftSessionHandler { @@ -65,9 +65,9 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyPing packet) { connection.setProtocolVersion(ProtocolVersion.LEGACY); - StatusSessionHandler handler = new StatusSessionHandler(server, - new LegacyInboundConnection(connection, packet)); - connection.setSessionHandler(handler); + StatusSessionHandler handler = + new StatusSessionHandler(server, new LegacyInboundConnection(connection, packet)); + connection.setActiveSessionHandler(StateRegistry.STATUS, handler); handler.handle(packet); return true; } @@ -90,13 +90,13 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus()); connection.close(true); } else { - connection.setState(nextState); connection.setProtocolVersion(handshake.getProtocolVersion()); connection.setAssociation(ic); switch (nextState) { case STATUS: - connection.setSessionHandler(new StatusSessionHandler(server, ic)); + connection.setActiveSessionHandler(StateRegistry.STATUS, + new StatusSessionHandler(server, ic)); break; case LOGIN: this.handleLogin(handshake, ic); @@ -140,14 +140,15 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { // and lower, otherwise IP information will never get forwarded. if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - ic.disconnectQuietly(Component.translatable( - "velocity.error.modern-forwarding-needs-new-client")); + ic.disconnectQuietly( + Component.translatable("velocity.error.modern-forwarding-needs-new-client")); return; } LoginInboundConnection lic = new LoginInboundConnection(ic); server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(lic)); - connection.setSessionHandler(new InitialLoginSessionHandler(server, connection, lic)); + connection.setActiveSessionHandler(StateRegistry.LOGIN, + new InitialLoginSessionHandler(server, connection, lic)); } private ConnectionType getHandshakeConnectionType(Handshake handshake) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java index fcab2d62d..f264d262e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java @@ -24,7 +24,6 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import org.apache.logging.log4j.LogManager; @@ -55,15 +54,7 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler { return true; } - if (PluginMessageUtil.isRegister(packet)) { - player.getKnownChannels().addAll(PluginMessageUtil.getChannels(packet)); - serverConn.ensureConnected().write(packet.retain()); - return true; - } else if (PluginMessageUtil.isUnregister(packet)) { - player.getKnownChannels().removeAll(PluginMessageUtil.getChannels(packet)); - serverConn.ensureConnected().write(packet.retain()); - return true; - } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { + if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) { return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index 01bef5f92..1409dc4ba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; import com.velocitypowered.proxy.protocol.packet.EncryptionResponse; @@ -120,47 +121,45 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { this.login = packet; PreLoginEvent event = new PreLoginEvent(inbound, login.getUsername()); - server.getEventManager().fire(event) - .thenRunAsync(() -> { - if (mcConnection.isClosed()) { - // The player was disconnected - return; + server.getEventManager().fire(event).thenRunAsync(() -> { + if (mcConnection.isClosed()) { + // The player was disconnected + return; + } + + PreLoginComponentResult result = event.getResult(); + Optional disconnectReason = result.getReasonComponent(); + if (disconnectReason.isPresent()) { + // The component is guaranteed to be provided if the connection was denied. + inbound.disconnect(disconnectReason.get()); + return; + } + + inbound.loginEventFired(() -> { + if (mcConnection.isClosed()) { + // The player was disconnected + return; + } + + mcConnection.eventLoop().execute(() -> { + if (!result.isForceOfflineMode() + && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) { + // Request encryption. + EncryptionRequest request = generateEncryptionRequest(); + this.verify = Arrays.copyOf(request.getVerifyToken(), 4); + mcConnection.write(request); + this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; + } else { + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, + GameProfile.forOfflinePlayer(login.getUsername()), false)); } - - PreLoginComponentResult result = event.getResult(); - Optional disconnectReason = result.getReasonComponent(); - if (disconnectReason.isPresent()) { - // The component is guaranteed to be provided if the connection was denied. - inbound.disconnect(disconnectReason.get()); - return; - } - - inbound.loginEventFired(() -> { - if (mcConnection.isClosed()) { - // The player was disconnected - return; - } - - mcConnection.eventLoop().execute(() -> { - if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() - || result.isOnlineModeAllowed())) { - // Request encryption. - EncryptionRequest request = generateEncryptionRequest(); - this.verify = Arrays.copyOf(request.getVerifyToken(), 4); - mcConnection.write(request); - this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; - } else { - mcConnection.setSessionHandler(new AuthSessionHandler( - server, inbound, GameProfile.forOfflinePlayer(login.getUsername()), false - )); - } - }); - }); - }, mcConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception in pre-login stage", ex); - return null; }); + }); + }, mcConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception in pre-login stage", ex); + return null; + }); return true; } @@ -246,13 +245,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { } } // All went well, initialize the session. - mcConnection.setSessionHandler(new AuthSessionHandler( - server, inbound, profile, true - )); + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, profile, true)); } else if (profileResponse.getStatusCode() == 204) { // Apparently an offline-mode user logged onto this online-mode proxy. - inbound.disconnect(Component.translatable("velocity.error.online-mode-only", - NamedTextColor.RED)); + inbound.disconnect( + Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)); } else { // Something else went wrong logger.error( diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java index 5d7542170..19b09c122 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java @@ -76,9 +76,9 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { }, /** - * The Mod list is sent to the server, captured by Velocity. Transition to - * {@link #WAITING_SERVER_DATA} when an ACK is sent, which indicates to the server to start - * sending state data. + * The Mod list is sent to the server, captured by Velocity. Transition to {@link + * #WAITING_SERVER_DATA} when an ACK is sent, which indicates to the server to start sending state + * data. */ MOD_LIST(LegacyForgeConstants.ACK_DISCRIMINATOR) { @Override @@ -138,11 +138,10 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { /** * The handshake is complete. The handshake can be reset. * - *

Note that a successful connection to a server does not mean that - * we will be in this state. After a handshake reset, if the next server is vanilla we will still - * be in the {@link #NOT_STARTED} phase, which means we must NOT send a reset packet. This is - * handled by overriding the {@link #resetConnectionPhase(ConnectedPlayer)} in this element (it is - * usually a no-op).

+ *

Note that a successful connection to a server does not mean that we will be in this state. + * After a handshake reset, if the next server is vanilla we will still be in the {@link + * #NOT_STARTED} phase, which means we must NOT send a reset packet. This is handled by overriding + * the {@link #resetConnectionPhase(ConnectedPlayer)} in this element (it is usually a no-op). */ COMPLETE(null) { @Override @@ -165,7 +164,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { // just in case the timing is awful player.sendKeepAlive(); - MinecraftSessionHandler handler = backendConn.getSessionHandler(); + MinecraftSessionHandler handler = backendConn.getActiveSessionHandler(); if (handler instanceof ClientPlaySessionHandler) { ((ClientPlaySessionHandler) handler).flushQueuedMessages(); } @@ -182,8 +181,8 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { * * @param packetToAdvanceOn The ID of the packet discriminator that indicates that the client has * moved onto a new phase, and as such, Velocity should do so too - * (inspecting {@link #nextPhase()}. A null indicates there is no further - * phase to transition to. + * (inspecting {@link #nextPhase()}. A null indicates there is + * no further phase to transition to. */ LegacyForgeHandshakeClientPhase(Integer packetToAdvanceOn) { this.packetToAdvanceOn = packetToAdvanceOn; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java new file mode 100644 index 000000000..1bca74515 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.registry; + +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.Nullable; + +/** + * Holds the registry data that is sent + * to the client during the config stage. + */ +public class ClientConfigData { + + private final @Nullable VelocityResourcePackInfo resourcePackInfo; + private final DataTag tag; + private final RegistrySync registry; + private final Key[] features; + private final String brand; + + private ClientConfigData(@Nullable VelocityResourcePackInfo resourcePackInfo, DataTag tag, + RegistrySync registry, Key[] features, String brand) { + this.resourcePackInfo = resourcePackInfo; + this.tag = tag; + this.registry = registry; + this.features = features; + this.brand = brand; + } + + public RegistrySync getRegistry() { + return registry; + } + + public DataTag getTag() { + return tag; + } + + public Key[] getFeatures() { + return features; + } + + public @Nullable VelocityResourcePackInfo getResourcePackInfo() { + return resourcePackInfo; + } + + public String getBrand() { + return brand; + } + + /** + * Creates a new builder. + * + * @return ClientConfigData.Builder + */ + public static ClientConfigData.Builder builder() { + return new Builder(); + } + + /** + * Builder for ClientConfigData. + */ + public static class Builder { + private VelocityResourcePackInfo resourcePackInfo; + private DataTag tag; + private RegistrySync registry; + private Key[] features; + private String brand; + + private Builder() { + } + + /** + * Clears the builder. + */ + public void clear() { + this.resourcePackInfo = null; + this.tag = null; + this.registry = null; + this.features = null; + this.brand = null; + } + + public Builder resourcePack(@Nullable VelocityResourcePackInfo resourcePackInfo) { + this.resourcePackInfo = resourcePackInfo; + return this; + } + + public Builder dataTag(DataTag tag) { + this.tag = tag; + return this; + } + + public Builder registry(RegistrySync registry) { + this.registry = registry; + return this; + } + + public Builder features(Key[] features) { + this.features = features; + return this; + } + + public Builder brand(String brand) { + this.brand = brand; + return this; + } + + public ClientConfigData build() { + return new ClientConfigData(resourcePackInfo, tag, registry, features, brand); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java new file mode 100644 index 000000000..9a7d0de73 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.registry; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a data tag. + */ +public class DataTag { + private final ImmutableList entrySets; + + public DataTag(ImmutableList entrySets) { + this.entrySets = entrySets; + } + + /** + * Returns the entry sets. + * + * @return List of entry sets + */ + public List getEntrySets() { + return entrySets; + } + + /** + * Represents a data tag set. + */ + public static class Set implements Keyed { + + private final Key key; + private final ImmutableList entries; + + public Set(Key key, ImmutableList entries) { + this.key = key; + this.entries = entries; + } + + /** + * Returns the entries. + * + * @return List of entries + */ + public List getEntries() { + return entries; + } + + @Override + public @NotNull Key key() { + return key; + } + } + + /** + * Represents a data tag entry. + */ + public static class Entry implements Keyed { + + private final Key key; + private final int[] elements; + + public Entry(Key key, int[] elements) { + this.key = key; + this.elements = elements; + } + + public int[] getElements() { + return elements; + } + + @Override + public @NotNull Key key() { + return key; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index 08bc04c07..fe360d4d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -35,8 +35,7 @@ import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import net.kyori.adventure.translation.GlobalTranslator; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.minecrell.terminalconsole.SimpleTerminalConsole; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -55,6 +54,8 @@ import org.jline.reader.LineReaderBuilder; public final class VelocityConsole extends SimpleTerminalConsole implements ConsoleCommandSource { private static final Logger logger = LogManager.getLogger(VelocityConsole.class); + private static final ComponentLogger componentLogger = ComponentLogger + .logger(VelocityConsole.class); private final VelocityServer server; private PermissionFunction permissionFunction = ALWAYS_TRUE; @@ -72,9 +73,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons @Override public void sendMessage(@NonNull Identity identity, @NonNull Component message, @NonNull MessageType messageType) { - Component translated = GlobalTranslator.render(message, ClosestLocaleMatcher.INSTANCE - .lookupClosest(Locale.getDefault())); - logger.info(LegacyComponentSerializer.legacySection().serialize(translated)); + componentLogger.info(message); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index 62f9681eb..a55147570 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -318,8 +318,7 @@ public class VelocityEventManager implements EventManager { if (returnType != void.class && continuationType == Continuation.class) { errors.add("method return type must be void if a continuation parameter is provided"); } else if (returnType != void.class && returnType != EventTask.class) { - errors.add("method return type must be void, EventTask, " - + "EventTask.Basic or EventTask.WithContinuation"); + errors.add("method return type must be void or EventTask"); } else if (returnType == EventTask.class) { asyncType = AsyncType.SOMETIMES; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 4126c9b12..ddd40b4a7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -216,9 +216,11 @@ public final class ConnectionManager { } /** - * Closes all endpoints. + * Closes all the currently registered endpoints. + * + * @param interrupt should closing forward interruptions */ - public void shutdown() { + public void closeEndpoints(boolean interrupt) { for (final Map.Entry entry : this.endpoints.entrySet()) { final SocketAddress address = entry.getKey(); final Endpoint endpoint = entry.getValue(); @@ -227,14 +229,26 @@ public final class ConnectionManager { // should have a chance to be notified before the server stops accepting connections. server.getEventManager().fire(new ListenerCloseEvent(address, endpoint.getType())).join(); - try { - LOGGER.info("Closing endpoint {}", address); - endpoint.getChannel().close().sync(); - } catch (final InterruptedException e) { - LOGGER.info("Interrupted whilst closing endpoint", e); - Thread.currentThread().interrupt(); + LOGGER.info("Closing endpoint {}", address); + if (interrupt) { + try { + endpoint.getChannel().close().sync(); + } catch (final InterruptedException e) { + LOGGER.info("Interrupted whilst closing endpoint", e); + Thread.currentThread().interrupt(); + } + } else { + endpoint.getChannel().close().syncUninterruptibly(); } } + this.endpoints.clear(); + } + + /** + * Closes all endpoints. + */ + public void shutdown() { + this.closeEndpoints(true); this.resolver.shutdown(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index dff8d2236..27ec4ba8b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -35,6 +35,7 @@ public class Connections { public static final String MINECRAFT_DECODER = "minecraft-decoder"; public static final String MINECRAFT_ENCODER = "minecraft-encoder"; public static final String READ_TIMEOUT = "read-timeout"; + public static final String PLAY_PACKET_QUEUE = "play-packet-queue"; private Connections() { throw new AssertionError(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java index 62f38ebd7..ef8e6b1cb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java @@ -29,6 +29,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; @@ -67,7 +68,8 @@ public class ServerChannelInitializer extends ChannelInitializer { .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND)); final MinecraftConnection connection = new MinecraftConnection(ch, this.server); - connection.setSessionHandler(new HandshakeSessionHandler(connection, this.server)); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, + new HandshakeSessionHandler(connection, this.server)); ch.pipeline().addLast(Connections.HANDLER, connection); if (this.server.getConfiguration().isProxyProtocol()) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index a862a2b53..2c6f752f7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -94,7 +94,7 @@ public class VelocityPluginManager implements PluginManager { for (Path path : stream) { try { found.add(loader.loadCandidate(path)); - } catch (Exception e) { + } catch (Throwable e) { logger.error("Unable to load plugin {}", path, e); } } @@ -126,7 +126,7 @@ public class VelocityPluginManager implements PluginManager { VelocityPluginContainer container = new VelocityPluginContainer(realPlugin); pluginContainers.put(container, loader.createModule(container)); loadedPluginsById.add(realPlugin.getId()); - } catch (Exception e) { + } catch (Throwable e) { logger.error("Can't create module for plugin {}", candidate.getId(), e); } } @@ -153,7 +153,7 @@ public class VelocityPluginManager implements PluginManager { try { loader.createPlugin(container, plugin.getValue(), commonModule); - } catch (Exception e) { + } catch (Throwable e) { logger.error("Can't create plugin {}", description.getId(), e); continue; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java index 40b00df7c..5e757708e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/VelocityPluginContainer.java @@ -71,4 +71,8 @@ public class VelocityPluginContainer implements PluginContainer { return this.service; } + + public boolean hasExecutorService() { + return this.service != null; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java index 8e59816af..a6331dd48 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import java.nio.file.Path; import java.util.concurrent.ExecutorService; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +50,7 @@ class VelocityPluginModule implements Module { binder.bind(description.getMainClass()).in(Scopes.SINGLETON); binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.getId())); + binder.bind(ComponentLogger.class).toInstance(ComponentLogger.logger(description.getId())); binder.bind(Path.class).annotatedWith(DataDirectory.class) .toInstance(basePluginPath.resolve(description.getId())); binder.bind(PluginDescription.class).toInstance(description); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 97b40def7..e65ee056b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -41,6 +41,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -202,8 +203,7 @@ public enum ProtocolUtils { buf.readableBytes()); String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8); buf.skipBytes(length); - checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", - str.length(), cap); + checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap); return str; } @@ -219,6 +219,59 @@ public enum ProtocolUtils { buf.writeCharSequence(str, StandardCharsets.UTF_8); } + /** + * Reads a standard Mojang Text namespaced:key from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key + */ + public static Key readKey(ByteBuf buf) { + return Key.key(readString(buf), Key.DEFAULT_SEPARATOR); + } + + /** + * Writes a standard Mojang Text namespaced:key to the buffer. + * + * @param buf the buffer to write to + * @param key the key to write + */ + public static void writeKey(ByteBuf buf, Key key) { + writeString(buf, key.asString()); + } + + /** + * Reads a standard Mojang Text namespaced:key array from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key array + */ + public static Key[] readKeyArray(ByteBuf buf) { + int length = readVarInt(buf); + checkFrame(length >= 0, "Got a negative-length array (%s)", length); + checkFrame(buf.isReadable(length), + "Trying to read an array that is too long (wanted %s, only have %s)", length, + buf.readableBytes()); + Key[] ret = new Key[length]; + + for (int i = 0; i < ret.length; i++) { + ret[i] = ProtocolUtils.readKey(buf); + } + return ret; + } + + /** + * Writes a standard Mojang Text namespaced:key array to the buffer. + * + * @param buf the buffer to write to + * @param keys the keys to write + */ + public static void writeKeyArray(ByteBuf buf, Key[] keys) { + writeVarInt(buf, keys.length); + for (Key key : keys) { + writeKey(buf, key); + } + } + public static byte[] readByteArray(ByteBuf buf) { return readByteArray(buf, DEFAULT_MAX_STRING_SIZE); } @@ -368,6 +421,38 @@ public enum ProtocolUtils { } } + /** + * Reads an Integer array from the {@code buf}. + * + * @param buf the buffer to read from + * @return the Integer array from the buffer + */ + public static int[] readVarIntArray(ByteBuf buf) { + int length = readVarInt(buf); + checkFrame(length >= 0, "Got a negative-length array (%s)", length); + checkFrame(buf.isReadable(length), + "Trying to read an array that is too long (wanted %s, only have %s)", length, + buf.readableBytes()); + int[] ret = new int[length]; + for (int i = 0; i < length; i++) { + ret[i] = readVarInt(buf); + } + return ret; + } + + /** + * Writes an Integer Array to the {@code buf}. + * + * @param buf the buffer to write to + * @param intArray the array to write + */ + public static void writeVarIntArray(ByteBuf buf, int[] intArray) { + writeVarInt(buf, intArray.length); + for (int i = 0; i < intArray.length; i++) { + writeVarInt(buf, intArray[i]); + } + } + /** * Writes a list of {@link com.velocitypowered.api.util.GameProfile.Property} to the buffer. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 9cb4aacdc..8ebb7eadc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -33,6 +33,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -55,8 +56,10 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; +import com.velocitypowered.proxy.protocol.packet.PingIdentify; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; @@ -72,6 +75,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponse; import com.velocitypowered.proxy.protocol.packet.TabCompleteRequest; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgement; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletion; import com.velocitypowered.proxy.protocol.packet.chat.SystemChat; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; @@ -79,6 +83,11 @@ import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.ActiveFeatures; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -98,9 +107,7 @@ import java.util.Objects; import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; -/** - * Registry of all Minecraft protocol states and the packets for each state. - */ +/** Registry of all Minecraft protocol states and the packets for each state. */ public enum StateRegistry { HANDSHAKE { @@ -111,15 +118,46 @@ public enum StateRegistry { }, STATUS { { - serverbound.register(StatusRequest.class, () -> StatusRequest.INSTANCE, - map(0x00, MINECRAFT_1_7_2, false)); - serverbound.register(StatusPing.class, StatusPing::new, - map(0x01, MINECRAFT_1_7_2, false)); + serverbound.register( + StatusRequest.class, () -> StatusRequest.INSTANCE, map(0x00, MINECRAFT_1_7_2, false)); + serverbound.register(StatusPing.class, StatusPing::new, map(0x01, MINECRAFT_1_7_2, false)); - clientbound.register(StatusResponse.class, StatusResponse::new, - map(0x00, MINECRAFT_1_7_2, false)); - clientbound.register(StatusPing.class, StatusPing::new, - map(0x01, MINECRAFT_1_7_2, false)); + clientbound.register( + StatusResponse.class, StatusResponse::new, map(0x00, MINECRAFT_1_7_2, false)); + clientbound.register(StatusPing.class, StatusPing::new, map(0x01, MINECRAFT_1_7_2, false)); + } + }, + CONFIG { + { + serverbound.register( + ClientSettings.class, ClientSettings::new, map(0x00, MINECRAFT_1_20_2, false)); + serverbound.register( + PluginMessage.class, PluginMessage::new, map(0x01, MINECRAFT_1_20_2, false)); + serverbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x02, MINECRAFT_1_20_2, false)); + serverbound.register(KeepAlive.class, KeepAlive::new, map(0x03, MINECRAFT_1_20_2, false)); + serverbound.register( + PingIdentify.class, PingIdentify::new, map(0x04, MINECRAFT_1_20_2, false)); + serverbound.register( + ResourcePackResponse.class, + ResourcePackResponse::new, + map(0x05, MINECRAFT_1_20_2, false)); + + clientbound.register( + PluginMessage.class, PluginMessage::new, map(0x00, MINECRAFT_1_20_2, false)); + clientbound.register(Disconnect.class, Disconnect::new, map(0x01, MINECRAFT_1_20_2, false)); + clientbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x02, MINECRAFT_1_20_2, false)); + clientbound.register(KeepAlive.class, KeepAlive::new, map(0x03, MINECRAFT_1_20_2, false)); + clientbound.register( + PingIdentify.class, PingIdentify::new, map(0x04, MINECRAFT_1_20_2, false)); + clientbound.register( + RegistrySync.class, RegistrySync::new, map(0x05, MINECRAFT_1_20_2, false)); + clientbound.register( + ResourcePackRequest.class, ResourcePackRequest::new, map(0x06, MINECRAFT_1_20_2, false)); + clientbound.register( + ActiveFeatures.class, ActiveFeatures::new, map(0x07, MINECRAFT_1_20_2, false)); + clientbound.register(TagsUpdate.class, TagsUpdate::new, map(0x08, MINECRAFT_1_20_2, false)); } }, PLAY { @@ -137,13 +175,20 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19, false), map(0x09, MINECRAFT_1_19_1, false), map(0x08, MINECRAFT_1_19_3, false), - map(0x09, MINECRAFT_1_19_4, false)); - serverbound.register(LegacyChat.class, LegacyChat::new, + map(0x09, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); + serverbound.register( + LegacyChat.class, + LegacyChat::new, map(0x01, MINECRAFT_1_7_2, false), map(0x02, MINECRAFT_1_9, false), map(0x03, MINECRAFT_1_12, false), map(0x02, MINECRAFT_1_12_1, false), map(0x03, MINECRAFT_1_14, MINECRAFT_1_18_2, false)); + serverbound.register( + ChatAcknowledgement.class, + ChatAcknowledgement::new, + map(0x03, MINECRAFT_1_19_3, false)); serverbound.register(KeyedPlayerCommand.class, KeyedPlayerCommand::new, map(0x03, MINECRAFT_1_19, false), map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); @@ -152,9 +197,13 @@ public enum StateRegistry { map(0x05, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); serverbound.register(SessionPlayerCommand.class, SessionPlayerCommand::new, map(0x04, MINECRAFT_1_19_3, false)); - serverbound.register(SessionPlayerChat.class, SessionPlayerChat::new, - map(0x05, MINECRAFT_1_19_3, false)); - serverbound.register(ClientSettings.class, ClientSettings::new, + serverbound.register( + SessionPlayerChat.class, + SessionPlayerChat::new, + map(0x05, MINECRAFT_1_19_3, MINECRAFT_1_20_2, false)); + serverbound.register( + ClientSettings.class, + ClientSettings::new, map(0x15, MINECRAFT_1_7_2, false), map(0x04, MINECRAFT_1_9, false), map(0x05, MINECRAFT_1_12, false), @@ -163,8 +212,11 @@ public enum StateRegistry { map(0x07, MINECRAFT_1_19, false), map(0x08, MINECRAFT_1_19_1, false), map(0x07, MINECRAFT_1_19_3, false), - map(0x08, MINECRAFT_1_19_4, false)); - serverbound.register(PluginMessage.class, PluginMessage::new, + map(0x08, MINECRAFT_1_19_4, false), + map(0x09, MINECRAFT_1_20_2, false)); + serverbound.register( + PluginMessage.class, + PluginMessage::new, map(0x17, MINECRAFT_1_7_2, false), map(0x09, MINECRAFT_1_9, false), map(0x0A, MINECRAFT_1_12, false), @@ -175,8 +227,11 @@ public enum StateRegistry { map(0x0C, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_1, false), map(0x0C, MINECRAFT_1_19_3, false), - map(0x0D, MINECRAFT_1_19_4, false)); - serverbound.register(KeepAlive.class, KeepAlive::new, + map(0x0D, MINECRAFT_1_19_4, false), + map(0x0F, MINECRAFT_1_20_2, false)); + serverbound.register( + KeepAlive.class, + KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), map(0x0C, MINECRAFT_1_12, false), @@ -188,8 +243,11 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_19, false), map(0x12, MINECRAFT_1_19_1, false), map(0x11, MINECRAFT_1_19_3, false), - map(0x12, MINECRAFT_1_19_4, false)); - serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, + map(0x12, MINECRAFT_1_19_4, false), + map(0x14, MINECRAFT_1_20_2, false)); + serverbound.register( + ResourcePackResponse.class, + ResourcePackResponse::new, map(0x19, MINECRAFT_1_8, false), map(0x16, MINECRAFT_1_9, false), map(0x18, MINECRAFT_1_12, false), @@ -198,16 +256,24 @@ public enum StateRegistry { map(0x20, MINECRAFT_1_16, false), map(0x21, MINECRAFT_1_16_2, false), map(0x23, MINECRAFT_1_19, false), - map(0x24, MINECRAFT_1_19_1, false)); + map(0x24, MINECRAFT_1_19_1, false), + map(0x27, MINECRAFT_1_20_2, false)); + serverbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x0B, MINECRAFT_1_20_2, false)); - clientbound.register(BossBar.class, BossBar::new, + clientbound.register( + BossBar.class, + BossBar::new, map(0x0C, MINECRAFT_1_9, false), map(0x0D, MINECRAFT_1_15, false), map(0x0C, MINECRAFT_1_16, false), map(0x0D, MINECRAFT_1_17, false), map(0x0A, MINECRAFT_1_19, false), - map(0x0B, MINECRAFT_1_19_4, false)); - clientbound.register(LegacyChat.class, LegacyChat::new, + map(0x0B, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); + clientbound.register( + LegacyChat.class, + LegacyChat::new, map(0x02, MINECRAFT_1_7_2, true), map(0x0F, MINECRAFT_1_9, true), map(0x0E, MINECRAFT_1_13, true), @@ -224,8 +290,11 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_17, false), map(0x0E, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_3, false), - map(0x0F, MINECRAFT_1_19_4, false)); - clientbound.register(AvailableCommands.class, AvailableCommands::new, + map(0x0F, MINECRAFT_1_19_4, false), + map(0x10, MINECRAFT_1_20_2, false)); + clientbound.register( + AvailableCommands.class, + AvailableCommands::new, map(0x11, MINECRAFT_1_13, false), map(0x12, MINECRAFT_1_15, false), map(0x11, MINECRAFT_1_16, false), @@ -233,8 +302,11 @@ public enum StateRegistry { map(0x12, MINECRAFT_1_17, false), map(0x0F, MINECRAFT_1_19, false), map(0x0E, MINECRAFT_1_19_3, false), - map(0x10, MINECRAFT_1_19_4, false)); - clientbound.register(PluginMessage.class, PluginMessage::new, + map(0x10, MINECRAFT_1_19_4, false), + map(0x11, MINECRAFT_1_20_2, false)); + clientbound.register( + PluginMessage.class, + PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), map(0x19, MINECRAFT_1_13, false), @@ -246,8 +318,11 @@ public enum StateRegistry { map(0x15, MINECRAFT_1_19, false), map(0x16, MINECRAFT_1_19_1, false), map(0x15, MINECRAFT_1_19_3, false), - map(0x17, MINECRAFT_1_19_4, false)); - clientbound.register(Disconnect.class, Disconnect::new, + map(0x17, MINECRAFT_1_19_4, false), + map(0x18, MINECRAFT_1_20_2, false)); + clientbound.register( + Disconnect.class, + Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), map(0x1B, MINECRAFT_1_13, false), @@ -259,8 +334,11 @@ public enum StateRegistry { map(0x17, MINECRAFT_1_19, false), map(0x19, MINECRAFT_1_19_1, false), map(0x17, MINECRAFT_1_19_3, false), - map(0x1A, MINECRAFT_1_19_4, false)); - clientbound.register(KeepAlive.class, KeepAlive::new, + map(0x1A, MINECRAFT_1_19_4, false), + map(0x1B, MINECRAFT_1_20_2, false)); + clientbound.register( + KeepAlive.class, + KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), map(0x21, MINECRAFT_1_13, false), @@ -272,8 +350,11 @@ public enum StateRegistry { map(0x1E, MINECRAFT_1_19, false), map(0x20, MINECRAFT_1_19_1, false), map(0x1F, MINECRAFT_1_19_3, false), - map(0x23, MINECRAFT_1_19_4, false)); - clientbound.register(JoinGame.class, JoinGame::new, + map(0x23, MINECRAFT_1_19_4, false), + map(0x24, MINECRAFT_1_20_2, false)); + clientbound.register( + JoinGame.class, + JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), map(0x25, MINECRAFT_1_13, false), @@ -285,8 +366,11 @@ public enum StateRegistry { map(0x23, MINECRAFT_1_19, false), map(0x25, MINECRAFT_1_19_1, false), map(0x24, MINECRAFT_1_19_3, false), - map(0x28, MINECRAFT_1_19_4, false)); - clientbound.register(Respawn.class, Respawn::new, + map(0x28, MINECRAFT_1_19_4, false), + map(0x29, MINECRAFT_1_20_2, false)); + clientbound.register( + Respawn.class, + Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), map(0x34, MINECRAFT_1_12, true), @@ -300,8 +384,11 @@ public enum StateRegistry { map(0x3B, MINECRAFT_1_19, true), map(0x3E, MINECRAFT_1_19_1, true), map(0x3D, MINECRAFT_1_19_3, true), - map(0x41, MINECRAFT_1_19_4, true)); - clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, + map(0x41, MINECRAFT_1_19_4, true), + map(0x43, MINECRAFT_1_20_2, true)); + clientbound.register( + ResourcePackRequest.class, + ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, false), map(0x32, MINECRAFT_1_9, false), map(0x33, MINECRAFT_1_12, false), @@ -315,8 +402,11 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_19, false), map(0x3D, MINECRAFT_1_19_1, false), map(0x3C, MINECRAFT_1_19_3, false), - map(0x40, MINECRAFT_1_19_4, false)); - clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, + map(0x40, MINECRAFT_1_19_4, false), + map(0x42, MINECRAFT_1_20_2, false)); + clientbound.register( + HeaderAndFooter.class, + HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_9_4, true), @@ -331,8 +421,11 @@ public enum StateRegistry { map(0x60, MINECRAFT_1_19, true), map(0x63, MINECRAFT_1_19_1, true), map(0x61, MINECRAFT_1_19_3, true), - map(0x65, MINECRAFT_1_19_4, true)); - clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, + map(0x65, MINECRAFT_1_19_4, true), + map(0x68, MINECRAFT_1_20_2, true)); + clientbound.register( + LegacyTitlePacket.class, + LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_12, true), @@ -346,31 +439,46 @@ public enum StateRegistry { map(0x58, MINECRAFT_1_18, true), map(0x5B, MINECRAFT_1_19_1, true), map(0x59, MINECRAFT_1_19_3, true), - map(0x5D, MINECRAFT_1_19_4, true)); - clientbound.register(TitleTextPacket.class, TitleTextPacket::new, + map(0x5D, MINECRAFT_1_19_4, true), + map(0x5F, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleTextPacket.class, + TitleTextPacket::new, map(0x59, MINECRAFT_1_17, true), map(0x5A, MINECRAFT_1_18, true), map(0x5D, MINECRAFT_1_19_1, true), map(0x5B, MINECRAFT_1_19_3, true), - map(0x5F, MINECRAFT_1_19_4, true)); - clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new, + map(0x5F, MINECRAFT_1_19_4, true), + map(0x61, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleActionbarPacket.class, + TitleActionbarPacket::new, map(0x41, MINECRAFT_1_17, true), map(0x40, MINECRAFT_1_19, true), map(0x43, MINECRAFT_1_19_1, true), map(0x42, MINECRAFT_1_19_3, true), - map(0x46, MINECRAFT_1_19_4, true)); - clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new, + map(0x46, MINECRAFT_1_19_4, true), + map(0x48, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleTimesPacket.class, + TitleTimesPacket::new, map(0x5A, MINECRAFT_1_17, true), map(0x5B, MINECRAFT_1_18, true), map(0x5E, MINECRAFT_1_19_1, true), map(0x5C, MINECRAFT_1_19_3, true), - map(0x60, MINECRAFT_1_19_4, true)); - clientbound.register(TitleClearPacket.class, TitleClearPacket::new, + map(0x60, MINECRAFT_1_19_4, true), + map(0x62, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleClearPacket.class, + TitleClearPacket::new, map(0x10, MINECRAFT_1_17, true), map(0x0D, MINECRAFT_1_19, true), map(0x0C, MINECRAFT_1_19_3, true), - map(0x0E, MINECRAFT_1_19_4, true)); - clientbound.register(LegacyPlayerListItem.class, LegacyPlayerListItem::new, + map(0x0E, MINECRAFT_1_19_4, true), + map(0x0F, MINECRAFT_1_20_2, true)); + clientbound.register( + LegacyPlayerListItem.class, + LegacyPlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), map(0x2E, MINECRAFT_1_12_1, false), @@ -384,68 +492,83 @@ public enum StateRegistry { map(0x37, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); clientbound.register(RemovePlayerInfo.class, RemovePlayerInfo::new, map(0x35, MINECRAFT_1_19_3, false), - map(0x39, MINECRAFT_1_19_4, false)); - clientbound.register(UpsertPlayerInfo.class, UpsertPlayerInfo::new, + map(0x39, MINECRAFT_1_19_4, false), + map(0x3B, MINECRAFT_1_20_2, false)); + clientbound.register( + UpsertPlayerInfo.class, + UpsertPlayerInfo::new, map(0x36, MINECRAFT_1_19_3, false), - map(0x3A, MINECRAFT_1_19_4, false)); - clientbound.register(SystemChat.class, SystemChat::new, + map(0x3A, MINECRAFT_1_19_4, false), + map(0x3C, MINECRAFT_1_20_2, false)); + clientbound.register( + SystemChat.class, + SystemChat::new, map(0x5F, MINECRAFT_1_19, true), map(0x62, MINECRAFT_1_19_1, true), map(0x60, MINECRAFT_1_19_3, true), - map(0x64, MINECRAFT_1_19_4, true)); - clientbound.register(PlayerChatCompletion.class, PlayerChatCompletion::new, + map(0x64, MINECRAFT_1_19_4, true), + map(0x67, MINECRAFT_1_20_2, true)); + clientbound.register( + PlayerChatCompletion.class, + PlayerChatCompletion::new, map(0x15, MINECRAFT_1_19_1, true), map(0x14, MINECRAFT_1_19_3, true), - map(0x16, MINECRAFT_1_19_4, true)); - clientbound.register(ServerData.class, ServerData::new, + map(0x16, MINECRAFT_1_19_4, true), + map(0x17, MINECRAFT_1_20_2, true)); + clientbound.register( + ServerData.class, + ServerData::new, map(0x3F, MINECRAFT_1_19, false), map(0x42, MINECRAFT_1_19_1, false), map(0x41, MINECRAFT_1_19_3, false), - map(0x45, MINECRAFT_1_19_4, false)); + map(0x45, MINECRAFT_1_19_4, false), + map(0x47, MINECRAFT_1_20_2, false)); + clientbound.register(StartUpdate.class, StartUpdate::new, map(0x65, MINECRAFT_1_20_2, false)); } }, LOGIN { { - serverbound.register(ServerLogin.class, ServerLogin::new, - map(0x00, MINECRAFT_1_7_2, false)); - serverbound.register(EncryptionResponse.class, EncryptionResponse::new, - map(0x01, MINECRAFT_1_7_2, false)); - serverbound.register(LoginPluginResponse.class, LoginPluginResponse::new, - map(0x02, MINECRAFT_1_13, false)); - clientbound.register(Disconnect.class, Disconnect::new, - map(0x00, MINECRAFT_1_7_2, false)); - clientbound.register(EncryptionRequest.class, EncryptionRequest::new, - map(0x01, MINECRAFT_1_7_2, false)); - clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new, - map(0x02, MINECRAFT_1_7_2, false)); - clientbound.register(SetCompression.class, SetCompression::new, - map(0x03, MINECRAFT_1_8, false)); - clientbound.register(LoginPluginMessage.class, LoginPluginMessage::new, - map(0x04, MINECRAFT_1_13, false)); + serverbound.register(ServerLogin.class, ServerLogin::new, map(0x00, MINECRAFT_1_7_2, false)); + serverbound.register( + EncryptionResponse.class, EncryptionResponse::new, map(0x01, MINECRAFT_1_7_2, false)); + serverbound.register( + LoginPluginResponse.class, LoginPluginResponse::new, map(0x02, MINECRAFT_1_13, false)); + serverbound.register( + LoginAcknowledged.class, LoginAcknowledged::new, map(0x03, MINECRAFT_1_20_2, false)); + + clientbound.register(Disconnect.class, Disconnect::new, map(0x00, MINECRAFT_1_7_2, false)); + clientbound.register( + EncryptionRequest.class, EncryptionRequest::new, map(0x01, MINECRAFT_1_7_2, false)); + clientbound.register( + ServerLoginSuccess.class, ServerLoginSuccess::new, map(0x02, MINECRAFT_1_7_2, false)); + clientbound.register( + SetCompression.class, SetCompression::new, map(0x03, MINECRAFT_1_8, false)); + clientbound.register( + LoginPluginMessage.class, LoginPluginMessage::new, map(0x04, MINECRAFT_1_13, false)); } }; public static final int STATUS_ID = 1; public static final int LOGIN_ID = 2; - protected final PacketRegistry clientbound = new PacketRegistry(CLIENTBOUND); - protected final PacketRegistry serverbound = new PacketRegistry(SERVERBOUND); + protected final PacketRegistry clientbound = new PacketRegistry(CLIENTBOUND, this); + protected final PacketRegistry serverbound = new PacketRegistry(SERVERBOUND, this); public StateRegistry.PacketRegistry.ProtocolRegistry getProtocolRegistry(Direction direction, ProtocolVersion version) { return (direction == SERVERBOUND ? serverbound : clientbound).getProtocolRegistry(version); } - /** - * Packet registry. - */ + /** Packet registry. */ public static class PacketRegistry { private final Direction direction; + private final StateRegistry registry; private final Map versions; private boolean fallback = true; - PacketRegistry(Direction direction) { + PacketRegistry(Direction direction, StateRegistry registry) { this.direction = direction; + this.registry = registry; Map mutableVersions = new EnumMap<>(ProtocolVersion.class); for (ProtocolVersion version : ProtocolVersion.values()) { @@ -505,19 +628,24 @@ public enum StateRegistry { } ProtocolRegistry registry = this.versions.get(protocol); if (registry == null) { - throw new IllegalArgumentException("Unknown protocol version " - + current.protocolVersion); + throw new IllegalArgumentException( + "Unknown protocol version " + current.protocolVersion); } if (registry.packetIdToSupplier.containsKey(current.id)) { - throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName() - + " with id " + current.id + " for " + registry.version - + " because another packet is already registered"); + throw new IllegalArgumentException( + "Can not register class " + + clazz.getSimpleName() + + " with id " + + current.id + + " for " + + registry.version + + " because another packet is already registered"); } if (registry.packetClassToId.containsKey(clazz)) { - throw new IllegalArgumentException(clazz.getSimpleName() - + " is already registered for version " + registry.version); + throw new IllegalArgumentException( + clazz.getSimpleName() + " is already registered for version " + registry.version); } if (!current.encodeOnly) { @@ -528,9 +656,7 @@ public enum StateRegistry { } } - /** - * Protocol registry. - */ + /** Protocol registry. */ public class ProtocolRegistry { public final ProtocolVersion version; @@ -569,18 +695,27 @@ public enum StateRegistry { final int id = this.packetClassToId.getInt(packet.getClass()); if (id == Integer.MIN_VALUE) { throw new IllegalArgumentException(String.format( - "Unable to find id for packet of type %s in %s protocol %s", - packet.getClass().getName(), PacketRegistry.this.direction, this.version + "Unable to find id for packet of type %s in %s protocol %s phase %s", + packet.getClass().getName(), PacketRegistry.this.direction, + this.version, PacketRegistry.this.registry )); } return id; } + + /** + * Checks if the registry contains a packet with the specified {@code id}. + * + * @param packet the packet to check + * @return {@code true} if the packet is registered, {@code false} otherwise + */ + public boolean containsPacket(final MinecraftPacket packet) { + return this.packetClassToId.containsKey(packet.getClass()); + } } } - /** - * Packet mapping. - */ + /** Packet mapping. */ public static final class PacketMapping { private final int id; @@ -599,9 +734,12 @@ public enum StateRegistry { @Override public String toString() { return "PacketMapping{" - + "id=" + id - + ", protocolVersion=" + protocolVersion - + ", encodeOnly=" + encodeOnly + + "id=" + + id + + ", protocolVersion=" + + protocolVersion + + ", encodeOnly=" + + encodeOnly + '}'; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java index a19baab70..6e7fb4d4e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java @@ -34,7 +34,7 @@ import java.util.List; public class MinecraftCompressDecoder extends MessageToMessageDecoder { private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB - private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 16 * 1024 * 1024; // 16MiB + private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB private static final int UNCOMPRESSED_CAP = Boolean.getBoolean("velocity.increased-compression-cap") diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java index b60fbbbc3..f8362cc09 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java @@ -35,8 +35,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter { public static final boolean DEBUG = Boolean.getBoolean("velocity.packet-decode-logging"); private static final QuietRuntimeException DECODE_FAILED = - new QuietRuntimeException("A packet did not decode successfully (invalid data). If you are a " - + "developer, launch Velocity with -Dvelocity.packet-decode-logging=true to see more."); + new QuietRuntimeException("A packet did not decode successfully (invalid data). For more " + + "information, launch Velocity with -Dvelocity.packet-decode-logging=true to see more."); private final ProtocolUtils.Direction direction; private StateRegistry state; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java index 313b7858d..7133f1d26 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java @@ -62,4 +62,8 @@ public class MinecraftEncoder extends MessageToByteEncoder { this.state = state; this.setProtocolVersion(registry.version); } + + public ProtocolUtils.Direction getDirection() { + return direction; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java new file mode 100644 index 000000000..612de451e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.netty; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.PlatformDependent; +import java.util.Queue; +import org.jetbrains.annotations.NotNull; + +/** + * Queues up any pending PLAY packets while the client is in the CONFIG state. + * + *

Much of the Velocity API (i.e. chat messages) utilize PLAY packets, however the client is + * incapable of receiving these packets during the CONFIG state. Certain events such as the + * ServerPreConnectEvent may be called during this time, and we need to ensure that any API that + * uses these packets will work as expected. + * + *

This handler will queue up any packets that are sent to the client during this time, and send + * them once the client has (re)entered the PLAY state. + */ +public class PlayPacketQueueHandler extends ChannelDuplexHandler { + + private final StateRegistry.PacketRegistry.ProtocolRegistry registry; + private final Queue queue = PlatformDependent.newMpscQueue(); + + /** + * Provides registries for client & server bound packets. + * + * @param version the protocol version + */ + public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = + StateRegistry.CONFIG.getProtocolRegistry(direction, version); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + if (!(msg instanceof MinecraftPacket)) { + ctx.write(msg, promise); + return; + } + + // If the packet exists in the CONFIG state, we want to always + // ensure that it gets sent out to the client + if (this.registry.containsPacket(((MinecraftPacket) msg))) { + ctx.write(msg, promise); + return; + } + + // Otherwise, queue the packet + this.queue.offer((MinecraftPacket) msg); + } + + @Override + public void channelInactive(@NotNull ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, false); + + super.channelInactive(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, ctx.channel().isActive()); + } + + private void releaseQueue(ChannelHandlerContext ctx, boolean active) { + if (this.queue.isEmpty()) { + return; + } + + // Send out all the queued packets + MinecraftPacket packet; + while ((packet = this.queue.poll()) != null) { + if (active) { + ctx.write(packet, ctx.voidPromise()); + } else { + ReferenceCountUtil.release(packet); + } + } + + if (active) { + ctx.flush(); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java index 5c267d195..a42d1ee90 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java @@ -23,10 +23,10 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; public class ClientSettings implements MinecraftPacket { - private @Nullable String locale; private byte viewDistance; private int chatVisibility; @@ -120,16 +120,10 @@ public class ClientSettings implements MinecraftPacket { @Override public String toString() { - return "ClientSettings{" - + "locale='" + locale + '\'' - + ", viewDistance=" + viewDistance - + ", chatVisibility=" + chatVisibility - + ", chatColors=" + chatColors - + ", skinParts=" + skinParts - + ", mainHand=" + mainHand - + ", chatFilteringEnabled=" + chatFilteringEnabled - + ", clientListingAllowed=" + clientListingAllowed - + '}'; + return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance + + ", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" + + skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + chatFilteringEnabled + + ", clientListingAllowed=" + clientListingAllowed + '}'; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index 90320f948..938d1190a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -42,6 +42,7 @@ public class JoinGame implements MinecraftPacket { private int viewDistance; // 1.14+ private boolean reducedDebugInfo; private boolean showRespawnScreen; + private boolean doLimitedCrafting; // 1.20.2+ private ImmutableSet levelNames; // 1.16+ private CompoundBinaryTag registry; // 1.16+ private DimensionInfo dimensionInfo; // 1.16+ @@ -49,6 +50,7 @@ public class JoinGame implements MinecraftPacket { private short previousGamemode; // 1.16+ private int simulationDistance; // 1.18+ private @Nullable Pair lastDeathPosition; // 1.19+ + private int portalCooldown; // 1.20+ public int getEntityId() { return entityId; @@ -142,6 +144,14 @@ public class JoinGame implements MinecraftPacket { this.isHardcore = isHardcore; } + public boolean getDoLimitedCrafting() { + return doLimitedCrafting; + } + + public void setDoLimitedCrafting(boolean doLimitedCrafting) { + this.doLimitedCrafting = doLimitedCrafting; + } + public CompoundBinaryTag getCurrentDimensionData() { return currentDimensionData; } @@ -162,37 +172,38 @@ public class JoinGame implements MinecraftPacket { this.lastDeathPosition = lastDeathPosition; } + public int getPortalCooldown() { + return portalCooldown; + } + + public void setPortalCooldown(int portalCooldown) { + this.portalCooldown = portalCooldown; + } + public CompoundBinaryTag getRegistry() { return registry; } @Override public String toString() { - return "JoinGame{" - + "entityId=" + entityId - + ", gamemode=" + gamemode - + ", dimension=" + dimension - + ", partialHashedSeed=" + partialHashedSeed - + ", difficulty=" + difficulty - + ", isHardcore=" + isHardcore - + ", maxPlayers=" + maxPlayers - + ", levelType='" + levelType + '\'' - + ", viewDistance=" + viewDistance - + ", reducedDebugInfo=" + reducedDebugInfo - + ", showRespawnScreen=" + showRespawnScreen - + ", levelNames=" + levelNames - + ", registry='" + registry + '\'' - + ", dimensionInfo='" + dimensionInfo + '\'' - + ", currentDimensionData='" + currentDimensionData + '\'' - + ", previousGamemode=" + previousGamemode - + ", simulationDistance=" + simulationDistance - + ", lastDeathPosition='" + lastDeathPosition + '\'' - + '}'; + return "JoinGame{" + "entityId=" + entityId + ", gamemode=" + gamemode + ", dimension=" + + dimension + ", partialHashedSeed=" + partialHashedSeed + ", difficulty=" + difficulty + + ", isHardcore=" + isHardcore + ", maxPlayers=" + maxPlayers + ", levelType='" + levelType + + '\'' + ", viewDistance=" + viewDistance + ", reducedDebugInfo=" + reducedDebugInfo + + ", showRespawnScreen=" + showRespawnScreen + ", doLimitedCrafting=" + doLimitedCrafting + + ", levelNames=" + levelNames + ", registry='" + registry + '\'' + ", dimensionInfo='" + + dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' + + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + + ", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown + + '}'; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // haha funny, they made 1.20.2 more complicated + this.decode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, // so separate it out. this.decode116Up(buf, version); @@ -279,11 +290,52 @@ public class JoinGame implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) { this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } + } + + private void decode1202Up(ByteBuf buf, ProtocolVersion version) { + this.entityId = buf.readInt(); + this.isHardcore = buf.readBoolean(); + + this.levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); + + this.maxPlayers = ProtocolUtils.readVarInt(buf); + + this.viewDistance = ProtocolUtils.readVarInt(buf); + this.simulationDistance = ProtocolUtils.readVarInt(buf); + + this.reducedDebugInfo = buf.readBoolean(); + this.showRespawnScreen = buf.readBoolean(); + this.doLimitedCrafting = buf.readBoolean(); + + String dimensionIdentifier = ProtocolUtils.readString(buf); + String levelName = ProtocolUtils.readString(buf); + this.partialHashedSeed = buf.readLong(); + + this.gamemode = buf.readByte(); + this.previousGamemode = buf.readByte(); + + boolean isDebug = buf.readBoolean(); + boolean isFlat = buf.readBoolean(); + this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); + + // optional death location + if (buf.readBoolean()) { + this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); + } + + this.portalCooldown = ProtocolUtils.readVarInt(buf); } @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // haha funny, they made 1.20.2 more complicated + this.encode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, // so separate it out. this.encode116Up(buf, version); @@ -376,6 +428,47 @@ public class JoinGame implements MinecraftPacket { buf.writeBoolean(false); } } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + ProtocolUtils.writeVarInt(buf, portalCooldown); + } + } + + private void encode1202Up(ByteBuf buf, ProtocolVersion version) { + buf.writeInt(entityId); + buf.writeBoolean(isHardcore); + + ProtocolUtils.writeStringArray(buf, levelNames.toArray(String[]::new)); + + ProtocolUtils.writeVarInt(buf, maxPlayers); + + ProtocolUtils.writeVarInt(buf, viewDistance); + ProtocolUtils.writeVarInt(buf, simulationDistance); + + buf.writeBoolean(reducedDebugInfo); + buf.writeBoolean(showRespawnScreen); + buf.writeBoolean(doLimitedCrafting); + + ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); + ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); + buf.writeLong(partialHashedSeed); + + buf.writeByte(gamemode); + buf.writeByte(previousGamemode); + + buf.writeBoolean(dimensionInfo.isDebugType()); + buf.writeBoolean(dimensionInfo.isFlat()); + + // optional death location + if (lastDeathPosition != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, lastDeathPosition.key()); + buf.writeLong(lastDeathPosition.value()); + } else { + buf.writeBoolean(false); + } + + ProtocolUtils.writeVarInt(buf, portalCooldown); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java new file mode 100644 index 000000000..1f7941004 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class LoginAcknowledged implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java new file mode 100644 index 000000000..2d3d9b5da --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018-2021 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class PingIdentify implements MinecraftPacket { + + private int id; + + @Override + public String toString() { + return "Ping{" + "id=" + id + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + id = buf.readInt(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + buf.writeInt(id); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java index 641407f9f..c80b97c16 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java @@ -17,17 +17,23 @@ package com.velocitypowered.proxy.protocol.packet; +import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.regex.Pattern; + public class ResourcePackRequest implements MinecraftPacket { private @MonotonicNonNull String url; @@ -35,6 +41,8 @@ public class ResourcePackRequest implements MinecraftPacket { private boolean isRequired; // 1.17+ private @Nullable Component prompt; // 1.17+ + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); // 1.20.2+ + public @Nullable String getUrl() { return url; } @@ -99,6 +107,19 @@ public class ResourcePackRequest implements MinecraftPacket { } } + public VelocityResourcePackInfo toServerPromptedPack() { + ResourcePackInfo.Builder builder = + new VelocityResourcePackInfo.BuilderImpl(Preconditions.checkNotNull(url)).setPrompt(prompt) + .setShouldForce(isRequired).setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + + if (hash != null && !hash.isEmpty()) { + if (PLAUSIBLE_SHA1_HASH.matcher(hash).matches()) { + builder.setHash(ByteBufUtil.decodeHexDump(hash)); + } + } + return (VelocityResourcePackInfo) builder.build(); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); @@ -106,11 +127,7 @@ public class ResourcePackRequest implements MinecraftPacket { @Override public String toString() { - return "ResourcePackRequest{" - + "url='" + url + '\'' - + ", hash='" + hash + '\'' - + ", isRequired=" + isRequired - + ", prompt='" + prompt + '\'' - + '}'; + return "ResourcePackRequest{" + "url='" + url + '\'' + ", hash='" + hash + '\'' + + ", isRequired=" + isRequired + ", prompt='" + prompt + '\'' + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java index 3b7c93608..4b67cf695 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java @@ -73,9 +73,6 @@ public class ResourcePackResponse implements MinecraftPacket { @Override public String toString() { - return "ResourcePackResponse{" - + "hash=" + hash + ", " - + "status=" + status - + '}'; + return "ResourcePackResponse{" + "hash=" + hash + ", " + "status=" + status + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java index 11d543c89..712024f63 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Respawn.java @@ -40,6 +40,7 @@ public class Respawn implements MinecraftPacket { private short previousGamemode; // 1.16+ private CompoundBinaryTag currentDimensionData; // 1.16.2+ private @Nullable Pair lastDeathPosition; // 1.19+ + private int portalCooldown; // 1.20+ public Respawn() { } @@ -47,7 +48,7 @@ public class Respawn implements MinecraftPacket { public Respawn(int dimension, long partialHashedSeed, short difficulty, short gamemode, String levelType, byte dataToKeep, DimensionInfo dimensionInfo, short previousGamemode, CompoundBinaryTag currentDimensionData, - @Nullable Pair lastDeathPosition) { + @Nullable Pair lastDeathPosition, int portalCooldown) { this.dimension = dimension; this.partialHashedSeed = partialHashedSeed; this.difficulty = difficulty; @@ -58,13 +59,14 @@ public class Respawn implements MinecraftPacket { this.previousGamemode = previousGamemode; this.currentDimensionData = currentDimensionData; this.lastDeathPosition = lastDeathPosition; + this.portalCooldown = portalCooldown; } public static Respawn fromJoinGame(JoinGame joinGame) { return new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), (byte) 0, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition()); + joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition(), joinGame.getPortalCooldown()); } public int getDimension() { @@ -123,12 +125,20 @@ public class Respawn implements MinecraftPacket { this.previousGamemode = previousGamemode; } + public Pair getLastDeathPosition() { + return lastDeathPosition; + } + public void setLastDeathPosition(Pair lastDeathPosition) { this.lastDeathPosition = lastDeathPosition; } - public Pair getLastDeathPosition() { - return lastDeathPosition; + public int getPortalCooldown() { + return portalCooldown; + } + + public void setPortalCooldown(int portalCooldown) { + this.portalCooldown = portalCooldown; } @Override @@ -144,6 +154,7 @@ public class Respawn implements MinecraftPacket { + ", dimensionInfo=" + dimensionInfo + ", previousGamemode=" + previousGamemode + ", dimensionData=" + currentDimensionData + + ", portalCooldown=" + portalCooldown + '}'; } @@ -188,6 +199,9 @@ public class Respawn implements MinecraftPacket { if (version.compareTo(ProtocolVersion.MINECRAFT_1_19) >= 0 && buf.readBoolean()) { this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } } @Override @@ -234,6 +248,10 @@ public class Respawn implements MinecraftPacket { buf.writeBoolean(false); } } + + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20) >= 0) { + ProtocolUtils.writeVarInt(buf, portalCooldown); + } } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java index 0cd589ee9..57c8c61c2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java @@ -74,10 +74,7 @@ public class ServerLogin implements MinecraftPacket { @Override public String toString() { - return "ServerLogin{" - + "username='" + username + '\'' - + "playerKey='" + playerKey + '\'' - + '}'; + return "ServerLogin{" + "username='" + username + '\'' + "playerKey='" + playerKey + '\'' + '}'; } @Override @@ -98,6 +95,11 @@ public class ServerLogin implements MinecraftPacket { } } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + this.holderUuid = ProtocolUtils.readUuid(buf); + return; + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (buf.readBoolean()) { holderUuid = ProtocolUtils.readUuid(buf); @@ -125,6 +127,11 @@ public class ServerLogin implements MinecraftPacket { } } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + ProtocolUtils.writeUuid(buf, this.holderUuid); + return; + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (playerKey != null && playerKey.getSignatureHolder() != null) { buf.writeBoolean(true); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java new file mode 100644 index 000000000..f1f9ad242 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/ChatAcknowledgement.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.chat; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class ChatAcknowledgement implements MinecraftPacket { + int offset; + + public ChatAcknowledgement(int offset) { + this.offset = offset; + } + + public ChatAcknowledgement() { + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + offset = ProtocolUtils.readVarInt(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, offset); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + @Override + public String toString() { + return "ChatAcknowledgement{" + + "offset=" + offset + + '}'; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java index fa3113af2..a1ed539d6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/LastSeenMessages.java @@ -49,4 +49,16 @@ public class LastSeenMessages { public boolean isEmpty() { return acknowledged.isEmpty(); } + + public int getOffset() { + return this.offset; + } + + @Override + public String toString() { + return "LastSeenMessages{" + + "offset=" + offset + + ", acknowledged=" + acknowledged + + '}'; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java index 8aa1051aa..193371aad 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/keyed/KeyedCommandHandler.java @@ -52,7 +52,7 @@ public class KeyedCommandHandler implements CommandHandler { && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { logger.fatal("A plugin tried to deny a command with signable component(s). " + "This is not supported. " - + "Disconnecting player " + player.getUsername()); + + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); player.disconnect(Component.text( "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); @@ -75,7 +75,7 @@ public class KeyedCommandHandler implements CommandHandler { && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " - + "Disconnecting player " + player.getUsername()); + + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); player.disconnect(Component.text( "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); @@ -95,7 +95,7 @@ public class KeyedCommandHandler implements CommandHandler { && playerKey.getKeyRevision().compareTo(IdentifiedKey.Revision.LINKED_V2) >= 0) { logger.fatal("A plugin tried to change a command with signed component(s). " + "This is not supported. " - + "Disconnecting player " + player.getUsername()); + + "Disconnecting player " + player.getUsername() + ". Command packet: " + packet); player.disconnect(Component.text( "A proxy plugin caused an illegal protocol state. " + "Contact your network administrator.")); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java index 782c78357..558531b72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionCommandHandler.java @@ -18,8 +18,10 @@ package com.velocitypowered.proxy.protocol.packet.chat.session; import com.velocitypowered.api.event.command.CommandExecuteEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgement; import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import java.util.concurrent.CompletableFuture; import net.kyori.adventure.text.Component; @@ -47,11 +49,15 @@ public class SessionCommandHandler implements CommandHandler= 0) { + return CompletableFuture.completedFuture(new ChatAcknowledgement(packet.lastSeenMessages.getOffset())); + } return CompletableFuture.completedFuture(null); } @@ -63,7 +69,7 @@ public class SessionCommandHandler implements CommandHandler= 0) { + return new ChatAcknowledgement(packet.lastSeenMessages.getOffset()); + } return null; }); }, packet.command, packet.timeStamp); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java index 5954530e9..5ef83fa45 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommand.java @@ -65,7 +65,8 @@ public class SessionPlayerCommand implements MinecraftPacket { } public boolean isSigned() { - return salt != 0 || !lastSeenMessages.isEmpty() || !argumentSignatures.isEmpty(); + if (salt == 0) return false; + return !lastSeenMessages.isEmpty() || !argumentSignatures.isEmpty(); } @Override @@ -73,6 +74,17 @@ public class SessionPlayerCommand implements MinecraftPacket { return handler.handle(this); } + @Override + public String toString() { + return "SessionPlayerCommand{" + + "command='" + command + '\'' + + ", timeStamp=" + timeStamp + + ", salt=" + salt + + ", argumentSignatures=" + argumentSignatures + + ", lastSeenMessages=" + lastSeenMessages + + '}'; + } + public static class ArgumentSignatures { private final List entries; @@ -104,6 +116,12 @@ public class SessionPlayerCommand implements MinecraftPacket { entry.encode(buf); } } + @Override + public String toString() { + return "ArgumentSignatures{" + + "entries=" + entries + + '}'; + } } public static class ArgumentSignature { @@ -120,5 +138,12 @@ public class SessionPlayerCommand implements MinecraftPacket { ProtocolUtils.writeString(buf, name); buf.writeBytes(signature); } + + @Override + public String toString() { + return "ArgumentSignature{" + + "name='" + name + '\'' + + '}'; + } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java new file mode 100644 index 000000000..de2a5047c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; +import net.kyori.adventure.key.Key; + +public class ActiveFeatures implements MinecraftPacket { + + private Key[] activeFeatures; + + public ActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } + + public ActiveFeatures() { + this.activeFeatures = new Key[0]; + } + + public void setActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } + + public Key[] getActiveFeatures() { + return activeFeatures; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + activeFeatures = ProtocolUtils.readKeyArray(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ProtocolUtils.writeKeyArray(buf, activeFeatures); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java new file mode 100644 index 000000000..866819b48 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class FinishedUpdate implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java new file mode 100644 index 000000000..74c275401 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; +import io.netty.buffer.ByteBuf; + +public class RegistrySync extends DeferredByteBufHolder implements MinecraftPacket { + + public RegistrySync() { + super(null); + } + + // NBT change in 1.20.2 makes it difficult to parse this packet. + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + this.replace(buf.readRetainedSlice(buf.readableBytes())); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + buf.writeBytes(content()); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java new file mode 100644 index 000000000..d1c25fa27 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class StartUpdate implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java new file mode 100644 index 000000000..79b432158 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.google.common.collect.ImmutableMap; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +import java.util.Map; + +public class TagsUpdate implements MinecraftPacket { + + private Map> tags; + + public TagsUpdate(Map> tags) { + this.tags = tags; + } + + public TagsUpdate() { + this.tags = Map.of(); + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + int size = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < size; i++) { + String key = ProtocolUtils.readString(buf); + + int innerSize = ProtocolUtils.readVarInt(buf); + ImmutableMap.Builder innerBuilder = ImmutableMap.builder(); + for (int j = 0; j < innerSize; j++) { + String innerKey = ProtocolUtils.readString(buf); + int[] innerValue = ProtocolUtils.readVarIntArray(buf); + innerBuilder.put(innerKey, innerValue); + } + + builder.put(key, innerBuilder.build()); + } + tags = builder.build(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, tags.size()); + for (Map.Entry> entry : tags.entrySet()) { + ProtocolUtils.writeString(buf, entry.getKey()); + // Oh, joy + ProtocolUtils.writeVarInt(buf, entry.getValue().size()); + for (Map.Entry innerEntry : entry.getValue().entrySet()) { + // Yea, object oriented programming be damned + ProtocolUtils.writeString(buf, innerEntry.getKey()); + ProtocolUtils.writeVarIntArray(buf, innerEntry.getValue()); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java b/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java new file mode 100644 index 000000000..df29ca56f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/provider/ComponentLoggerProviderImpl.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.provider; + +import com.velocitypowered.proxy.util.TranslatableMapper; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider; +import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer; +import org.jetbrains.annotations.NotNull; +import org.slf4j.LoggerFactory; + +/** + * Velocity ComponentLogger Provider. + */ +@SuppressWarnings("UnstableApiUsage") +public final class ComponentLoggerProviderImpl implements ComponentLoggerProvider { + private static final ANSIComponentSerializer SERIALIZER = ANSIComponentSerializer.builder() + .flattener(TranslatableMapper.FLATTENER) + .build(); + + @Override + public @NotNull ComponentLogger logger( + final @NotNull LoggerHelper helper, + final @NotNull String name + ) { + return helper.delegating(LoggerFactory.getLogger(name), SERIALIZER::serialize); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java index 96d0b94d6..727832bd4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java @@ -30,9 +30,12 @@ import com.velocitypowered.api.scheduler.ScheduledTask; import com.velocitypowered.api.scheduler.Scheduler; import com.velocitypowered.api.scheduler.TaskStatus; import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -116,17 +119,24 @@ public class VelocityScheduler implements Scheduler { task.cancel(); } timerExecutionService.shutdown(); - for (final PluginContainer container : this.pluginManager.getPlugins()) { + final List plugins = new ArrayList<>(this.pluginManager.getPlugins()); + final Iterator pluginIterator = plugins.iterator(); + while (pluginIterator.hasNext()) { + final PluginContainer container = pluginIterator.next(); if (container instanceof VelocityPluginContainer) { - (container).getExecutorService().shutdown(); + final VelocityPluginContainer pluginContainer = (VelocityPluginContainer) container; + if (pluginContainer.hasExecutorService()) { + container.getExecutorService().shutdown(); + } else { + pluginIterator.remove(); + } + } else { + pluginIterator.remove(); } } boolean allShutdown = true; - for (final PluginContainer container : this.pluginManager.getPlugins()) { - if (!(container instanceof VelocityPluginContainer)) { - continue; - } + for (final PluginContainer container : plugins) { final String id = container.getDescription().getId(); final ExecutorService service = (container).getExecutorService(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 7113e06e3..c3ff9f570 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -34,8 +34,8 @@ import java.net.SocketAddress; import java.util.concurrent.CompletableFuture; /** - * Session handler used to implement - * {@link VelocityRegisteredServer#ping(EventLoop, ProtocolVersion)}. + * Session handler used to implement {@link VelocityRegisteredServer#ping(EventLoop, + * ProtocolVersion)}. */ public class PingSessionHandler implements MinecraftSessionHandler { @@ -70,6 +70,7 @@ public class PingSessionHandler implements MinecraftSessionHandler { handshake.setProtocolVersion(version); connection.delayedWrite(handshake); + connection.setActiveSessionHandler(StateRegistry.STATUS); connection.setState(StateRegistry.STATUS); connection.delayedWrite(StatusRequest.INSTANCE); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index df5e79915..728757251 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -37,6 +37,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; @@ -94,8 +95,8 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud } /** - * Pings the specified server using the specified event {@code loop}, claiming to be - * {@code version}. + * Pings the specified server using the specified event {@code loop}, claiming to be {@code + * version}. * * @param loop the event loop to use * @param pingOptions the options to apply to this ping @@ -106,35 +107,30 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud throw new IllegalStateException("No Velocity proxy instance available"); } CompletableFuture pingFuture = new CompletableFuture<>(); - server.createBootstrap(loop, serverInfo.getAddress()) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline() - .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) - .addLast(READ_TIMEOUT, - new ReadTimeoutHandler(pingOptions.getTimeout() == 0 - ? server.getConfiguration().getReadTimeout() : pingOptions.getTimeout(), - TimeUnit.MILLISECONDS)) - .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) - .addLast(MINECRAFT_DECODER, - new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) - .addLast(MINECRAFT_ENCODER, - new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); + server.createBootstrap(loop, serverInfo.getAddress()).handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(READ_TIMEOUT, new ReadTimeoutHandler( + pingOptions.getTimeout() == 0 + ? server.getConfiguration().getReadTimeout() + : pingOptions.getTimeout(), TimeUnit.MILLISECONDS)) + .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) + .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); - ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); - } - }) - .connect(serverInfo.getAddress()) - .addListener((ChannelFutureListener) future -> { - if (future.isSuccess()) { - MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); - conn.setSessionHandler(new PingSessionHandler( - pingFuture, VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion())); - } else { - pingFuture.completeExceptionally(future.cause()); - } - }); + ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); + } + }).connect(serverInfo.getAddress()).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); + conn.setActiveSessionHandler(StateRegistry.HANDSHAKE, + new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn, + pingOptions.getProtocolVersion())); + } else { + pingFuture.completeExceptionally(future.cause()); + } + }); return pingFuture; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java index f3add2290..dcf00ecae 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/InternalTabList.java @@ -35,4 +35,6 @@ public interface InternalTabList extends TabList { default void processRemove(RemovePlayerInfo infoPacket) { } + + void clearAllSilent(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java index 63a9ae56f..e24c337ed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/KeyedVelocityTabList.java @@ -26,7 +26,6 @@ import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; import java.util.ArrayList; @@ -70,7 +69,7 @@ public class KeyedVelocityTabList implements InternalTabList { @Override public void clearHeaderAndFooter() { - connection.write(HeaderAndFooter.reset()); + this.player.clearPlayerListHeaderAndFooter(); } @Override @@ -131,10 +130,15 @@ public class KeyedVelocityTabList implements InternalTabList { for (TabListEntry value : listEntries) { items.add(LegacyPlayerListItem.Item.from(value)); } - entries.clear(); + clearAllSilent(); connection.delayedWrite(new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, items)); } + @Override + public void clearAllSilent() { + entries.clear(); + } + @Override public Collection getEntries() { return Collections.unmodifiableCollection(this.entries.values()); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java index f707c95ca..4a95b00de 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabList.java @@ -25,7 +25,6 @@ import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.console.VelocityConsole; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import com.velocitypowered.proxy.protocol.packet.chat.RemoteChatSession; @@ -74,7 +73,7 @@ public class VelocityTabList implements InternalTabList { @Override public void clearHeaderAndFooter() { - connection.write(HeaderAndFooter.reset()); + this.player.clearPlayerListHeaderAndFooter(); } @Override @@ -175,6 +174,11 @@ public class VelocityTabList implements InternalTabList { @Override public void clearAll() { this.connection.delayedWrite(new RemovePlayerInfo(new ArrayList<>(this.entries.keySet()))); + clearAllSilent(); + } + + @Override + public void clearAllSilent() { this.entries.clear(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java index d23ac4cb5..6e1777788 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/tablist/VelocityTabListLegacy.java @@ -71,6 +71,11 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList { connection.delayedWrite(new LegacyPlayerListItem(LegacyPlayerListItem.REMOVE_PLAYER, Collections.singletonList(LegacyPlayerListItem.Item.from(value)))); } + clearAllSilent(); + } + + @Override + public void clearAllSilent() { entries.clear(); nameMapping.clear(); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java new file mode 100644 index 000000000..c2ca2366f --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/TranslatableMapper.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.util; + +import java.util.Locale; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TranslatableComponent; +import net.kyori.adventure.text.flattener.ComponentFlattener; +import net.kyori.adventure.translation.GlobalTranslator; +import net.kyori.adventure.translation.TranslationRegistry; +import net.kyori.adventure.translation.Translator; +import org.jetbrains.annotations.Nullable; + + +/** + * Velocity Translation Mapper. + */ +public enum TranslatableMapper implements BiConsumer> { + INSTANCE; + + public static final ComponentFlattener FLATTENER = ComponentFlattener.basic().toBuilder() + .complexMapper(TranslatableComponent.class, TranslatableMapper.INSTANCE) + .build(); + + @Override + public void accept( + final TranslatableComponent translatableComponent, + final Consumer componentConsumer + ) { + for (final Translator source : GlobalTranslator.translator().sources()) { + if (source instanceof TranslationRegistry + && ((TranslationRegistry) source).contains(translatableComponent.key())) { + componentConsumer.accept(GlobalTranslator.render(translatableComponent, + ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); + return; + } + } + final @Nullable String fallback = translatableComponent.fallback(); + if (fallback == null) { + return; + } + for (final Translator source : GlobalTranslator.translator().sources()) { + if (source instanceof TranslationRegistry + && ((TranslationRegistry) source).contains(fallback)) { + componentConsumer.accept( + GlobalTranslator.render(Component.translatable(fallback), + ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()))); + return; + } + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java b/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java deleted file mode 100644 index 692910d57..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/collect/CappedSet.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2019-2023 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.util.collect; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ForwardingSet; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - -/** - * An unsynchronized collection that puts an upper bound on the size of the collection. - */ -public final class CappedSet extends ForwardingSet { - - private final Set delegate; - private final int upperSize; - - private CappedSet(Set delegate, int upperSize) { - this.delegate = delegate; - this.upperSize = upperSize; - } - - /** - * Creates a capped collection backed by a {@link HashSet}. - * - * @param maxSize the maximum size of the collection - * @param the type of elements in the collection - * @return the new collection - */ - public static Set create(int maxSize) { - return new CappedSet<>(new HashSet<>(), maxSize); - } - - @Override - protected Set delegate() { - return delegate; - } - - @Override - public boolean add(T element) { - if (this.delegate.size() >= upperSize) { - Preconditions.checkState(this.delegate.contains(element), - "collection is too large (%s >= %s)", - this.delegate.size(), this.upperSize); - return false; - } - return this.delegate.add(element); - } - - @Override - public boolean addAll(Collection collection) { - return this.standardAddAll(collection); - } -} diff --git a/proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider b/proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider new file mode 100644 index 000000000..b95209776 --- /dev/null +++ b/proxy/src/main/resources/META-INF/services/net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider @@ -0,0 +1 @@ +com.velocitypowered.proxy.provider.ComponentLoggerProviderImpl \ No newline at end of file diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java index 81a18e72c..05a83eea1 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java @@ -44,7 +44,7 @@ class PacketRegistryTest { private StateRegistry.PacketRegistry setupRegistry() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x01, MINECRAFT_1_8, null, false), new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, null, false), @@ -84,7 +84,7 @@ class PacketRegistryTest { @Test void failOnNoMappings() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new)); assertThrows(IllegalArgumentException.class, @@ -94,7 +94,7 @@ class PacketRegistryTest { @Test void failOnWrongOrder() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); assertThrows(IllegalArgumentException.class, () -> registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x01, MINECRAFT_1_13, null, false), @@ -115,7 +115,7 @@ class PacketRegistryTest { @Test void failOnDuplicate() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false)); assertThrows(IllegalArgumentException.class, @@ -129,7 +129,7 @@ class PacketRegistryTest { @Test void shouldNotFailWhenRegisterLatestProtocolVersion() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); assertDoesNotThrow(() -> registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_8, null, false), new StateRegistry.PacketMapping(0x01, getLast(ProtocolVersion.SUPPORTED_VERSIONS), @@ -139,7 +139,7 @@ class PacketRegistryTest { @Test void registrySuppliesCorrectPacketsByProtocol() { StateRegistry.PacketRegistry registry = new StateRegistry.PacketRegistry( - ProtocolUtils.Direction.CLIENTBOUND); + ProtocolUtils.Direction.CLIENTBOUND, StateRegistry.PLAY); registry.register(Handshake.class, Handshake::new, new StateRegistry.PacketMapping(0x00, MINECRAFT_1_12, null, false), new StateRegistry.PacketMapping(0x01, MINECRAFT_1_12_1, null, false), diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java deleted file mode 100644 index 2e118b4ac..000000000 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/collect/CappedSetTest.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2019-2021 Velocity Contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.velocitypowered.proxy.util.collect; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.common.collect.ImmutableSet; -import java.util.Collection; -import java.util.Set; -import org.junit.jupiter.api.Test; - -class CappedSetTest { - - @Test - void basicVerification() { - Collection coll = CappedSet.create(1); - assertTrue(coll.add("coffee"), "did not add single item"); - assertThrows(IllegalStateException.class, () -> coll.add("tea"), - "item was added to collection although it is too full"); - assertEquals(1, coll.size(), "collection grew in size unexpectedly"); - } - - @Test - void testAddAll() { - Set doesFill1 = ImmutableSet.of("coffee", "tea"); - Set doesFill2 = ImmutableSet.of("chocolate"); - Set overfill = ImmutableSet.of("Coke", "Pepsi"); - - Collection coll = CappedSet.create(3); - assertTrue(coll.addAll(doesFill1), "did not add items"); - assertTrue(coll.addAll(doesFill2), "did not add items"); - assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), - "items added to collection although it is too full"); - assertEquals(3, coll.size(), "collection grew in size unexpectedly"); - } - - @Test - void handlesSetBehaviorCorrectly() { - Set doesFill1 = ImmutableSet.of("coffee", "tea"); - Set doesFill2 = ImmutableSet.of("coffee", "chocolate"); - Set overfill = ImmutableSet.of("coffee", "Coke", "Pepsi"); - - Collection coll = CappedSet.create(3); - assertTrue(coll.addAll(doesFill1), "did not add items"); - assertTrue(coll.addAll(doesFill2), "did not add items"); - assertThrows(IllegalStateException.class, () -> coll.addAll(overfill), - "items added to collection although it is too full"); - - assertFalse(coll.addAll(doesFill1), "added items?!?"); - - assertEquals(3, coll.size(), "collection grew in size unexpectedly"); - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b8d700802..0c31266f9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,22 @@ +@file:Suppress("UnstableApiUsage") + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + maven("https://s01.oss.sonatype.org/content/repositories/snapshots/") // adventure + maven("https://repo.papermc.io/repository/maven-public/") + } +} + +pluginManagement { + includeBuild("build-logic") + repositories { + mavenCentral() + gradlePluginPortal() + } +} + plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0" } @@ -6,8 +25,8 @@ rootProject.name = "velocity" sequenceOf( "api", - "proxy", "native", + "proxy", ).forEach { val project = ":velocity-$it" include(project)