diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
new file mode 100644
index 000000000..82cada2b6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -0,0 +1,69 @@
+name: Bug Report
+description: Report issues with Velocity not working properly.
+labels: ["type: bug"]
+body:
+ - type: textarea
+ attributes:
+ label: Expected Behavior
+ description: What you expected to work and how.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Actual Behavior
+ description: What actually happens.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Steps to Reproduce
+ description: Information on how we can reproduce this bug on our own, this can be e.g. just an explanation, a video or your Velocity config.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Plugin List
+ description: |
+ All plugins running on your proxy and the backend server you're experiencing this issue on.
+ Use `/velocity plugins` to list plugins on Velocity and `/plugins` to list plugins on your backend server.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Velocity Version
+ description: |
+ The full, unmodified output of running `/velocity info`.
+ *"Latest"* is not a version. We require you to paste the text, not a screenshot.
+
+ Example
+
+ ```
+ [17:44:10 INFO]: Velocity 3.3.0-SNAPSHOT (git-9d25d309-b400)
+ [17:44:10 INFO]: Copyright 2018-2023 Velocity Contributors. Velocity is licensed under the terms of the GNU General Public License v3.
+ [17:44:10 INFO]: velocitypowered.com - GitHub
+ ```
+
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Additional Information
+ description: Anything else you think is helpful.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this issue, please ensure the following:
+
+ 1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity).
+ 2. You searched for and ensured there isn't already an open issue regarding this.
+
+ If you think you have a bug, but are not sure, feel free to ask in the `#velocity-help` channel on our
+ [Discord](https://discord.gg/papermc).
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..faf9d2e70
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,10 @@
+blank_issues_enabled: false
+contact_links:
+ - name: PaperMC Discord
+ url: https://discord.gg/papermc
+ about: If you are having issues with the proxy not connecting to servers or have other minor issues, come ask us on our Discord server!
+ - name: Exploit Report
+ url: https://discord.gg/papermc
+ about: |
+ Due to GitHub not currently allowing private issues, exploit reports are currently handled via our Discord.
+ To report an exploit, see the #paper-exploit-report channel.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
new file mode 100644
index 000000000..f47b06d00
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -0,0 +1,48 @@
+name: Feature Request
+description: Request for a feature to be implemented into Velocity.
+labels: ["type: feature"]
+body:
+ - type: textarea
+ attributes:
+ label: Requested Feature
+ description: |
+ Please describe as best as you can what you'd like to be added to Velocity.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Why is this needed?
+ description: |
+ Please describe why do you need this feature.
+ Do you think it could be useful? Is it due to another problem?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Alternative Solutions
+ description: |
+ Are there any alternative solutions to implementing a new feature?
+ What have you tried instead?
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Additional Information
+ description: Anything else you want to add.
+ validations:
+ required: false
+
+ - type: markdown
+ attributes:
+ value: |
+ Before submitting this request, please ensure the following:
+
+ 1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity).
+ 2. You searched for and ensured there isn't already an open issue regarding this.
+ 3. The feature you're requesting has to be implemented on Velocity and not on the backend server.
+
+ If you are unsure whether your problem can already be fixed in another way, feel free to ask in the `#velocity-help` channel on our
+ [Discord](https://discord.gg/papermc).
\ No newline at end of file
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index aa6778fca..2e524db9f 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -67,7 +67,9 @@ tasks {
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
- "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine"
+ "https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
+ "https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
+ "https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
)
o.tags(
diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java
index 6cf90a791..257fc9e64 100644
--- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java
+++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java
@@ -44,7 +44,7 @@ public interface CommandManager {
* @param otherAliases additional aliases
* @throws IllegalArgumentException if one of the given aliases is already registered, or
* the given command does not implement a registrable {@link Command} subinterface
- * @see Command for a list of registrable {@link Command} subinterfaces
+ * @see Command for a list of registrable Command subinterfaces
*/
default void register(String alias, Command command, String... otherAliases) {
register(metaBuilder(alias).aliases(otherAliases).build(), command);
@@ -65,7 +65,7 @@ public interface CommandManager {
* @param command the command to register
* @throws IllegalArgumentException if one of the given aliases is already registered, or
* the given command does not implement a registrable {@link Command} subinterface
- * @see Command for a list of registrable {@link Command} subinterfaces
+ * @see Command for a list of registrable Command subinterfaces
*/
void register(CommandMeta meta, Command command);
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java
new file mode 100644
index 000000000..96d1bb8e5
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerResourcePackRemoveEvent.java
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+package com.velocitypowered.api.event.player;
+
+import com.google.common.base.Preconditions;
+import com.velocitypowered.api.event.ResultedEvent;
+import com.velocitypowered.api.event.annotation.AwaitingEvent;
+import com.velocitypowered.api.proxy.ServerConnection;
+import java.util.UUID;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * This event is fired when the downstream server tries to remove a resource pack from player
+ * or clear all of them. The proxy will wait on this event to finish before forwarding the
+ * action to the user. If this event is denied, no resource packs will be removed from player.
+ */
+@AwaitingEvent
+public class ServerResourcePackRemoveEvent implements ResultedEvent {
+
+ private GenericResult result;
+ private final @MonotonicNonNull UUID packId;
+ private final ServerConnection serverConnection;
+
+ /**
+ * Instantiates this event.
+ */
+ public ServerResourcePackRemoveEvent(UUID packId, ServerConnection serverConnection) {
+ this.result = ResultedEvent.GenericResult.allowed();
+ this.packId = packId;
+ this.serverConnection = serverConnection;
+ }
+
+ /**
+ * Returns the id of the resource pack, if it's null all the resource packs
+ * from player will be cleared.
+ *
+ * @return the id
+ */
+ @Nullable
+ public UUID getPackId() {
+ return packId;
+ }
+
+ /**
+ * Returns the server that tries to remove a resource pack from player or clear all of them.
+ *
+ * @return the server connection
+ */
+ public ServerConnection getServerConnection() {
+ return serverConnection;
+ }
+
+ @Override
+ public GenericResult getResult() {
+ return this.result;
+ }
+
+ @Override
+ public void setResult(GenericResult result) {
+ this.result = Preconditions.checkNotNull(result, "result");
+ }
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java
new file mode 100644
index 000000000..6e042af1c
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerConfigurationEvent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.player.configuration;
+
+import com.velocitypowered.api.event.annotation.AwaitingEvent;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is executed when a player entered the configuration state and can be configured by Velocity.
+ *
Velocity will wait for this event before continuing/ending the configuration state.
+ *
+ * @param player The player who can be configured.
+ * @param server The server that is currently configuring the player.
+ * @since 3.3.0
+ * @sinceMinecraft 1.20.2
+ */
+@AwaitingEvent
+public record PlayerConfigurationEvent(@NotNull Player player, ServerConnection server) {
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java
new file mode 100644
index 000000000..05d6c2af0
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnterConfigurationEvent.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.player.configuration;
+
+import com.velocitypowered.api.event.annotation.AwaitingEvent;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is executed when a player is about to enter the configuration state.
+ * It is not called for the initial configuration of a player after login.
+ *
Velocity will wait for this event before asking the client to enter configuration state.
+ * However due to backend server being unable to keep the connection alive during state changes,
+ * Velocity will only wait for a maximum of 5 seconds.
+ *
+ * @param player The player who is about to enter configuration state.
+ * @param server The server that wants to reconfigure the player.
+ * @since 3.3.0
+ * @sinceMinecraft 1.20.2
+ */
+@AwaitingEvent
+public record PlayerEnterConfigurationEvent(@NotNull Player player, ServerConnection server) {
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java
new file mode 100644
index 000000000..c16777066
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerEnteredConfigurationEvent.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.player.configuration;
+
+import com.velocitypowered.api.network.ProtocolState;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is executed when a player has entered the configuration state.
+ *
From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
+ * the {@linkplain Player#getProtocolState()} method is guaranteed
+ * to return {@link ProtocolState#CONFIGURATION}.
+ *
+ * @param player The player who has entered the configuration state.
+ * @param server The server that will now (re-)configure the player.
+ * @since 3.3.0
+ * @sinceMinecraft 1.20.2
+ */
+public record PlayerEnteredConfigurationEvent(@NotNull Player player, ServerConnection server) {
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java
new file mode 100644
index 000000000..50df5a8ab
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishConfigurationEvent.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.player.configuration;
+
+import com.velocitypowered.api.event.annotation.AwaitingEvent;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is executed when a player is about to finish the configuration state.
+ *
Velocity will wait for this event before asking the client to finish the configuration state.
+ * However due to backend server being unable to keep the connection alive during state changes,
+ * Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
+ * state, use the {@link PlayerConfigurationEvent}.
+ *
+ * @param player The player who is about to finish the configuration phase.
+ * @param server The server that has (re-)configured the player.
+ * @since 3.3.0
+ * @sinceMinecraft 1.20.2
+ */
+@AwaitingEvent
+public record PlayerFinishConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) {
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java
new file mode 100644
index 000000000..517f119cf
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/player/configuration/PlayerFinishedConfigurationEvent.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.player.configuration;
+
+import com.velocitypowered.api.network.ProtocolState;
+import com.velocitypowered.api.proxy.Player;
+import com.velocitypowered.api.proxy.ServerConnection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is executed when a player has finished the configuration state.
+ *
From this moment on, the {@link Player#getProtocolState()} method
+ * will return {@link ProtocolState#PLAY}.
+ *
+ * @param player The player who has finished the configuration state.
+ * @param server The server that has (re-)configured the player.
+ * @since 3.3.0
+ * @sinceMinecraft 1.20.2
+ */
+public record PlayerFinishedConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) {
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java
new file mode 100644
index 000000000..754492a60
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerRegisteredEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.proxy.server;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is fired by the proxy after a backend server is registered to the server map.
+ * Currently, it may occur when a server is registered dynamically at runtime or when a server is
+ * replaced due to configuration reload.
+ *
+ * @see com.velocitypowered.api.proxy.ProxyServer#registerServer(ServerInfo)
+ *
+ * @param registeredServer A {@link RegisteredServer} that has been registered.
+ * @since 3.3.0
+ */
+@Beta
+public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) {
+ public ServerRegisteredEvent {
+ Preconditions.checkNotNull(registeredServer, "registeredServer");
+ }
+}
diff --git a/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java
new file mode 100644
index 000000000..36b4023bb
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/event/proxy/server/ServerUnregisteredEvent.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+package com.velocitypowered.api.event.proxy.server;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.Preconditions;
+import com.velocitypowered.api.proxy.server.RegisteredServer;
+import com.velocitypowered.api.proxy.server.ServerInfo;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This event is fired by the proxy after a backend server is unregistered from the server map.
+ * Currently, it may occur when a server is unregistered dynamically at runtime
+ * or when a server is replaced due to configuration reload.
+ *
+ * @see com.velocitypowered.api.proxy.ProxyServer#unregisterServer(ServerInfo)
+ *
+ * @param unregisteredServer A {@link RegisteredServer} that has been unregistered.
+ * @since 3.3.0
+ */
+@Beta
+public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) {
+ public ServerUnregisteredEvent {
+ Preconditions.checkNotNull(unregisteredServer, "unregisteredServer");
+ }
+}
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 e968b0255..8bb0c98b0 100644
--- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java
+++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java
@@ -87,7 +87,7 @@ public enum ProtocolVersion implements Ordered {
MINECRAFT_1_20_2(764, "1.20.2"),
MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"),
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"),
- MINECRAFT_1_21(767, "1.21");
+ MINECRAFT_1_21(767, "1.21", "1.21.1");
private static final int SNAPSHOT_BIT = 30;
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 dfe9a2bc7..04e65c849 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java
@@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.player.TabList;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.ModInfo;
+import com.velocitypowered.api.util.ServerLink;
import java.net.InetSocketAddress;
import java.util.Collection;
import java.util.List;
@@ -461,4 +462,16 @@ public interface Player extends
* @sinceMinecraft 1.20.5
*/
void requestCookie(Key key);
+
+ /**
+ * Send the player a list of custom links to display in their client's pause menu.
+ *
+ *
Note that later packets sent by the backend server may override links sent by the proxy.
+ *
+ * @param links an ordered list of {@link ServerLink}s to send to the player
+ * @throws IllegalArgumentException if the player is from a version lower than 1.21
+ * @since 3.3.0
+ * @sinceMinecraft 1.21
+ */
+ void setServerLinks(@NotNull List links);
}
\ No newline at end of file
diff --git a/api/src/main/java/com/velocitypowered/api/util/ServerLink.java b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java
new file mode 100644
index 000000000..9eb04a980
--- /dev/null
+++ b/api/src/main/java/com/velocitypowered/api/util/ServerLink.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2021-2024 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.
+ */
+
+package com.velocitypowered.api.util;
+
+import com.google.common.base.Preconditions;
+import java.net.URI;
+import java.util.Optional;
+import net.kyori.adventure.text.Component;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Represents a custom URL servers can show in player pause menus.
+ * Links can be of a built-in type or use a custom component text label.
+ */
+public final class ServerLink {
+
+ private @Nullable Type type;
+ private @Nullable Component label;
+ private final URI url;
+
+ private ServerLink(Component label, String url) {
+ this.label = Preconditions.checkNotNull(label, "label");
+ this.url = URI.create(url);
+ }
+
+ private ServerLink(Type type, String url) {
+ this.type = Preconditions.checkNotNull(type, "type");
+ this.url = URI.create(url);
+ }
+
+ /**
+ * Construct a server link with a custom component label.
+ *
+ * @param label a custom component label to display
+ * @param link the URL to open when clicked
+ */
+ public static ServerLink serverLink(Component label, String link) {
+ return new ServerLink(label, link);
+ }
+
+ /**
+ * Construct a server link with a built-in type.
+ *
+ * @param type the {@link Type built-in type} of link
+ * @param link the URL to open when clicked
+ */
+ public static ServerLink serverLink(Type type, String link) {
+ return new ServerLink(type, link);
+ }
+
+ /**
+ * Get the type of the server link.
+ *
+ * @return the type of the server link
+ */
+ public Optional getBuiltInType() {
+ return Optional.ofNullable(type);
+ }
+
+ /**
+ * Get the custom component label of the server link.
+ *
+ * @return the custom component label of the server link
+ */
+ public Optional getCustomLabel() {
+ return Optional.ofNullable(label);
+ }
+
+ /**
+ * Get the link {@link URI}.
+ *
+ * @return the link {@link URI}
+ */
+ public URI getUrl() {
+ return url;
+ }
+
+ /**
+ * Built-in types of server links.
+ *
+ * @apiNote {@link Type#BUG_REPORT} links are shown on the connection error screen
+ */
+ public enum Type {
+ BUG_REPORT,
+ COMMUNITY_GUIDELINES,
+ SUPPORT,
+ STATUS,
+ FEEDBACK,
+ COMMUNITY,
+ WEBSITE,
+ FORUMS,
+ NEWS,
+ ANNOUNCEMENTS
+ }
+
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c2d6ab114..35aa6910d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,7 +11,7 @@ shadow = "io.github.goooler.shadow:8.1.5"
spotless = "com.diffplug.spotless:6.25.0"
[libraries]
-adventure-bom = "net.kyori:adventure-bom:4.16.0"
+adventure-bom = "net.kyori:adventure-bom:4.17.0"
adventure-facet = "net.kyori:adventure-platform-facet:4.3.2"
asm = "org.ow2.asm:asm:9.6"
auto-service = "com.google.auto.service:auto-service:1.0.1"
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index ef8217c69..004243616 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -45,7 +45,7 @@ import com.velocitypowered.proxy.command.builtin.ShutdownCommand;
import com.velocitypowered.proxy.command.builtin.VelocityCommand;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
-import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
+import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
import com.velocitypowered.proxy.connection.util.ServerListPingHandler;
import com.velocitypowered.proxy.console.VelocityConsole;
import com.velocitypowered.proxy.crypto.EncryptionUtils;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java
index 7904a06a0..79ac41e74 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java
@@ -379,7 +379,7 @@ public final class VelocityCommand {
this.heapGenerator.invoke(hotspotMbean, file.toString(), true);
} catch (Throwable e1) {
// This should not occur
- throw new RuntimeException(e);
+ throw new RuntimeException(e1);
}
src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN));
};
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
index 11c65d527..9c2e1e828 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
@@ -60,7 +60,7 @@ public class VelocityConfiguration implements ProxyConfig {
private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class);
@Expose
- private String bind = "0.0.0.0:25577";
+ private String bind = "0.0.0.0:25565";
@Expose
private String motd = "A Velocity Server";
@Expose
@@ -503,7 +503,7 @@ public class VelocityConfiguration implements ProxyConfig {
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED);
- final String bind = config.getOrElse("bind", "0.0.0.0:25577");
+ final String bind = config.getOrElse("bind", "0.0.0.0:25565");
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
final boolean onlineMode = config.getOrElse("online-mode", true);
final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
@@ -830,7 +830,7 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose
private boolean queryEnabled = false;
@Expose
- private int queryPort = 25577;
+ private int queryPort = 25565;
@Expose
private String queryMap = "Velocity";
@Expose
@@ -849,7 +849,7 @@ public class VelocityConfiguration implements ProxyConfig {
private Query(CommentedConfig config) {
if (config != null) {
this.queryEnabled = config.getOrElse("enabled", false);
- this.queryPort = config.getIntOrElse("port", 25577);
+ this.queryPort = config.getIntOrElse("port", 25565);
this.queryMap = config.getOrElse("map", "Velocity");
this.showPlugins = config.getOrElse("show-plugins", false);
}
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 b1c91ebdb..c91739078 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java
@@ -47,7 +47,8 @@ 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.protocol.netty.PlayPacketQueueInboundHandler;
+import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf;
@@ -148,13 +149,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
return;
}
- if (msg instanceof MinecraftPacket) {
- MinecraftPacket pkt = (MinecraftPacket) msg;
+ if (msg instanceof MinecraftPacket pkt) {
if (!pkt.handle(activeSessionHandler)) {
activeSessionHandler.handleGeneric((MinecraftPacket) msg);
}
- } else if (msg instanceof HAProxyMessage) {
- HAProxyMessage proxyMessage = (HAProxyMessage) msg;
+ } else if (msg instanceof HAProxyMessage proxyMessage) {
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) {
@@ -383,9 +382,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (state == StateRegistry.CONFIG) {
// Activate the play packet queue
addPlayPacketQueueHandler();
- } else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) {
+ } else {
// Remove the queue
- this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE);
+ if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
+ this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_OUTBOUND);
+ }
+ if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) != null) {
+ this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_INBOUND);
+ }
}
}
@@ -393,10 +397,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* 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()));
+ if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
+ this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
+ new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
+ }
+ if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
+ this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
+ new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection()));
}
}
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 5b83e6549..98b1b3e6e 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
@@ -29,10 +29,13 @@ import com.velocitypowered.api.event.connection.PreTransferEvent;
import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.CookieStoreEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
+import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent;
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
+import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
+import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.command.CommandGraphInjector;
@@ -40,8 +43,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
-import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
-import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler;
+import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
+import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry;
@@ -258,14 +261,26 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(RemoveResourcePackPacket packet) {
- final ConnectedPlayer player = serverConn.getPlayer();
- final ResourcePackHandler handler = player.resourcePackHandler();
- if (packet.getId() != null) {
- handler.remove(packet.getId());
- } else {
- handler.clearAppliedResourcePacks();
- }
- playerConnection.write(packet);
+ final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent(
+ packet.getId(), this.serverConn);
+ server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> {
+ if (playerConnection.isClosed()) {
+ return;
+ }
+ if (serverResourcePackRemoveEvent.getResult().isAllowed()) {
+ final ConnectedPlayer player = serverConn.getPlayer();
+ final ResourcePackHandler handler = player.resourcePackHandler();
+ if (packet.getId() != null) {
+ handler.remove(packet.getId());
+ } else {
+ handler.clearAppliedResourcePacks();
+ }
+ playerConnection.write(packet);
+ }
+ }, playerConnection.eventLoop()).exceptionally((ex) -> {
+ logger.error("Exception while handling resource pack remove for {}", playerConnection, ex);
+ return null;
+ });
return true;
}
@@ -275,31 +290,14 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
return true;
}
- // Register and unregister packets are simply forwarded to the server as-is.
- if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
- return false;
- }
-
- if (PluginMessageUtil.isMcBrand(packet)) {
- PluginMessagePacket rewritten = PluginMessageUtil
- .rewriteMinecraftBrand(packet,
- server.getVersion(), playerConnection.getProtocolVersion());
- playerConnection.write(rewritten);
- return true;
- }
-
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
// Handled.
return true;
}
- ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
- if (id == null) {
- return false;
- }
-
byte[] copy = ByteBufUtil.getBytes(packet.content());
- PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy);
+ String channel = packet.getChannel();
+ PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
PluginMessagePacket copied = new PluginMessagePacket(
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java
index 3c2691781..b047d1868 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java
@@ -37,7 +37,6 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.util.Optional;
import java.util.StringJoiner;
-import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
@@ -143,7 +142,7 @@ public class BungeeCordMessageResponder {
out.writeUTF("PlayerList");
out.writeUTF(info.getServerInfo().getName());
- StringJoiner joiner = new StringJoiner(", ");
+ final StringJoiner joiner = new StringJoiner(", ");
for (Player online : info.getPlayersConnected()) {
joiner.add(online.getUsername());
}
@@ -187,10 +186,9 @@ public class BungeeCordMessageResponder {
Component messageComponent = serializer.deserialize(message);
if (target.equals("ALL")) {
- proxy.sendMessage(Identity.nil(), messageComponent);
+ proxy.sendMessage(messageComponent);
} else {
- proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(),
- messageComponent));
+ proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent));
}
}
@@ -262,6 +260,13 @@ public class BungeeCordMessageResponder {
});
}
+ private void processKickRaw(ByteBufDataInput in) {
+ proxy.getPlayer(in.readUTF()).ifPresent(player -> {
+ String kickReason = in.readUTF();
+ player.disconnect(GsonComponentSerializer.gson().deserialize(kickReason));
+ });
+ }
+
private void processForwardToPlayer(ByteBufDataInput in) {
Optional player = proxy.getPlayer(in.readUTF());
if (player.isPresent()) {
@@ -374,6 +379,9 @@ public class BungeeCordMessageResponder {
case "KickPlayer":
this.processKick(in);
break;
+ case "KickPlayerRaw":
+ this.processKickRaw(in);
+ break;
default:
// Do nothing, unknown command
break;
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
index 3f4325e52..74f0576c1 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java
@@ -21,6 +21,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent;
import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.CookieStoreEvent;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
+import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent;
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
@@ -29,7 +30,8 @@ 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.player.resourcepack.VelocityResourcePackInfo;
+import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
@@ -41,6 +43,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
+import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
@@ -132,7 +135,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(KeepAlivePacket packet) {
- serverConn.ensureConnected().write(packet);
+ serverConn.getPendingPings().put(packet.getRandomId(), System.nanoTime());
+ serverConn.getPlayer().getConnection().write(packet);
return true;
}
@@ -192,31 +196,53 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
}
@Override
- public boolean handle(FinishedUpdatePacket packet) {
- MinecraftConnection smc = serverConn.ensureConnected();
- ConnectedPlayer player = serverConn.getPlayer();
- ClientConfigSessionHandler configHandler =
- (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler();
+ public boolean handle(RemoveResourcePackPacket packet) {
+ final MinecraftConnection playerConnection = this.serverConn.getPlayer().getConnection();
+
+ final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent(
+ packet.getId(), this.serverConn);
+ server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> {
+ if (playerConnection.isClosed()) {
+ return;
+ }
+ if (serverResourcePackRemoveEvent.getResult().isAllowed()) {
+ final ConnectedPlayer player = serverConn.getPlayer();
+ final ResourcePackHandler handler = player.resourcePackHandler();
+ if (packet.getId() != null) {
+ handler.remove(packet.getId());
+ } else {
+ handler.clearAppliedResourcePacks();
+ }
+ playerConnection.write(packet);
+ }
+ }, playerConnection.eventLoop()).exceptionally((ex) -> {
+ logger.error("Exception while handling resource pack remove for {}", playerConnection, ex);
+ return null;
+ });
+ return true;
+ }
+
+ @Override
+ public boolean handle(FinishedUpdatePacket packet) {
+ final MinecraftConnection smc = serverConn.ensureConnected();
+ final ConnectedPlayer player = serverConn.getPlayer();
+ final 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) -> {
+ //noinspection DataFlowIssue
+ configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> {
+ smc.write(FinishedUpdatePacket.INSTANCE);
if (serverConn == player.getConnectedServer()) {
smc.setActiveSessionHandler(StateRegistry.PLAY);
- player.sendPlayerListHeaderAndFooter(
- player.getPlayerListHeader(), player.getPlayerListFooter());
+ 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));
+ smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture));
}
- if (player.resourcePackHandler().getFirstAppliedPack() == null
- && resourcePackToApply != null) {
+ if (player.resourcePackHandler().getFirstAppliedPack() == null && resourcePackToApply != null) {
player.resourcePackHandler().queueResourcePack(resourcePackToApply);
}
- smc.setAutoReading(true);
}, smc.eventLoop());
return true;
}
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 a672c9174..612e9c25a 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
@@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
+import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer;
@@ -142,10 +143,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(ServerLoginSuccessPacket packet) {
- if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
- && !informationForwarded) {
- resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE,
- serverConn.getServer()));
+ if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) {
+ resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer()));
serverConn.disconnect();
return true;
}
@@ -156,22 +155,20 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
// Move into the PLAY phase.
MinecraftConnection smc = serverConn.ensureConnected();
if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
- smc.setActiveSessionHandler(StateRegistry.PLAY,
- new TransitionSessionHandler(server, serverConn, resultFuture));
+ smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture));
} else {
smc.write(new LoginAcknowledgedPacket());
- smc.setActiveSessionHandler(StateRegistry.CONFIG,
- new ConfigSessionHandler(server, serverConn, resultFuture));
+ 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) {
+ if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
smc.setAutoReading(false);
- ((ClientPlaySessionHandler) player.getConnection()
- .getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> {
- smc.setAutoReading(true);
- }, smc.eventLoop());
+ clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
+ } else {
+ // Initial login - the player is already in configuration state.
+ server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
}
}
@@ -211,7 +208,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
The connection to the remote server was unexpectedly closed.
This is usually because the remote server does not have \
BungeeCord IP forwarding correctly enabled.
- See https://velocitypowered.com/wiki/users/forwarding/ for instructions \
+ See https://docs.papermc.io/velocity/player-information-forwarding for instructions \
on how to configure player info forwarding correctly."""));
} else {
resultFuture.completeExceptionally(
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 77290584d..ac02bcf76 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
@@ -178,14 +178,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data"));
} else {
loginState = State.ACKNOWLEDGED;
- mcConnection.setActiveSessionHandler(StateRegistry.CONFIG,
- new ClientConfigSessionHandler(server, connectedPlayer));
+ 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;
- });
+ server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> {
+ return connectToInitialServer(connectedPlayer);
+ }).exceptionally((ex) -> {
+ logger.error("Exception while connecting {} to initial server", connectedPlayer, ex);
+ return null;
+ });
}
return true;
}
@@ -224,8 +224,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
player.disconnect0(reason.get(), true);
} else {
if (!server.registerConnection(player)) {
- player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"),
- true);
+ player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), true);
return;
}
@@ -238,13 +237,13 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
loginState = State.SUCCESS_SENT;
if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
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.setActiveSessionHandler(StateRegistry.PLAY, new InitialConnectSessionHandler(player, server));
+ server.getEventManager().fire(new PostLoginEvent(player)).thenCompose((ignored) -> {
+ return connectToInitialServer(player);
+ }).exceptionally((ex) -> {
+ logger.error("Exception while connecting {} to initial server", player, ex);
+ return null;
+ });
}
}
}, mcConnection.eventLoop()).exceptionally((ex) -> {
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
index 52b67f7b5..7bb7bedfa 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java
@@ -19,6 +19,9 @@ package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
+import com.velocitypowered.api.event.player.configuration.PlayerConfigurationEvent;
+import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent;
+import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
@@ -46,8 +49,6 @@ 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;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
/**
* Handles the client config stage.
@@ -59,6 +60,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
private final ConnectedPlayer player;
private String brandChannel = null;
+ private CompletableFuture> configurationFuture;
private CompletableFuture configSwitchFuture;
/**
@@ -77,13 +79,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
configSwitchFuture = new CompletableFuture<>();
}
+ @Override
+ public void deactivated() {
+ configurationFuture = null;
+ }
+
@Override
public boolean handle(final KeepAlivePacket packet) {
- final VelocityServerConnection serverConnection = player.getConnectedServer();
- if (!this.sendKeepAliveToBackend(serverConnection, packet)) {
- final VelocityServerConnection connectionInFlight = player.getConnectionInFlight();
- this.sendKeepAliveToBackend(connectionInFlight, packet);
- }
+ player.forwardKeepAlive(packet);
return true;
}
@@ -104,8 +107,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(FinishedUpdatePacket packet) {
- player.getConnection()
- .setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player));
+ player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player));
configSwitchFuture.complete(null);
return true;
@@ -139,12 +141,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(KnownPacksPacket packet) {
- if (player.getConnectionInFlight() != null) {
- player.getConnectionInFlight().ensureConnected().write(packet);
- return true;
- }
+ callConfigurationEvent().thenRun(() -> {
+ player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet);
+ }).exceptionally(ex -> {
+ logger.error("Error forwarding known packs response to backend:", ex);
+ return null;
+ });
- return false;
+ return true;
}
@Override
@@ -207,26 +211,25 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override
public void exception(Throwable throwable) {
- player.disconnect(
- Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
+ player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
}
- private boolean sendKeepAliveToBackend(
- final @Nullable VelocityServerConnection serverConnection,
- final @NotNull KeepAlivePacket packet
- ) {
- if (serverConnection != null) {
- final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
- if (sentTime != null) {
- final MinecraftConnection smc = serverConnection.getConnection();
- if (smc != null) {
- player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
- smc.write(packet);
- return true;
- }
- }
+ /**
+ * Calls the {@link PlayerConfigurationEvent}.
+ * For 1.20.5+ backends this is done when the client responds to
+ * the known packs request. The response is delayed until the event
+ * has been called.
+ * For 1.20.2-1.20.4 servers this is done when the client acknowledges
+ * the end of the configuration.
+ * This is handled differently because for 1.20.5+ servers can't keep
+ * their connection alive between states and older servers don't have
+ * the known packs transaction.
+ */
+ private CompletableFuture> callConfigurationEvent() {
+ if (configurationFuture != null) {
+ return configurationFuture;
}
- return false;
+ return configurationFuture = server.getEventManager().fire(new PlayerConfigurationEvent(player, player.getConnectionInFlightOrConnectedServer()));
}
/**
@@ -246,14 +249,18 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
smc.write(brandPacket);
}
- player.getConnection().eventLoop().execute(() -> {
+ callConfigurationEvent().thenCompose(v -> {
+ return server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn))
+ .completeOnTimeout(null, 5, TimeUnit.SECONDS);
+ }).thenRunAsync(() -> {
player.getConnection().write(FinishedUpdatePacket.INSTANCE);
player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY);
+ server.getEventManager().fireAndForget(new PlayerFinishedConfigurationEvent(player, serverConn));
+ }, player.getConnection().eventLoop()).exceptionally(ex -> {
+ logger.error("Error finishing configuration state:", ex);
+ return null;
});
- smc.write(FinishedUpdatePacket.INSTANCE);
- smc.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY);
-
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 2a595b167..3b3f7d422 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
@@ -27,6 +27,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent;
+import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
@@ -53,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
+import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler;
import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
@@ -84,7 +86,6 @@ import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.TimeUnit;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
@@ -176,17 +177,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(KeepAlivePacket packet) {
- final VelocityServerConnection serverConnection = player.getConnectedServer();
- if (serverConnection != null) {
- final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
- if (sentTime != null) {
- final MinecraftConnection smc = serverConnection.getConnection();
- if (smc != null) {
- player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
- smc.write(packet);
- }
- }
- }
+ player.forwardKeepAlive(packet);
return true;
}
@@ -349,43 +340,25 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
}
if (!player.getPhase().handle(player, packet, serverConn)) {
- ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
- if (id == null) {
- // We don't have any plugins listening on this channel, process the packet now.
- if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
- .consideredComplete()) {
- // The client is trying to send messages too early. This is primarily caused by mods,
- // but further aggravated by Velocity. To work around these issues, we will queue any
- // non-FML handshake messages to be sent once the FML handshake has completed or the
- // JoinGame packet has been received by the proxy, whichever comes first.
- //
- // We also need to make sure to retain these packets, so they can be flushed
- // appropriately.
- loginPluginMessages.add(packet.retain());
- } else {
- // The connection is ready, send the packet now.
- backendConn.write(packet.retain());
- }
- } else {
- byte[] copy = ByteBufUtil.getBytes(packet.content());
- PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
- server.getEventManager().fire(event).thenAcceptAsync(pme -> {
- if (pme.getResult().isAllowed()) {
- PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
- Unpooled.wrappedBuffer(copy));
- if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
- .consideredComplete()) {
- // We're still processing the connection (see above), enqueue the packet for now.
- loginPluginMessages.add(message.retain());
- } else {
- backendConn.write(message);
- }
+ byte[] copy = ByteBufUtil.getBytes(packet.content());
+ String channel = packet.getChannel();
+ PluginMessageEvent event = new PluginMessageEvent(player, serverConn, channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
+ server.getEventManager().fire(event).thenAcceptAsync(pme -> {
+ if (pme.getResult().isAllowed()) {
+ PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
+ Unpooled.wrappedBuffer(copy));
+ if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
+ .consideredComplete()) {
+ // We're still processing the connection (see above), enqueue the packet for now.
+ loginPluginMessages.add(message.retain());
+ } else {
+ backendConn.write(message);
}
- }, backendConn.eventLoop()).exceptionally((ex) -> {
- logger.error("Exception while handling plugin message packet for {}", player, ex);
- return null;
- });
- }
+ }
+ }, backendConn.eventLoop()).exceptionally((ex) -> {
+ logger.error("Exception while handling plugin message packet for {}", player, ex);
+ return null;
+ });
}
}
}
@@ -406,6 +379,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Complete client switch
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
VelocityServerConnection serverConnection = player.getConnectedServer();
+ server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
if (serverConnection != null) {
MinecraftConnection smc = serverConnection.ensureConnected();
CompletableFuture.runAsync(() -> {
@@ -421,6 +395,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true;
}
+ @Override
+ public boolean handle(ChatAcknowledgementPacket packet) {
+ if (player.getCurrentServer().isEmpty()) {
+ return true;
+ }
+ player.getChatQueue().handleAcknowledgement(packet.offset());
+ return true;
+ }
+
@Override
public boolean handle(ServerboundCookieResponsePacket packet) {
server.getEventManager()
@@ -512,7 +495,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
* @return a future that completes when the switch is complete
*/
public CompletableFuture doSwitch() {
- VelocityServerConnection existingConnection = player.getConnectedServer();
+ final VelocityServerConnection existingConnection = player.getConnectedServer();
if (existingConnection != null) {
// Shut down the existing server connection.
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 a471e2b53..2b22fc7bc 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
@@ -37,6 +37,7 @@ import com.velocitypowered.api.event.player.KickedFromServerEvent.ServerKickResu
import com.velocitypowered.api.event.player.PlayerModInfoEvent;
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
+import com.velocitypowered.api.event.player.configuration.PlayerEnterConfigurationEvent;
import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.permission.PermissionFunction;
@@ -54,18 +55,21 @@ import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.api.util.ModInfo;
+import com.velocitypowered.api.util.ServerLink;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
-import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
-import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler;
+import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
+import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
+import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
import com.velocitypowered.proxy.connection.util.VelocityInboundConnection;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
+import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
@@ -80,7 +84,9 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
+import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket;
+import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput;
@@ -106,6 +112,7 @@ import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.identity.Identity;
@@ -629,6 +636,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return connectionInFlight;
}
+ public VelocityServerConnection getConnectionInFlightOrConnectedServer() {
+ return connectionInFlight != null ? connectionInFlight : connectedServer;
+ }
+
public void resetInFlightConnection() {
connectionInFlight = null;
}
@@ -806,7 +817,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}, connection.eventLoop());
} else if (event.getResult() instanceof final Notify res) {
if (event.kickedDuringServerConnect() && previousConnection != null) {
- sendMessage(Identity.nil(), res.getMessageComponent());
+ sendMessage(res.getMessageComponent());
} else {
disconnect(res.getMessageComponent());
}
@@ -1057,6 +1068,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}, connection.eventLoop());
}
+ @Override
+ public void setServerLinks(final @NotNull List links) {
+ Preconditions.checkNotNull(links, "links");
+ Preconditions.checkArgument(
+ this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21),
+ "Player version must be at least 1.21 to be able to set server links");
+
+ if (connection.getState() != StateRegistry.PLAY
+ && connection.getState() != StateRegistry.CONFIG) {
+ throw new IllegalStateException("Can only send server links in CONFIGURATION or PLAY protocol");
+ }
+
+ connection.write(new ClientboundServerLinksPacket(List.copyOf(links).stream()
+ .map(l -> new ClientboundServerLinksPacket.ServerLink(l, getProtocolVersion())).toList()));
+ }
+
@Override
public void addCustomChatCompletions(@NotNull Collection completions) {
Preconditions.checkNotNull(completions, "completions");
@@ -1088,11 +1115,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
"input cannot be greater than " + LegacyChatPacket.MAX_SERVERBOUND_MESSAGE_LENGTH
+ " characters in length");
if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
- this.chatQueue.hijack(getChatBuilderFactory().builder().asPlayer(this).message(input),
- (instant, item) -> {
- item.setTimestamp(instant);
- return item.toServer();
- });
+ ChatBuilderV2 message = getChatBuilderFactory().builder().asPlayer(this).message(input);
+ this.chatQueue.queuePacket(chatState -> {
+ message.setTimestamp(chatState.lastTimestamp);
+ message.setLastSeenMessages(chatState.createLastSeen());
+ return message.toServer();
+ });
} else {
ensureBackendConnection().write(getChatBuilderFactory().builder()
.asPlayer(this).message(input).toServer());
@@ -1217,20 +1245,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}
}
+ /**
+ * Forwards the keep alive packet to the backend server it belongs to.
+ * This is either the connection in flight or the connected server.
+ */
+ public boolean forwardKeepAlive(final KeepAlivePacket packet) {
+ if (!this.sendKeepAliveToBackend(connectedServer, packet)) {
+ return this.sendKeepAliveToBackend(connectionInFlight, packet);
+ }
+ return false;
+ }
+
+ private boolean sendKeepAliveToBackend(final @Nullable VelocityServerConnection serverConnection, final @NotNull KeepAlivePacket packet) {
+ if (serverConnection != null) {
+ final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
+ if (sentTime != null) {
+ final MinecraftConnection smc = serverConnection.getConnection();
+ if (smc != null) {
+ setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
+ smc.write(packet);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Switches the connection to the client into config state.
*/
public void switchToConfigState() {
- CompletableFuture.runAsync(() -> {
- connection.write(StartUpdatePacket.INSTANCE);
- 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;
- });
+ server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
+ .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
+ if (bundleHandler.isInBundleSession()) {
+ bundleHandler.toggleBundleSession();
+ connection.write(BundleDelimiterPacket.INSTANCE);
+ }
+ connection.write(StartUpdatePacket.INSTANCE);
+ 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;
+ });
}
/**
@@ -1363,24 +1421,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
}
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:
+ case ALREADY_CONNECTED -> sendMessage(ConnectionMessages.ALREADY_CONNECTED);
+ case CONNECTION_IN_PROGRESS -> sendMessage(ConnectionMessages.IN_PROGRESS);
+ case CONNECTION_CANCELLED -> {
// Ignored; the plugin probably already handled this.
- break;
- case SERVER_DISCONNECTED:
- Component reason = status.getReasonComponent()
- .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
+ }
+ case SERVER_DISCONNECTED -> {
+ final Component reason = status.getReasonComponent()
+ .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
handleConnectionException(toConnect,
- DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe());
- break;
- default:
+ DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe());
+ }
+ default -> {
// The only remaining value is successful (no need to do anything!)
- break;
+ }
}
}, connection.eventLoop()).thenApply(Result::isSuccessful);
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java
similarity index 95%
rename from proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java
rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java
index e92d10587..d5d52ef03 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/BundleDelimiterHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/bundle/BundleDelimiterHandler.java
@@ -15,10 +15,11 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.connection.client;
+package com.velocitypowered.proxy.connection.player.bundle;
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.StateRegistry;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import java.util.concurrent.CompletableFuture;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java
similarity index 98%
rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java
rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java
index 67f90fd15..4292710e0 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/VelocityResourcePackInfo.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/VelocityResourcePackInfo.java
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.connection.player;
+package com.velocitypowered.proxy.connection.player.resourcepack;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java
similarity index 99%
rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java
rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java
index 385bdce4c..a1af567dd 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/Legacy117ResourcePackHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/Legacy117ResourcePackHandler.java
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.connection.player.resourcepack;
+package com.velocitypowered.proxy.connection.player.resourcepack.handler;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.proxy.VelocityServer;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java
similarity index 98%
rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java
rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java
index b1b325484..53fa2421c 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/LegacyResourcePackHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/LegacyResourcePackHandler.java
@@ -15,13 +15,14 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.connection.player.resourcepack;
+package com.velocitypowered.proxy.connection.player.resourcepack.handler;
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
+import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java
similarity index 98%
rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java
rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java
index 5ba74a273..077ce701d 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ModernResourcePackHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ModernResourcePackHandler.java
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.connection.player.resourcepack;
+package com.velocitypowered.proxy.connection.player.resourcepack.handler;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimaps;
@@ -23,6 +23,7 @@ import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
+import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java
similarity index 97%
rename from proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java
rename to proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java
index 4e6e72505..e5df9f659 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/ResourcePackHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/player/resourcepack/handler/ResourcePackHandler.java
@@ -15,14 +15,15 @@
* along with this program. If not, see .
*/
-package com.velocitypowered.proxy.connection.player.resourcepack;
+package com.velocitypowered.proxy.connection.player.resourcepack.handler;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
-import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
+import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
+import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
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 27ec4ba8b..ca4e6b1b2 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java
@@ -35,7 +35,8 @@ 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";
+ public static final String PLAY_PACKET_QUEUE_OUTBOUND = "play-packet-queue-outbound";
+ public static final String PLAY_PACKET_QUEUE_INBOUND = "play-packet-queue-inbound";
private Connections() {
throw new AssertionError();
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 2c6f752f7..0af477c42 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java
@@ -43,13 +43,13 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.HashSet;
+import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
-import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -86,35 +86,48 @@ public class VelocityPluginManager implements PluginManager {
checkNotNull(directory, "directory");
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
- List found = new ArrayList<>();
+ Map foundCandidates = new LinkedHashMap<>();
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
try (DirectoryStream stream = Files.newDirectoryStream(directory,
p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
for (Path path : stream) {
try {
- found.add(loader.loadCandidate(path));
+ PluginDescription candidate = loader.loadCandidate(path);
+
+ // If we found a duplicate candidate (with the same ID), don't load it.
+ PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent(
+ candidate.getId(), candidate);
+
+ if (maybeExistingCandidate != null) {
+ logger.error("Refusing to load plugin at path {} since we already "
+ + "loaded a plugin with the same ID {} from {}",
+ candidate.getSource().map(Objects::toString).orElse(""),
+ candidate.getId(),
+ maybeExistingCandidate.getSource().map(Objects::toString).orElse(""));
+ }
} catch (Throwable e) {
logger.error("Unable to load plugin {}", path, e);
}
}
}
- if (found.isEmpty()) {
+ if (foundCandidates.isEmpty()) {
// No plugins found
return;
}
- List sortedPlugins = PluginDependencyUtils.sortCandidates(found);
+ List sortedPlugins = PluginDependencyUtils.sortCandidates(
+ new ArrayList<>(foundCandidates.values()));
- Set loadedPluginsById = new HashSet<>();
+ Map loadedCandidates = new HashMap<>();
Map pluginContainers = new LinkedHashMap<>();
// Now load the plugins
pluginLoad:
for (PluginDescription candidate : sortedPlugins) {
// Verify dependencies
for (PluginDependency dependency : candidate.getDependencies()) {
- if (!dependency.isOptional() && !loadedPluginsById.contains(dependency.getId())) {
+ if (!dependency.isOptional() && !loadedCandidates.containsKey(dependency.getId())) {
logger.error("Can't load plugin {} due to missing dependency {}", candidate.getId(),
dependency.getId());
continue pluginLoad;
@@ -125,7 +138,7 @@ public class VelocityPluginManager implements PluginManager {
PluginDescription realPlugin = loader.createPluginFromCandidate(candidate);
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
pluginContainers.put(container, loader.createModule(container));
- loadedPluginsById.add(realPlugin.getId());
+ loadedCandidates.put(realPlugin.getId(), realPlugin);
} catch (Throwable e) {
logger.error("Can't create module for plugin {}", candidate.getId(), e);
}
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 f8362cc09..d75cb46ca 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
@@ -56,8 +56,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- if (msg instanceof ByteBuf) {
- ByteBuf buf = (ByteBuf) msg;
+ if (msg instanceof ByteBuf buf) {
tryDecode(ctx, buf);
} else {
ctx.fireChannelRead(msg);
@@ -147,4 +146,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
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/PlayPacketQueueInboundHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java
new file mode 100644
index 000000000..1affc13bc
--- /dev/null
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueInboundHandler.java
@@ -0,0 +1,94 @@
+/*
+ * 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.util.ReferenceCountUtil;
+import java.util.ArrayDeque;
+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 PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
+
+ private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
+ private final Queue