diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index acec1f85..c0ad9c30 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -10,13 +10,14 @@ jobs:
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- - name: Validate Gradle Wrapper
- uses: gradle/actions/wrapper-validation@v3
+ with:
+ persist-credentials: false
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'temurin'
- cache: 'gradle'
- name: Build with Gradle
run: ./gradlew build
diff --git a/api/build.gradle.kts b/api/build.gradle.kts
index 2e524db9..a4cbd741 100644
--- a/api/build.gradle.kts
+++ b/api/build.gradle.kts
@@ -61,6 +61,7 @@ tasks {
o.encoding = "UTF-8"
o.source = "17"
+ o.use()
o.links(
"https://www.slf4j.org/apidocs/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java
index b3263639..3cc03925 100644
--- a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java
+++ b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java
@@ -27,7 +27,7 @@ public interface CommandSource extends Audience, PermissionSubject {
* for more information on the format.
**/
default void sendRichMessage(final @NotNull String message) {
- this.sendMessage(MiniMessage.miniMessage().deserialize(message));
+ this.sendMessage(MiniMessage.miniMessage().deserialize(message, this));
}
/**
@@ -43,7 +43,7 @@ public interface CommandSource extends Audience, PermissionSubject {
final @NotNull String message,
final @NotNull TagResolver @NotNull... resolvers
) {
- this.sendMessage(MiniMessage.miniMessage().deserialize(message, resolvers));
+ this.sendMessage(MiniMessage.miniMessage().deserialize(message, this, resolvers));
}
/**
diff --git a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java
index 0d52ed2c..98fa2e86 100644
--- a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java
+++ b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java
@@ -12,6 +12,14 @@ package com.velocitypowered.api.event;
*/
public enum PostOrder {
- FIRST, EARLY, NORMAL, LATE, LAST, CUSTOM
+ FIRST, EARLY, NORMAL, LATE, LAST,
+
+ /**
+ * Previously used to specify that {@link Subscribe#priority()} should be used.
+ *
+ * @deprecated No longer required, you only need to specify {@link Subscribe#priority()}.
+ */
+ @Deprecated
+ CUSTOM
}
diff --git a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java
index abb96c94..e0cdeb86 100644
--- a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java
+++ b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java
@@ -32,12 +32,9 @@ public @interface Subscribe {
* The priority of this event handler. Priorities are used to determine the order in which event
* handlers are called. The higher the priority, the earlier the event handler will be called.
*
- *
Note that due to compatibility constraints, you must specify {@link PostOrder#CUSTOM}
- * in order to use this field.
- *
* @return the priority
*/
- short priority() default Short.MIN_VALUE;
+ short priority() default 0;
/**
* Whether the handler must be called asynchronously. By default, all event handlers are called
diff --git a/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java b/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java
index 3f9b0298..d00cffac 100644
--- a/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/command/PlayerAvailableCommandsEvent.java
@@ -9,7 +9,6 @@ package com.velocitypowered.api.event.command;
import static com.google.common.base.Preconditions.checkNotNull;
-import com.google.common.annotations.Beta;
import com.mojang.brigadier.tree.RootCommandNode;
import com.velocitypowered.api.event.annotation.AwaitingEvent;
import com.velocitypowered.api.proxy.Player;
@@ -21,7 +20,6 @@ import com.velocitypowered.api.proxy.Player;
* client.
*/
@AwaitingEvent
-@Beta
public class PlayerAvailableCommandsEvent {
private final Player player;
diff --git a/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java
index 1be39544..95e70a49 100644
--- a/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java
+++ b/api/src/main/java/com/velocitypowered/api/event/player/ServerPostConnectEvent.java
@@ -7,7 +7,6 @@
package com.velocitypowered.api.event.player;
-import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -18,7 +17,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* available in {@link Player#getCurrentServer()}. Velocity will not wait on this event to finish
* firing.
*/
-@Beta
public class ServerPostConnectEvent {
private final Player player;
private final RegisteredServer previousServer;
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
index 754492a6..1ffb58bc 100644
--- 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
@@ -7,7 +7,6 @@
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;
@@ -23,7 +22,6 @@ import org.jetbrains.annotations.NotNull;
* @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
index 36b4023b..1d9548b7 100644
--- 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
@@ -7,7 +7,6 @@
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;
@@ -23,7 +22,6 @@ import org.jetbrains.annotations.NotNull;
* @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 d1eaa389..346a43c7 100644
--- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java
+++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java
@@ -89,7 +89,8 @@ public enum ProtocolVersion implements Ordered {
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"),
MINECRAFT_1_21(767, "1.21", "1.21.1"),
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
- MINECRAFT_1_21_4(769, "1.21.4");
+ MINECRAFT_1_21_4(769, "1.21.4"),
+ MINECRAFT_1_21_5(770, /*1073742067,*/ "1.21.5");
private static final int SNAPSHOT_BIT = 30;
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java
index 1b16e9e1..884cd411 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java
@@ -7,6 +7,7 @@
package com.velocitypowered.api.proxy;
+import com.velocitypowered.api.network.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import java.net.InetSocketAddress;
@@ -60,4 +61,11 @@ public interface InboundConnection {
* @return the protocol state of the connection
*/
ProtocolState getProtocolState();
+
+ /**
+ * Returns the initial intent for the connection.
+ *
+ * @return the intent of the connection
+ */
+ HandshakeIntent getHandshakeIntent();
}
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 f043b0c6..bbf999b2 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,13 @@ public interface ProxyServer extends Audience {
*/
void shutdown();
+ /**
+ * Returns whether the proxy is currently shutting down.
+ *
+ * @return {@code true} if the proxy is shutting down, {@code false} otherwise
+ */
+ boolean isShuttingDown();
+
/**
* Closes all listening endpoints for this server.
* This includes the main minecraft listener and query channel.
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 feffd76e..a4035530 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
@@ -40,6 +40,7 @@ public interface TabList {
* Adds a {@link TabListEntry} to the {@link Player}'s tab list.
*
* @param entry to add to the tab list
+ * @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists
*/
void addEntry(TabListEntry entry);
@@ -47,6 +48,7 @@ public interface TabList {
* Adds a {@link Iterable} of {@link TabListEntry}'s to the {@link Player}'s tab list.
*
* @param entries to add to the tab list
+ * @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists
*/
default void addEntries(Iterable entries) {
for (TabListEntry entry : entries) {
@@ -58,6 +60,7 @@ public interface TabList {
* Adds an array of {@link TabListEntry}'s to the {@link Player}'s tab list.
*
* @param entries to add to the tab list
+ * @throws IllegalArgumentException on versions below 1.19.3, if an entry with the same UUID already exists
*/
default void addEntries(TabListEntry... entries) {
for (TabListEntry entry : entries) {
@@ -187,6 +190,26 @@ public interface TabList {
* @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
*/
@Deprecated
+ default TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
+ int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder) {
+ return buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, true);
+ }
+
+ /**
+ * Represents an entry in a {@link Player}'s tab list.
+ *
+ * @param profile the profile
+ * @param displayName the display name
+ * @param latency the latency
+ * @param gameMode the game mode
+ * @param chatSession the chat session
+ * @param listed the visible status of entry
+ * @param listOrder the order/priority of entry in the tab list
+ * @param showHat the visibility of this entry's hat layer
+ * @return the entry
+ * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
+ */
+ @Deprecated
TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
- int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder);
+ int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat);
}
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java
index b5140776..aea45287 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/player/TabListEntry.java
@@ -80,7 +80,7 @@ public interface TabListEntry extends KeyIdentifiable {
* 150-300 will display 4 bars
* 300-600 will display 3 bars
* 600-1000 will display 2 bars
- * A latency move than 1 second will display 1 bar
+ * A latency greater than 1 second will display 1 bar
*
*
* @return latency set for {@code this} entry
@@ -160,6 +160,27 @@ public interface TabListEntry extends KeyIdentifiable {
return this;
}
+ /**
+ * Returns whether this entry's hat layer is shown in the tab list.
+ *
+ * @return whether to show this entry's hat layer
+ * @sinceMinecraft 1.21.4
+ */
+ default boolean isShowHat() {
+ return true;
+ }
+
+ /**
+ * Sets whether to show this entry's hat layer in the tab list.
+ *
+ * @param showHat whether to show this entry's hat layer
+ * @return {@code this}, for chaining
+ * @sinceMinecraft 1.21.4
+ */
+ default TabListEntry setShowHat(boolean showHat) {
+ return this;
+ }
+
/**
* Returns a {@link Builder} to create a {@link TabListEntry}.
*
@@ -183,6 +204,7 @@ public interface TabListEntry extends KeyIdentifiable {
private int gameMode = 0;
private boolean listed = true;
private int listOrder = 0;
+ private boolean showHat;
private @Nullable ChatSession chatSession;
@@ -268,7 +290,7 @@ public interface TabListEntry extends KeyIdentifiable {
* Sets whether this entry should be visible.
*
* @param listed to set
- * @return ${code this}, for chaining
+ * @return {@code this}, for chaining
* @see TabListEntry#isListed()
*/
public Builder listed(boolean listed) {
@@ -280,7 +302,7 @@ public interface TabListEntry extends KeyIdentifiable {
* Sets the order/priority of this entry in the tab list.
*
* @param order to set
- * @return ${code this}, for chaining
+ * @return {@code this}, for chaining
* @sinceMinecraft 1.21.2
* @see TabListEntry#getListOrder()
*/
@@ -289,6 +311,18 @@ public interface TabListEntry extends KeyIdentifiable {
return this;
}
+ /**
+ * Sets whether this entry's hat layer should be shown in the tab list.
+ *
+ * @param showHat to set
+ * @return {@code this}, for chaining
+ * @see TabListEntry#isShowHat()
+ */
+ public Builder showHat(boolean showHat) {
+ this.showHat = showHat;
+ return this;
+ }
+
/**
* Constructs the {@link TabListEntry} specified by {@code this} {@link Builder}.
*
@@ -301,7 +335,7 @@ public interface TabListEntry extends KeyIdentifiable {
if (profile == null) {
throw new IllegalStateException("The GameProfile must be set when building a TabListEntry");
}
- return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder);
+ return tabList.buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, showHat);
}
}
}
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java b/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java
index 51be358e..a3cd0ac1 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/server/PingOptions.java
@@ -15,6 +15,7 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit;
import net.kyori.adventure.builder.AbstractBuilder;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
/**
* Contains the parameters used to ping a {@link RegisteredServer}.
@@ -30,10 +31,12 @@ public final class PingOptions {
public static final PingOptions DEFAULT = PingOptions.builder().build();
private final ProtocolVersion protocolVersion;
private final long timeout;
+ private final String virtualHost;
private PingOptions(final Builder builder) {
this.protocolVersion = builder.protocolVersion;
this.timeout = builder.timeout;
+ this.virtualHost = builder.virtualHost;
}
/**
@@ -54,6 +57,16 @@ public final class PingOptions {
return this.timeout;
}
+ /**
+ * The virtual host to pass to the server for the ping.
+ *
+ * @return the virtual hostname to pass to the server for the ping
+ * @since 3.4.0
+ */
+ public @Nullable String getVirtualHost() {
+ return this.virtualHost;
+ }
+
/**
* Create a new builder to assign values to a new PingOptions.
*
@@ -68,10 +81,9 @@ public final class PingOptions {
if (o == null) {
return false;
}
- if (!(o instanceof PingOptions)) {
+ if (!(o instanceof final PingOptions other)) {
return false;
}
- final PingOptions other = (PingOptions) o;
return Objects.equals(this.protocolVersion, other.protocolVersion)
&& Objects.equals(this.timeout, other.timeout);
}
@@ -97,6 +109,7 @@ public final class PingOptions {
public static final class Builder implements AbstractBuilder {
private ProtocolVersion protocolVersion = ProtocolVersion.UNKNOWN;
private long timeout = 0;
+ private String virtualHost = null;
private Builder() {
}
@@ -146,6 +159,18 @@ public final class PingOptions {
return this;
}
+ /**
+ * Sets the virtual host to pass to the server for the ping.
+ *
+ * @param virtualHost the virtual hostname to pass to the server for the ping
+ * @return this builder
+ * @since 3.4.0
+ */
+ public Builder virtualHost(final @Nullable String virtualHost) {
+ this.virtualHost = virtualHost;
+ return this;
+ }
+
/**
* Create a new {@link PingOptions} with the values of this Builder.
*
diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java
index bf0892ec..4bc199fb 100644
--- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java
+++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java
@@ -14,6 +14,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
@@ -160,31 +161,79 @@ public final class ServerPing {
}
+ /**
+ * Uses the modified {@code version} info in the response.
+ *
+ * @param version version info to set
+ * @return this builder, for chaining
+ */
public Builder version(Version version) {
this.version = Preconditions.checkNotNull(version, "version");
return this;
}
+ /**
+ * Uses the modified {@code onlinePlayers} number in the response.
+ *
+ * @param onlinePlayers number for online players to set
+ * @return this builder, for chaining
+ */
public Builder onlinePlayers(int onlinePlayers) {
this.onlinePlayers = onlinePlayers;
return this;
}
+ /**
+ * Uses the modified {@code maximumPlayers} number in the response.
+ * This will not modify the actual maximum players that can join the server.
+ *
+ * @param maximumPlayers number for maximum players to set
+ * @return this builder, for chaining
+ */
public Builder maximumPlayers(int maximumPlayers) {
this.maximumPlayers = maximumPlayers;
return this;
}
+ /**
+ * Uses the modified {@code players} array in the response.
+ *
+ * @param players array of SamplePlayers to add
+ * @return this builder, for chaining
+ */
public Builder samplePlayers(SamplePlayer... players) {
this.samplePlayers.addAll(Arrays.asList(players));
return this;
}
+ /**
+ * Uses the modified {@code players} collection in the response.
+ *
+ * @param players collection of SamplePlayers to add
+ * @return this builder, for chaining
+ */
+ public Builder samplePlayers(Collection players) {
+ this.samplePlayers.addAll(players);
+ return this;
+ }
+
+ /**
+ * Uses the modified {@code modType} in the response.
+ *
+ * @param modType the mod type to set
+ * @return this builder, for chaining
+ */
public Builder modType(String modType) {
this.modType = Preconditions.checkNotNull(modType, "modType");
return this;
}
+ /**
+ * Uses the modified {@code mods} array in the response.
+ *
+ * @param mods array of mods to use
+ * @return this builder, for chaining
+ */
public Builder mods(ModInfo.Mod... mods) {
this.mods.addAll(Arrays.asList(mods));
return this;
@@ -194,7 +243,7 @@ public final class ServerPing {
* Uses the modified {@code mods} list in the response.
*
* @param mods the mods list to use
- * @return this build, for chaining
+ * @return this builder, for chaining
*/
public Builder mods(ModInfo mods) {
Preconditions.checkNotNull(mods, "mods");
@@ -204,36 +253,74 @@ public final class ServerPing {
return this;
}
+ /**
+ * Clears the current list of mods to use in the response.
+ *
+ * @return this builder, for chaining
+ */
public Builder clearMods() {
this.mods.clear();
return this;
}
+ /**
+ * Clears the current list of PlayerSamples to use in the response.
+ *
+ * @return this builder, for chaining
+ */
public Builder clearSamplePlayers() {
this.samplePlayers.clear();
return this;
}
+ /**
+ * Defines the server as mod incompatible in the response.
+ *
+ * @return this builder, for chaining
+ */
public Builder notModCompatible() {
this.nullOutModinfo = true;
return this;
}
+ /**
+ * Enables nulling Players in the response.
+ * This will display the player count as {@code ???}.
+ *
+ * @return this builder, for chaining
+ */
public Builder nullPlayers() {
this.nullOutPlayers = true;
return this;
}
+ /**
+ * Uses the {@code description} Component in the response.
+ *
+ * @param description Component to use as the description.
+ * @return this builder, for chaining
+ */
public Builder description(net.kyori.adventure.text.Component description) {
this.description = Preconditions.checkNotNull(description, "description");
return this;
}
+ /**
+ * Uses the {@code favicon} in the response.
+ *
+ * @param favicon Favicon instance to use.
+ * @return this builder, for chaining
+ */
public Builder favicon(Favicon favicon) {
this.favicon = Preconditions.checkNotNull(favicon, "favicon");
return this;
}
+ /**
+ * Clears the current favicon used in the response.
+ *
+ * @return this builder, for chaining
+ */
public Builder clearFavicon() {
this.favicon = null;
return this;
diff --git a/api/src/main/java/com/velocitypowered/api/util/ModInfo.java b/api/src/main/java/com/velocitypowered/api/util/ModInfo.java
index 8a51f322..cfc52289 100644
--- a/api/src/main/java/com/velocitypowered/api/util/ModInfo.java
+++ b/api/src/main/java/com/velocitypowered/api/util/ModInfo.java
@@ -76,9 +76,17 @@ public final class ModInfo {
private final String id;
private final String version;
+ /**
+ * Creates a new mod info.
+ *
+ * @param id the mod identifier
+ * @param version the mod version
+ */
public Mod(String id, String version) {
this.id = Preconditions.checkNotNull(id, "id");
this.version = Preconditions.checkNotNull(version, "version");
+ Preconditions.checkArgument(id.length() < 128, "mod id is too long");
+ Preconditions.checkArgument(version.length() < 128, "mod version is too long");
}
public String getId() {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8dd1e3fb..09e5170f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@ configurate3 = "3.7.3"
configurate4 = "4.1.2"
flare = "2.0.1"
log4j = "2.24.1"
-netty = "4.1.114.Final"
+netty = "4.1.119.Final"
[plugins]
indra-publishing = "net.kyori.indra.publishing:2.0.6"
@@ -11,14 +11,14 @@ shadow = "io.github.goooler.shadow:8.1.5"
spotless = "com.diffplug.spotless:6.25.0"
[libraries]
-adventure-bom = "net.kyori:adventure-bom:4.17.0"
-adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.17.0"
+adventure-bom = "net.kyori:adventure-bom:4.19.0"
+adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.19.0"
adventure-facet = "net.kyori:adventure-platform-facet:4.3.4"
-asm = "org.ow2.asm:asm:9.6"
+asm = "org.ow2.asm:asm:9.7.1"
auto-service = "com.google.auto.service:auto-service:1.0.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
-bstats = "org.bstats:bstats-base:3.0.2"
+bstats = "org.bstats:bstats-base:3.0.3"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
checker-qual = "org.checkerframework:checker-qual:3.42.0"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
@@ -37,7 +37,7 @@ jline = "org.jline:jline-terminal-jansi:3.27.1"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:5.10.2"
jspecify = "org.jspecify:jspecify:0.3.0"
-kyori-ansi = "net.kyori:ansi:1.1.0"
+kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:25.1-jre"
gson = "com.google.code.gson:gson:2.10.1"
guice = "com.google.inject:guice:6.0.0"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e6441136..a4b76b95 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index b82aa23a..e2847c82 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 1aa94a42..5bdb31f8 100755
--- a/gradlew
+++ b/gradlew
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -200,7 +203,7 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
@@ -246,4 +249,4 @@ eval "set -- $(
tr '\n' ' '
)" '"$@"'
-exec "$JAVACMD" "$@"
+exec "$JAVACMD" "$@"
\ No newline at end of file
diff --git a/gradlew.bat b/gradlew.bat
index 7101f8e4..6042cb3e 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -34,7 +36,7 @@ set APP_HOME=%DIRNAME%
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -89,4 +91,4 @@ exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
-:omega
+:omega
\ No newline at end of file
diff --git a/native/src/main/c/jni_cipher_macos.c b/native/src/main/c/jni_cipher_macos.c
index aa7d1aba..d658eb5d 100644
--- a/native/src/main/c/jni_cipher_macos.c
+++ b/native/src/main/c/jni_cipher_macos.c
@@ -26,6 +26,15 @@ Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_init(JNIEnv *env,
return 0;
}
+ // But, you're saying, *why* are we using the key as the IV? After all, reusing the key as
+ // the IV defeats the entire point - we might as well just initialize it to all zeroes.
+ //
+ // You can blame Mojang. For the record, we also don't consider the Minecraft protocol
+ // encryption scheme to be secure, and it has reached the point where any serious cryptographic
+ // protocol needs a refresh. There are multiple obvious weaknesses, and this is far from the
+ // most serious.
+ //
+ // If you are using Minecraft in a security-sensitive application, *I don't know what to say.*
CCCryptorRef cryptor = NULL;
CCCryptorStatus result = CCCryptorCreateWithMode(encrypt ? kCCEncrypt : kCCDecrypt,
kCCModeCFB8,
diff --git a/native/src/main/c/jni_cipher_openssl.c b/native/src/main/c/jni_cipher_openssl.c
index 83515be5..06f09e11 100644
--- a/native/src/main/c/jni_cipher_openssl.c
+++ b/native/src/main/c/jni_cipher_openssl.c
@@ -32,6 +32,15 @@ Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_init(JNIEnv *env,
return 0;
}
+ // But, you're saying, *why* are we using the key as the IV? After all, reusing the key as
+ // the IV defeats the entire point - we might as well just initialize it to all zeroes.
+ //
+ // You can blame Mojang. For the record, we also don't consider the Minecraft protocol
+ // encryption scheme to be secure, and it has reached the point where any serious cryptographic
+ // protocol needs a refresh. There are multiple obvious weaknesses, and this is far from the
+ // most serious.
+ //
+ // If you are using Minecraft in a security-sensitive application, *I don't know what to say.*
int result = EVP_CipherInit(ctx, EVP_aes_128_cfb8(), (byte*) keyBytes, (byte*) keyBytes,
encrypt);
if (result != 1) {
diff --git a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java
index 7182b352..b6c3aab4 100644
--- a/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java
+++ b/native/src/main/java/com/velocitypowered/natives/compression/JavaVelocityCompressor.java
@@ -57,7 +57,8 @@ public class JavaVelocityCompressor implements VelocityCompressor {
inflater.setInput(source.nioBuffer());
try {
- while (!inflater.finished() && inflater.getBytesWritten() < uncompressedSize) {
+ final int readable = source.readableBytes();
+ while (!inflater.finished() && inflater.getBytesRead() < readable) {
if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE);
}
diff --git a/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java b/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java
index 58a7da58..00f5a1bf 100644
--- a/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java
+++ b/native/src/main/java/com/velocitypowered/natives/encryption/JavaVelocityCipher.java
@@ -48,6 +48,15 @@ public class JavaVelocityCipher implements VelocityCipher {
private JavaVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException {
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
+ // But, you're saying, *why* are we using the key as the IV? After all, reusing the key as
+ // the IV defeats the entire point - we might as well just initialize it to all zeroes.
+ //
+ // You can blame Mojang. For the record, we also don't consider the Minecraft protocol
+ // encryption scheme to be secure, and it has reached the point where any serious cryptographic
+ // protocol needs a refresh. There are multiple obvious weaknesses, and this is far from the
+ // most serious.
+ //
+ // If you are using Minecraft in a security-sensitive application, *I don't know what to say.*
this.cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key,
new IvParameterSpec(key.getEncoded()));
}
diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts
index 1121aea4..b3c5892f 100644
--- a/proxy/build.gradle.kts
+++ b/proxy/build.gradle.kts
@@ -51,7 +51,11 @@ tasks {
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
exclude("it/unimi/dsi/fastutil/ints/IntAVL*")
- exclude("it/unimi/dsi/fastutil/ints/IntArray*")
+ exclude("it/unimi/dsi/fastutil/ints/IntArrayF*")
+ exclude("it/unimi/dsi/fastutil/ints/IntArrayI*")
+ exclude("it/unimi/dsi/fastutil/ints/IntArrayL*")
+ exclude("it/unimi/dsi/fastutil/ints/IntArrayP*")
+ exclude("it/unimi/dsi/fastutil/ints/IntArraySet*")
exclude("it/unimi/dsi/fastutil/ints/*IntBi*")
exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
exclude("it/unimi/dsi/fastutil/ints/IntLinked*")
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
index 89f63b56..7feeb25f 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/Metrics.java
@@ -65,7 +65,8 @@ public class Metrics {
logger::info,
config.isLogErrorsEnabled(),
config.isLogSentDataEnabled(),
- config.isLogResponseStatusTextEnabled()
+ config.isLogResponseStatusTextEnabled(),
+ false
);
if (!config.didExistBefore()) {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
index 8b5d4d3d..abf2cf00 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java
@@ -236,6 +236,15 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
registerTranslations();
+ // Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds
+ // dangerous, right? We're well within the realm of factoring such a key...
+ //
+ // You can blame Mojang. For the record, we also don't consider the Minecraft protocol
+ // encryption scheme to be secure, and it has reached the point where any serious cryptographic
+ // protocol needs a refresh. There are multiple obvious weaknesses, and this is far from the
+ // most serious.
+ //
+ // If you are using Minecraft in a security-sensitive application, *I don't know what to say.*
serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
cm.logChannelInformation();
@@ -794,6 +803,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar;
}
+
+ @Override
+ public boolean isShuttingDown() {
+ return shutdownInProgress.get();
+ }
@Override
public InetSocketAddress getBoundAddress() {
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java
index 402a0ce8..b60ead91 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java
@@ -17,6 +17,7 @@
package com.velocitypowered.proxy.command.builtin;
+import com.google.gson.JsonSyntaxException;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
@@ -25,8 +26,9 @@ import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.proxy.VelocityServer;
+import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
-import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
+import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
/**
* Shuts down the proxy.
@@ -53,11 +55,22 @@ public final class ShutdownCommand {
StringArgumentType.greedyString())
.executes(context -> {
String reason = context.getArgument("reason", String.class);
- server.shutdown(true, MiniMessage.miniMessage().deserialize(
- MiniMessage.miniMessage().serialize(
- LegacyComponentSerializer.legacy('&').deserialize(reason)
- )
- ));
+ Component reasonComponent = null;
+
+ if (reason.startsWith("{") || reason.startsWith("[") || reason.startsWith("\"")) {
+ try {
+ reasonComponent = GsonComponentSerializer.gson()
+ .deserializeOrNull(reason);
+ } catch (JsonSyntaxException expected) {
+
+ }
+ }
+
+ if (reasonComponent == null) {
+ reasonComponent = MiniMessage.miniMessage().deserialize(reason);
+ }
+
+ server.shutdown(true, reasonComponent);
return Command.SINGLE_SUCCESS;
})
).build());
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 85bf2061..1e393476 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
@@ -82,7 +82,7 @@ public final class VelocityCommand {
.executes(new Heap())
.build();
final LiteralCommandNode info = BrigadierCommand.literalArgumentBuilder("info")
- .requires(source -> source.getPermissionValue("velocity.command.info") != Tristate.FALSE)
+ .requires(source -> source.getPermissionValue("velocity.command.info") == Tristate.TRUE)
.executes(new Info(server))
.build();
final LiteralCommandNode plugins = BrigadierCommand
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 9c2e1e82..bbd49f55 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java
@@ -407,6 +407,10 @@ public class VelocityConfiguration implements ProxyConfig {
return forceKeyAuthentication;
}
+ public boolean isEnableReusePort() {
+ return advanced.isEnableReusePort();
+ }
+
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
@@ -716,6 +720,8 @@ public class VelocityConfiguration implements ProxyConfig {
private boolean logPlayerConnections = true;
@Expose
private boolean acceptTransfers = false;
+ @Expose
+ private boolean enableReusePort = false;
private Advanced() {
}
@@ -741,6 +747,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.logCommandExecutions = config.getOrElse("log-command-executions", false);
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
this.acceptTransfers = config.getOrElse("accepts-transfers", false);
+ this.enableReusePort = config.getOrElse("enable-reuse-port", false);
}
}
@@ -804,6 +811,10 @@ public class VelocityConfiguration implements ProxyConfig {
return this.acceptTransfers;
}
+ public boolean isEnableReusePort() {
+ return enableReusePort;
+ }
+
@Override
public String toString() {
return "Advanced{"
@@ -821,6 +832,7 @@ public class VelocityConfiguration implements ProxyConfig {
+ ", logCommandExecutions=" + logCommandExecutions
+ ", logPlayerConnections=" + logPlayerConnections
+ ", acceptTransfers=" + acceptTransfers
+ + ", enableReusePort=" + enableReusePort
+ '}';
}
}
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 b047d186..6161c4c2 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
@@ -301,6 +301,21 @@ public class BungeeCordMessageResponder {
}
}
+ private void processGetPlayerServer(ByteBufDataInput in) {
+ proxy.getPlayer(in.readUTF()).ifPresent(player -> {
+ player.getCurrentServer().ifPresent(server -> {
+ ByteBuf buf = Unpooled.buffer();
+ ByteBufDataOutput out = new ByteBufDataOutput(buf);
+
+ out.writeUTF("GetPlayerServer");
+ out.writeUTF(player.getUsername());
+ out.writeUTF(server.getServerInfo().getName());
+
+ sendResponseOnConnection(buf);
+ });
+ });
+ }
+
static String getBungeeCordChannel(ProtocolVersion version) {
return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId()
: LEGACY_CHANNEL.getId();
@@ -331,6 +346,9 @@ public class BungeeCordMessageResponder {
ByteBufDataInput in = new ByteBufDataInput(message.content());
String subChannel = in.readUTF();
switch (subChannel) {
+ case "GetPlayerServer":
+ this.processGetPlayerServer(in);
+ break;
case "ForwardToPlayer":
this.processForwardToPlayer(in);
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 74f0576c..1860093d 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
@@ -17,6 +17,7 @@
package com.velocitypowered.proxy.connection.backend;
+import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.connection.PreTransferEvent;
import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.CookieStoreEvent;
@@ -24,6 +25,7 @@ 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.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
@@ -54,6 +56,8 @@ import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
+import io.netty.buffer.ByteBufUtil;
+import io.netty.buffer.Unpooled;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;
@@ -261,7 +265,29 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion()));
} else {
- serverConn.getPlayer().getConnection().write(packet.retain());
+ byte[] bytes = ByteBufUtil.getBytes(packet.content());
+ ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
+
+ if (id == null) {
+ serverConn.getPlayer().getConnection().write(packet.retain());
+ return true;
+ }
+
+ // Handling this stuff async means that we should probably pause
+ // the connection while we toss this off into another pool
+ this.serverConn.getConnection().setAutoReading(false);
+ this.server.getEventManager()
+ .fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
+ .thenAcceptAsync(pme -> {
+ if (pme.getResult().isAllowed() && !serverConn.getPlayer().getConnection().isClosed()) {
+ serverConn.getPlayer().getConnection().write(new PluginMessagePacket(
+ pme.getIdentifier().getId(), Unpooled.wrappedBuffer(bytes)));
+ }
+ this.serverConn.getConnection().setAutoReading(true);
+ }, serverConn.ensureConnected().eventLoop()).exceptionally((ex) -> {
+ logger.error("Exception while handling plugin message {}", packet, ex);
+ return null;
+ });
}
return true;
}
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 6165a846..0aea8d77 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
@@ -97,7 +97,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(),
mcConnection, inbound.getVirtualHost().orElse(null), inbound.getRawVirtualHost().orElse(null), onlineMode,
- inbound.getIdentifiedKey());
+ inbound.getHandshakeIntent(), inbound.getIdentifiedKey());
this.connectedPlayer = player;
if (!server.canRegisterConnection(player)) {
player.disconnect0(
@@ -106,7 +106,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
return CompletableFuture.completedFuture(null);
}
- logger.info("{} has connected", player);
+ if (server.getConfiguration().isLogPlayerConnections()) {
+ logger.info("{} has connected", player);
+ }
return server.getEventManager()
.fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))
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 7bb7bedf..a7143462 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
@@ -17,14 +17,17 @@
package com.velocitypowered.proxy.connection.client;
+import com.velocitypowered.api.event.connection.PluginMessageEvent;
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.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
+import com.velocitypowered.proxy.connection.backend.BungeeCordMessageResponder;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
@@ -41,6 +44,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
@@ -123,8 +127,32 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
brandChannel = packet.getChannel();
// Client sends `minecraft:brand` packet immediately after Login,
// but at this time the backend server may not be ready
+ } else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
+ return true;
} else if (serverConn != null) {
- serverConn.ensureConnected().write(packet.retain());
+ byte[] bytes = ByteBufUtil.getBytes(packet.content());
+ ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
+
+ if (id == null) {
+ serverConn.ensureConnected().write(packet.retain());
+ return true;
+ }
+
+ // Handling this stuff async means that we should probably pause
+ // the connection while we toss this off into another pool
+ serverConn.getPlayer().getConnection().setAutoReading(false);
+ this.server.getEventManager()
+ .fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
+ .thenAcceptAsync(pme -> {
+ if (pme.getResult().isAllowed() && serverConn.getConnection() != null) {
+ serverConn.ensureConnected().write(new PluginMessagePacket(
+ pme.getIdentifier().getId(), Unpooled.wrappedBuffer(bytes)));
+ }
+ serverConn.getPlayer().getConnection().setAutoReading(true);
+ }, player.getConnection().eventLoop()).exceptionally((ex) -> {
+ logger.error("Exception while handling plugin message packet for {}", player, ex);
+ return null;
+ });
}
return true;
}
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 9983b81e..6900825e 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
@@ -170,6 +170,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void deactivated() {
+ player.discardChatQueue();
for (PluginMessagePacket message : loginPluginMessages) {
ReferenceCountUtil.release(message);
}
@@ -426,6 +427,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true;
}
+ @Override
+ public boolean handle(JoinGamePacket packet) {
+ // Forward the packet as normal, but discard any chat state we have queued - the client will do this too
+ player.discardChatQueue();
+ return false;
+ }
+
@Override
public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer();
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 46c3d63d..7324544d 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
@@ -38,6 +38,7 @@ 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.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.permission.PermissionFunction;
@@ -156,6 +157,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private final MinecraftConnection connection;
private final @Nullable InetSocketAddress virtualHost;
private final @Nullable String rawVirtualHost;
+ private final HandshakeIntent handshakeIntent;
private GameProfile profile;
private PermissionFunction permissionFunction;
private int tryIndex = 0;
@@ -188,17 +190,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private @Nullable Locale effectiveLocale;
private final @Nullable IdentifiedKey playerKey;
private @Nullable ClientSettingsPacket clientSettingsPacket;
- private final ChatQueue chatQueue;
+ private volatile ChatQueue chatQueue;
private final ChatBuilderFactory chatBuilderFactory;
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
- @Nullable IdentifiedKey playerKey) {
+ HandshakeIntent handshakeIntent, @Nullable IdentifiedKey playerKey) {
this.server = server;
this.profile = profile;
this.connection = connection;
this.virtualHost = virtualHost;
this.rawVirtualHost = rawVirtualHost;
+ this.handshakeIntent = handshakeIntent;
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
this.connectionPhase = connection.getType().getInitialClientPhase();
this.onlineMode = onlineMode;
@@ -233,6 +236,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return chatQueue;
}
+ /**
+ * Discards any messages still being processed by the {@link ChatQueue}, and creates a fresh state for future packets.
+ * This should be used on server switches, or whenever the client resets its own 'last seen' state.
+ */
+ public void discardChatQueue() {
+ // No need for atomic swap, should only be called from event loop
+ final ChatQueue oldChatQueue = chatQueue;
+ chatQueue = new ChatQueue(this);
+ oldChatQueue.close();
+ }
+
public BundleDelimiterHandler getBundleHandler() {
return this.bundleHandler;
}
@@ -1087,8 +1101,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
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()));
+ connection.write(new ClientboundServerLinksPacket(links.stream()
+ .map(l -> new ClientboundServerLinksPacket.ServerLink(
+ l.getBuiltInType().map(Enum::ordinal).orElse(-1),
+ l.getCustomLabel()
+ .map(c -> new ComponentHolder(getProtocolVersion(), translateMessage(c)))
+ .orElse(null),
+ l.getUrl().toString()))
+ .toList()));
}
@Override
@@ -1284,6 +1304,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
public void switchToConfigState() {
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
+ // if the connection was closed earlier, there is a risk that the player is no longer connected
+ if (!connection.getChannel().isActive()) {
+ return;
+ }
+
if (bundleHandler.isInBundleSession()) {
bundleHandler.toggleBundleSession();
connection.write(BundleDelimiterPacket.INSTANCE);
@@ -1335,6 +1360,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return connection.getState().toProtocolState();
}
+ @Override
+ public HandshakeIntent getHandshakeIntent() {
+ return handshakeIntent;
+ }
+
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect;
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 f31e0812..80260d2b 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
@@ -277,5 +277,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
public ProtocolState getProtocolState() {
return connection.getState().toProtocolState();
}
+
+ @Override
+ public HandshakeIntent getHandshakeIntent() {
+ return HandshakeIntent.STATUS;
+ }
}
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java
index 7b918374..35368844 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java
@@ -17,6 +17,7 @@
package com.velocitypowered.proxy.connection.client;
+import com.velocitypowered.api.network.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.InboundConnection;
@@ -98,6 +99,11 @@ public final class InitialInboundConnection implements VelocityInboundConnection
return connection.getState().toProtocolState();
}
+ @Override
+ public HandshakeIntent getHandshakeIntent() {
+ return handshake.getIntent();
+ }
+
/**
* Disconnects the connection from the server.
*
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java
index 18596e4e..9922c509 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java
@@ -17,6 +17,7 @@
package com.velocitypowered.proxy.connection.client;
+import com.velocitypowered.api.network.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.LoginPhaseConnection;
@@ -177,4 +178,9 @@ public class LoginInboundConnection implements LoginPhaseConnection, KeyIdentifi
public ProtocolState getProtocolState() {
return delegate.getProtocolState();
}
+
+ @Override
+ public HandshakeIntent getHandshakeIntent() {
+ return delegate.getHandshakeIntent();
+ }
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java
index 1d066117..854a52d7 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeUtil.java
@@ -64,6 +64,7 @@ class LegacyForgeUtil {
if (discriminator == MOD_LIST_DISCRIMINATOR) {
ImmutableList.Builder mods = ImmutableList.builder();
int modCount = ProtocolUtils.readVarInt(contents);
+ Preconditions.checkArgument(modCount < 1024, "Oversized mods list");
for (int index = 0; index < modCount; index++) {
String id = ProtocolUtils.readString(contents);
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java
index 951d6098..97947eb6 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ServerListPingHandler.java
@@ -63,7 +63,7 @@ public class ServerListPingHandler {
}
private CompletableFuture attemptPingPassthrough(VelocityInboundConnection connection,
- PingPassthroughMode mode, List servers, ProtocolVersion responseProtocolVersion) {
+ PingPassthroughMode mode, List servers, ProtocolVersion responseProtocolVersion, String virtualHostStr) {
ServerPing fallback = constructLocalPing(connection.getProtocolVersion());
List> pings = new ArrayList<>();
for (String s : servers) {
@@ -73,7 +73,7 @@ public class ServerListPingHandler {
}
VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get();
pings.add(vrs.ping(connection.getConnection().eventLoop(), PingOptions.builder()
- .version(responseProtocolVersion).build()));
+ .version(responseProtocolVersion).virtualHost(virtualHostStr).build()));
}
if (pings.isEmpty()) {
return CompletableFuture.completedFuture(fallback);
@@ -155,7 +155,7 @@ public class ServerListPingHandler {
.orElse("");
List serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
- return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion);
+ return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion, virtualHostStr);
}
}
}
diff --git a/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java
index 283c8d49..04107b39 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/crypto/EncryptionUtils.java
@@ -23,8 +23,6 @@ import com.velocitypowered.proxy.util.except.QuietDecoderException;
import it.unimi.dsi.fastutil.Pair;
import java.io.IOException;
import java.math.BigInteger;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.Key;
@@ -111,42 +109,6 @@ public enum EncryptionUtils {
}
}
- /**
- * Generates a signature for input data.
- *
- * @param algorithm the signature algorithm
- * @param base the private key to sign with
- * @param toSign the byte array(s) of data to sign
- * @return the generated signature
- */
- public static byte[] generateSignature(String algorithm, PrivateKey base, byte[]... toSign) {
- Preconditions.checkArgument(toSign.length > 0);
- try {
- Signature construct = Signature.getInstance(algorithm);
- construct.initSign(base);
- for (byte[] bytes : toSign) {
- construct.update(bytes);
- }
- return construct.sign();
- } catch (GeneralSecurityException e) {
- throw new IllegalArgumentException("Invalid signature parameters");
- }
- }
-
- /**
- * Encodes a long array as Big-endian byte array.
- *
- * @param bits the long (array) of numbers to encode
- * @return the encoded bytes
- */
- public static byte[] longToBigEndianByteArray(long... bits) {
- ByteBuffer ret = ByteBuffer.allocate(8 * bits.length).order(ByteOrder.BIG_ENDIAN);
- for (long put : bits) {
- ret.putLong(put);
- }
- return ret.array();
- }
-
public static String encodeUrlEncoded(byte[] data) {
return MIME_SPECIAL_ENCODER.encodeToString(data);
}
@@ -155,22 +117,6 @@ public enum EncryptionUtils {
return Base64.getMimeDecoder().decode(toParse);
}
- /**
- * Parse a cer-encoded RSA key into its key bytes.
- *
- * @param toParse the cer-encoded key String
- * @param descriptors the type of key
- * @return the parsed key bytes
- */
- public static byte[] parsePemEncoded(String toParse, Pair descriptors) {
- int startIdx = toParse.indexOf(descriptors.first());
- Preconditions.checkArgument(startIdx >= 0);
- int firstLen = descriptors.first().length();
- int endIdx = toParse.indexOf(descriptors.second(), firstLen + startIdx) + 1;
- Preconditions.checkArgument(endIdx > 0);
- return decodeUrlEncoded(toParse.substring(startIdx + firstLen, endIdx));
- }
-
/**
* Encodes an RSA key as String cer format.
*
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 4c48068c..9d54b2d0 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java
@@ -350,8 +350,9 @@ public class VelocityEventManager implements EventManager {
asyncType = AsyncType.ALWAYS;
}
+ // The default value of 0 will fall back to PostOrder, the default PostOrder (NORMAL) is also 0
final short order;
- if (subscribe.order() == PostOrder.CUSTOM) {
+ if (subscribe.priority() != 0) {
order = subscribe.priority();
} else {
order = (short) POST_ORDER_MAP.get(subscribe.order());
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 54d51280..2a4c27c8 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java
@@ -18,6 +18,8 @@
package com.velocitypowered.proxy.network;
import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
import com.velocitypowered.api.event.proxy.ListenerBoundEvent;
import com.velocitypowered.api.event.proxy.ListenerCloseEvent;
import com.velocitypowered.api.network.ListenerType;
@@ -28,14 +30,17 @@ import com.velocitypowered.proxy.protocol.netty.GameSpyQueryHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
+import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark;
+import io.netty.channel.unix.UnixChannelOption;
import io.netty.util.concurrent.GlobalEventExecutor;
+import io.netty.util.concurrent.MultithreadEventExecutorGroup;
import java.net.InetSocketAddress;
import java.net.http.HttpClient;
-import java.util.HashMap;
+import java.util.Collection;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -49,7 +54,7 @@ public final class ConnectionManager {
private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
1 << 21);
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
- private final Map endpoints = new HashMap<>();
+ private final Multimap endpoints = HashMultimap.create();
private final TransportType transportType;
private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup;
@@ -93,7 +98,6 @@ public final class ConnectionManager {
public void bind(final InetSocketAddress address) {
final ServerBootstrap bootstrap = new ServerBootstrap()
.channelFactory(this.transportType.serverSocketChannelFactory)
- .group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(this.serverChannelInitializer.get())
.childOption(ChannelOption.TCP_NODELAY, true)
@@ -104,26 +108,50 @@ public final class ConnectionManager {
bootstrap.option(ChannelOption.TCP_FASTOPEN, 3);
}
- bootstrap.bind()
- .addListener((ChannelFutureListener) future -> {
- final Channel channel = future.channel();
- if (future.isSuccess()) {
- this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT));
-
- // Warn people with console access that HAProxy is in use, see PR: #1436
- if (this.server.getConfiguration().isProxyProtocol()) {
- LOGGER.warn("Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", channel.localAddress());
+ if (server.getConfiguration().isEnableReusePort()) {
+ // We don't need a boss group, since each worker will bind to the socket
+ bootstrap.option(UnixChannelOption.SO_REUSEPORT, true)
+ .group(this.workerGroup);
+ } else {
+ bootstrap.group(this.bossGroup, this.workerGroup);
+ }
+
+ final int binds = server.getConfiguration().isEnableReusePort()
+ ? ((MultithreadEventExecutorGroup) this.workerGroup).executorCount() : 1;
+
+ for (int bind = 0; bind < binds; bind++) {
+ // Wait for each bind to open. If we encounter any errors, don't try to bind again.
+ int finalBind = bind;
+ ChannelFuture f = bootstrap.bind()
+ .addListener((ChannelFutureListener) future -> {
+ final Channel channel = future.channel();
+ if (future.isSuccess()) {
+ this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT));
+
+ LOGGER.info("Listening on {}", channel.localAddress());
+
+ if (finalBind == 0) {
+ // Warn people with console access that HAProxy is in use, see PR: #1436
+ if (this.server.getConfiguration().isProxyProtocol()) {
+ LOGGER.warn(
+ "Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.",
+ channel.localAddress());
+ }
+
+ // Fire the proxy bound event after the socket is bound
+ server.getEventManager().fireAndForget(
+ new ListenerBoundEvent(address, ListenerType.MINECRAFT));
+ }
+ } else {
+ LOGGER.error("Can't bind to {}", address, future.cause());
}
+ });
+ f.syncUninterruptibly();
- LOGGER.info("Listening on {}", channel.localAddress());
-
- // Fire the proxy bound event after the socket is bound
- server.getEventManager().fireAndForget(
- new ListenerBoundEvent(address, ListenerType.MINECRAFT));
- } else {
- LOGGER.error("Can't bind to {}", address, future.cause());
- }
- });
+ if (!f.isSuccess()) {
+ break;
+ }
+ }
}
/**
@@ -181,17 +209,20 @@ public final class ConnectionManager {
* @param oldBind the endpoint to close
*/
public void close(InetSocketAddress oldBind) {
- Endpoint endpoint = endpoints.remove(oldBind);
+ Collection endpoints = this.endpoints.removeAll(oldBind);
+ Preconditions.checkState(!endpoints.isEmpty(), "Endpoint was not registered");
+
+ ListenerType type = endpoints.iterator().next().getType();
// Fire proxy close event to notify plugins of socket close. We block since plugins
// should have a chance to be notified before the server stops accepting connections.
- server.getEventManager().fire(new ListenerCloseEvent(oldBind, endpoint.getType())).join();
+ server.getEventManager().fire(new ListenerCloseEvent(oldBind, type)).join();
- Channel serverChannel = endpoint.getChannel();
-
- Preconditions.checkState(serverChannel != null, "Endpoint %s not registered", oldBind);
- LOGGER.info("Closing endpoint {}", serverChannel.localAddress());
- serverChannel.close().syncUninterruptibly();
+ for (Endpoint endpoint : endpoints) {
+ Channel serverChannel = endpoint.getChannel();
+ LOGGER.info("Closing endpoint {}", serverChannel.localAddress());
+ serverChannel.close().syncUninterruptibly();
+ }
}
/**
@@ -200,24 +231,28 @@ public final class ConnectionManager {
* @param interrupt should closing forward interruptions
*/
public void closeEndpoints(boolean interrupt) {
- for (final Map.Entry entry : this.endpoints.entrySet()) {
+ for (final Map.Entry> entry : this.endpoints.asMap()
+ .entrySet()) {
final InetSocketAddress address = entry.getKey();
- final Endpoint endpoint = entry.getValue();
+ final Collection endpoints = entry.getValue();
+ ListenerType type = endpoints.iterator().next().getType();
// Fire proxy close event to notify plugins of socket close. We block since plugins
// should have a chance to be notified before the server stops accepting connections.
- server.getEventManager().fire(new ListenerCloseEvent(address, endpoint.getType())).join();
+ server.getEventManager().fire(new ListenerCloseEvent(address, type)).join();
- 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();
+ for (Endpoint endpoint : endpoints) {
+ 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();
}
- } else {
- endpoint.getChannel().close().syncUninterruptibly();
}
}
this.endpoints.clear();
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 e5e02141..47f7ae85 100644
--- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
+++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
@@ -39,6 +39,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
+import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
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;
@@ -377,7 +378,8 @@ public enum StateRegistry {
map(0x0D, MINECRAFT_1_17, false),
map(0x0A, MINECRAFT_1_19, false),
map(0x0B, MINECRAFT_1_19_4, false),
- map(0x0A, MINECRAFT_1_20_2, false));
+ map(0x0A, MINECRAFT_1_20_2, false),
+ map(0x09, MINECRAFT_1_21_5, false));
clientbound.register(
LegacyChatPacket.class,
LegacyChatPacket::new,
@@ -398,7 +400,8 @@ public enum StateRegistry {
map(0x0E, MINECRAFT_1_19, false),
map(0x0D, MINECRAFT_1_19_3, false),
map(0x0F, MINECRAFT_1_19_4, false),
- map(0x10, MINECRAFT_1_20_2, false));
+ map(0x10, MINECRAFT_1_20_2, false),
+ map(0x0F, MINECRAFT_1_21_5, false));
clientbound.register(
AvailableCommandsPacket.class,
AvailableCommandsPacket::new,
@@ -410,10 +413,12 @@ public enum StateRegistry {
map(0x0F, MINECRAFT_1_19, false),
map(0x0E, MINECRAFT_1_19_3, false),
map(0x10, MINECRAFT_1_19_4, false),
- map(0x11, MINECRAFT_1_20_2, false));
+ map(0x11, MINECRAFT_1_20_2, false),
+ map(0x10, MINECRAFT_1_21_5, false));
clientbound.register(
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
- map(0x16, MINECRAFT_1_20_5, false));
+ map(0x16, MINECRAFT_1_20_5, false),
+ map(0x15, MINECRAFT_1_21_5, false));
clientbound.register(
PluginMessagePacket.class,
PluginMessagePacket::new,
@@ -430,7 +435,8 @@ public enum StateRegistry {
map(0x15, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false),
map(0x18, MINECRAFT_1_20_2, false),
- map(0x19, MINECRAFT_1_20_5, false));
+ map(0x19, MINECRAFT_1_20_5, false),
+ map(0x18, MINECRAFT_1_21_5, false));
clientbound.register(
DisconnectPacket.class,
() -> new DisconnectPacket(this),
@@ -447,7 +453,8 @@ public enum StateRegistry {
map(0x17, MINECRAFT_1_19_3, false),
map(0x1A, MINECRAFT_1_19_4, false),
map(0x1B, MINECRAFT_1_20_2, false),
- map(0x1D, MINECRAFT_1_20_5, false));
+ map(0x1D, MINECRAFT_1_20_5, false),
+ map(0x1C, MINECRAFT_1_21_5, false));
clientbound.register(
KeepAlivePacket.class,
KeepAlivePacket::new,
@@ -465,7 +472,8 @@ public enum StateRegistry {
map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, false),
- map(0x27, MINECRAFT_1_21_2, false));
+ map(0x27, MINECRAFT_1_21_2, false),
+ map(0x26, MINECRAFT_1_21_5, false));
clientbound.register(
JoinGamePacket.class,
JoinGamePacket::new,
@@ -483,7 +491,8 @@ public enum StateRegistry {
map(0x28, MINECRAFT_1_19_4, false),
map(0x29, MINECRAFT_1_20_2, false),
map(0x2B, MINECRAFT_1_20_5, false),
- map(0x2C, MINECRAFT_1_21_2, false));
+ map(0x2C, MINECRAFT_1_21_2, false),
+ map(0x2B, MINECRAFT_1_21_5, false));
clientbound.register(
RespawnPacket.class,
RespawnPacket::new,
@@ -504,13 +513,15 @@ public enum StateRegistry {
map(0x43, MINECRAFT_1_20_2, true),
map(0x45, MINECRAFT_1_20_3, true),
map(0x47, MINECRAFT_1_20_5, true),
- map(0x4C, MINECRAFT_1_21_2, true));
+ map(0x4C, MINECRAFT_1_21_2, true),
+ map(0x4B, MINECRAFT_1_21_5, true));
clientbound.register(
RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new,
map(0x43, MINECRAFT_1_20_3, false),
map(0x45, MINECRAFT_1_20_5, false),
- map(0x4A, MINECRAFT_1_21_2, false));
+ map(0x4A, MINECRAFT_1_21_2, false),
+ map(0x49, MINECRAFT_1_21_5, false));
clientbound.register(
ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new,
@@ -531,7 +542,8 @@ public enum StateRegistry {
map(0x42, MINECRAFT_1_20_2, false),
map(0x44, MINECRAFT_1_20_3, false),
map(0x46, MINECRAFT_1_20_5, false),
- map(0x4B, MINECRAFT_1_21_2, false));
+ map(0x4B, MINECRAFT_1_21_2, false),
+ map(0x4A, MINECRAFT_1_21_5, false));
clientbound.register(
HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new,
@@ -553,7 +565,8 @@ public enum StateRegistry {
map(0x68, MINECRAFT_1_20_2, true),
map(0x6A, MINECRAFT_1_20_3, true),
map(0x6D, MINECRAFT_1_20_5, true),
- map(0x74, MINECRAFT_1_21_2, true));
+ map(0x74, MINECRAFT_1_21_2, true),
+ map(0x73, MINECRAFT_1_21_5, true));
clientbound.register(
LegacyTitlePacket.class,
LegacyTitlePacket::new,
@@ -574,7 +587,8 @@ public enum StateRegistry {
map(0x5F, MINECRAFT_1_20_2, true),
map(0x61, MINECRAFT_1_20_3, true),
map(0x63, MINECRAFT_1_20_5, true),
- map(0x6A, MINECRAFT_1_21_2, true));
+ map(0x6A, MINECRAFT_1_21_2, true),
+ map(0x69, MINECRAFT_1_21_5, true));
clientbound.register(
TitleTextPacket.class,
TitleTextPacket::new,
@@ -586,7 +600,8 @@ public enum StateRegistry {
map(0x61, MINECRAFT_1_20_2, true),
map(0x63, MINECRAFT_1_20_3, true),
map(0x65, MINECRAFT_1_20_5, true),
- map(0x6C, MINECRAFT_1_21_2, true));
+ map(0x6C, MINECRAFT_1_21_2, true),
+ map(0x6B, MINECRAFT_1_21_5, true));
clientbound.register(
TitleActionbarPacket.class,
TitleActionbarPacket::new,
@@ -598,7 +613,8 @@ public enum StateRegistry {
map(0x48, MINECRAFT_1_20_2, true),
map(0x4A, MINECRAFT_1_20_3, true),
map(0x4C, MINECRAFT_1_20_5, true),
- map(0x51, MINECRAFT_1_21_2, true));
+ map(0x51, MINECRAFT_1_21_2, true),
+ map(0x50, MINECRAFT_1_21_5, true));
clientbound.register(
TitleTimesPacket.class,
TitleTimesPacket::new,
@@ -610,7 +626,8 @@ public enum StateRegistry {
map(0x62, MINECRAFT_1_20_2, true),
map(0x64, MINECRAFT_1_20_3, true),
map(0x66, MINECRAFT_1_20_5, true),
- map(0x6D, MINECRAFT_1_21_2, true));
+ map(0x6D, MINECRAFT_1_21_2, true),
+ map(0x6C, MINECRAFT_1_21_5, true));
clientbound.register(
TitleClearPacket.class,
TitleClearPacket::new,
@@ -618,7 +635,8 @@ public enum StateRegistry {
map(0x0D, MINECRAFT_1_19, true),
map(0x0C, MINECRAFT_1_19_3, true),
map(0x0E, MINECRAFT_1_19_4, true),
- map(0x0F, MINECRAFT_1_20_2, true));
+ map(0x0F, MINECRAFT_1_20_2, true),
+ map(0x0E, MINECRAFT_1_21_5, true));
clientbound.register(
LegacyPlayerListItemPacket.class,
LegacyPlayerListItemPacket::new,
@@ -638,7 +656,8 @@ public enum StateRegistry {
map(0x39, MINECRAFT_1_19_4, false),
map(0x3B, MINECRAFT_1_20_2, false),
map(0x3D, MINECRAFT_1_20_5, false),
- map(0x3F, MINECRAFT_1_21_2, false));
+ map(0x3F, MINECRAFT_1_21_2, false),
+ map(0x3E, MINECRAFT_1_21_5, false));
clientbound.register(
UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new,
@@ -646,11 +665,13 @@ public enum StateRegistry {
map(0x3A, MINECRAFT_1_19_4, false),
map(0x3C, MINECRAFT_1_20_2, false),
map(0x3E, MINECRAFT_1_20_5, false),
- map(0x40, MINECRAFT_1_21_2, false));
+ map(0x40, MINECRAFT_1_21_2, false),
+ map(0x3F, MINECRAFT_1_21_5, false));
clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false),
- map(0x72, MINECRAFT_1_21_2, false));
+ map(0x72, MINECRAFT_1_21_2, false),
+ map(0x71, MINECRAFT_1_21_5, false));
clientbound.register(
SystemChatPacket.class,
SystemChatPacket::new,
@@ -661,7 +682,8 @@ public enum StateRegistry {
map(0x67, MINECRAFT_1_20_2, true),
map(0x69, MINECRAFT_1_20_3, true),
map(0x6C, MINECRAFT_1_20_5, true),
- map(0x73, MINECRAFT_1_21_2, true));
+ map(0x73, MINECRAFT_1_21_2, true),
+ map(0x72, MINECRAFT_1_21_5, true));
clientbound.register(
PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new,
@@ -669,7 +691,8 @@ public enum StateRegistry {
map(0x14, MINECRAFT_1_19_3, true),
map(0x16, MINECRAFT_1_19_4, true),
map(0x17, MINECRAFT_1_20_2, true),
- map(0x18, MINECRAFT_1_20_5, true));
+ map(0x18, MINECRAFT_1_20_5, true),
+ map(0x17, MINECRAFT_1_21_5, true));
clientbound.register(
ServerDataPacket.class,
ServerDataPacket::new,
@@ -680,14 +703,16 @@ public enum StateRegistry {
map(0x47, MINECRAFT_1_20_2, false),
map(0x49, MINECRAFT_1_20_3, false),
map(0x4B, MINECRAFT_1_20_5, false),
- map(0x50, MINECRAFT_1_21_2, false));
+ map(0x50, MINECRAFT_1_21_2, false),
+ map(0x4F, MINECRAFT_1_21_5, false));
clientbound.register(
StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE,
map(0x65, MINECRAFT_1_20_2, false),
map(0x67, MINECRAFT_1_20_3, false),
map(0x69, MINECRAFT_1_20_5, false),
- map(0x70, MINECRAFT_1_21_2, false));
+ map(0x70, MINECRAFT_1_21_2, false),
+ map(0x6F, MINECRAFT_1_21_5, false));
clientbound.register(
BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE,
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 6e7fb4d4..5321b6ac 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
@@ -52,6 +52,9 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder {
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List