Compare commits

..

35 Commits

Author SHA1 Message Date
b06af3718c Merge remote-tracking branch 'github/dev/3.0.0'
All checks were successful
SteamWarCI Build successful
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionPlayerCommandPacket.java
2025-04-01 07:06:14 +02:00
9324a52ce0 fix: server link's custom labels not being translated (#1537) 2025-03-31 12:58:43 -05:00
cc93f5eea4 feat: improve tablist (#1532)
* fix: setDisplayName in TabListEntry duplicating players on 1.7.10 (#1530)

* feat: expose toggling hat layer in TabListEntry (#1531)
2025-03-30 12:52:54 -05:00
c3d10bd410 feat: add function to directly pass collections in ServerPing.Builder (#1538) 2025-03-30 12:49:36 -05:00
d2cd79185b Minecraft 1.21.5 (#1489)
Missing adventure component changes, so entity and item hovers from the API may not work for 1.21.5 clients

Co-authored-by: Gero <gecam59@gmail.com>
Co-authored-by: Shane Freeder <theboyetronic@gmail.com>
2025-03-27 15:56:15 +01:00
4df640268f Fix: discard chat queue and chat state when switching servers (#1534)
This has two effects:
- Will no longer send queued chat packets from previous server after switch (race condition)
- The offset in 'last seen' updates will be corrected, as the internal ChatState will be reset (only applied if the player had not sent a message in a while, and >20 messages had been received)
2025-03-21 12:28:13 +00:00
d9f1016bd5 Validate uncompressed packet size (#1527)
* Validate uncompressed packet size

* Fix debug using incorrect value
2025-03-14 15:26:59 +00:00
c9aa1cca09 Enable use tab in javadocs (#1525) 2025-03-10 15:11:01 +00:00
f980037bfd chore(proxy): require explicitly setting velocity.command.info permission to true 2025-03-06 19:14:18 -08:00
8f3dea5427 Bump Netty version 2025-02-28 22:21:32 -05:00
b8fe3577c9 Use correct component serializer in ComponentHolder#getBinaryTag 2025-02-28 15:01:30 +01:00
d4ea40a4a2 Add support for SO_REUSEPORT 2025-02-27 23:42:39 -05:00
0afe061224 Updated Adventure to 4.19.0 (#1520)
Also updated ASM and Ansi to support Java 23 and 24
2025-02-27 01:25:01 -05:00
58816c804a fix: problem with PluginMessageEvents for configuration phase (#1517) 2025-02-22 16:08:37 +00:00
e69213f987 respect log-player-connections flag in config for connection messages (#1503) 2025-02-17 12:07:06 +00:00
f986eb51ec Do not print an exception if a client closed before switching to config state 2025-02-13 12:12:33 +00:00
9d1332d3a3 Add PluginMessageEvents for configuration phase 2025-02-13 11:53:30 +00:00
83c1749eed Add javadoc for ServerPing.Builder (#1500)
* Add javadoc for ServerPing.Builder

* Address codestyle reports
2025-02-12 14:35:38 -05:00
a26d5581c4 docs: Remove beta annotations on events (#1505)
These events aren't in beta, some of them have been stable for years now.
2025-02-12 14:35:15 -05:00
1652d44f5f Fix checkstyle 💩 2025-01-31 19:35:43 +01:00
6815808d32 Improve fml mod list parsing 2025-01-31 19:11:34 +01:00
91bdcebb1a Use real vanilla limits for LegacyChat (#1502)
Client -> server
< 1.11 has 100
>= 1.11 has 256

Server -> client has always 262144
2025-01-31 13:11:39 +00:00
0e84b57e53 Add Virtualhost support for server list pings (#1265)
* Add virtualhost support for server list pings

* Add virtualhost support to ping public API

* Applied suggestions

* fixed checkstyle

* Added nullable annotation

---------

Co-authored-by: Adrian <adriangonzalesval@gmail.com>
2025-01-31 07:04:52 -05:00
876b9c3601 Fix ShutdownCommand message styling (#1427)
* Fix ShutdownCommand message

* Fix checkstyle violation.
2025-01-26 23:03:37 -05:00
6995f415d3 Expose shutdownInProgress to the API. (#1485)
Co-authored-by: kyngs <kyngs@users.noreply.github.com>
2025-01-26 07:49:35 -05:00
371e686076 properly apply vanilla cap to chat packets 2025-01-23 19:30:55 +00:00
7392cd6574 Fix nonsensical deprecation for specifying listener priority (#1491)
* Fix nonsensical deprecation for specifying listener priority

* Fix checkstyle error
2025-01-21 12:37:20 -05:00
71feb11b2e Fix fallback compression handler 2025-01-17 15:11:03 +00:00
c0fdf20224 Add InboundConnection#getHandshakeIntent (#1493)
* Add InboundConnection#getHandshakeIntent
2025-01-14 20:44:20 -05:00
00b68859ff Add "GetPlayerServer". (#1484) 2025-01-02 09:53:53 +00:00
1db8c8c6ab Bump adventure to 4.18.0 (#1481) 2024-12-23 02:59:13 +00:00
af97ffffa5 A few small code cleanups for cryptography
* Remove some unused cryptographic code
* Add some notes about how Minecraft's cryptography choices have not quite survived the test of time
2024-12-21 03:45:17 -05:00
39191957ea Migrate to setup-gradle, Gradle 8.11.1. (#1480) 2024-12-21 03:02:30 -05:00
d77e508e9c [ci skip] Fix typo in TabListEntry latency docs (#1479) 2024-12-19 16:04:56 +00:00
4aa9ee7735 fix: NoClassDefFoundError on FastUtil's toIntArray (#1475) 2024-12-14 20:48:08 +00:00
67 changed files with 731 additions and 238 deletions

View File

@ -10,13 +10,14 @@ jobs:
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Validate Gradle Wrapper with:
uses: gradle/actions/wrapper-validation@v3 persist-credentials: false
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Set up JDK 17 - name: Set up JDK 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: 'temurin' distribution: 'temurin'
cache: 'gradle'
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew build

View File

@ -61,6 +61,7 @@ tasks {
o.encoding = "UTF-8" o.encoding = "UTF-8"
o.source = "17" o.source = "17"
o.use()
o.links( o.links(
"https://www.slf4j.org/apidocs/", "https://www.slf4j.org/apidocs/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/", "https://guava.dev/releases/${libs.guava.get().version}/api/docs/",

View File

@ -27,7 +27,7 @@ public interface CommandSource extends Audience, PermissionSubject {
* for more information on the format. * for more information on the format.
**/ **/
default void sendRichMessage(final @NotNull String message) { 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 String message,
final @NotNull TagResolver @NotNull... resolvers final @NotNull TagResolver @NotNull... resolvers
) { ) {
this.sendMessage(MiniMessage.miniMessage().deserialize(message, resolvers)); this.sendMessage(MiniMessage.miniMessage().deserialize(message, this, resolvers));
} }
/** /**

View File

@ -12,6 +12,14 @@ package com.velocitypowered.api.event;
*/ */
public enum PostOrder { 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
} }

View File

@ -32,12 +32,9 @@ public @interface Subscribe {
* The priority of this event handler. Priorities are used to determine the order in which event * 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. * handlers are called. The higher the priority, the earlier the event handler will be called.
* *
* <p>Note that due to compatibility constraints, you must specify {@link PostOrder#CUSTOM}
* in order to use this field.</p>
*
* @return the priority * @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 * Whether the handler must be called asynchronously. By default, all event handlers are called

View File

@ -9,7 +9,6 @@ package com.velocitypowered.api.event.command;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.Beta;
import com.mojang.brigadier.tree.RootCommandNode; import com.mojang.brigadier.tree.RootCommandNode;
import com.velocitypowered.api.event.annotation.AwaitingEvent; import com.velocitypowered.api.event.annotation.AwaitingEvent;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
@ -21,7 +20,6 @@ import com.velocitypowered.api.proxy.Player;
* client. * client.
*/ */
@AwaitingEvent @AwaitingEvent
@Beta
public class PlayerAvailableCommandsEvent { public class PlayerAvailableCommandsEvent {
private final Player player; private final Player player;

View File

@ -7,7 +7,6 @@
package com.velocitypowered.api.event.player; package com.velocitypowered.api.event.player;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.server.RegisteredServer; 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 * available in {@link Player#getCurrentServer()}. Velocity will not wait on this event to finish
* firing. * firing.
*/ */
@Beta
public class ServerPostConnectEvent { public class ServerPostConnectEvent {
private final Player player; private final Player player;
private final RegisteredServer previousServer; private final RegisteredServer previousServer;

View File

@ -7,7 +7,6 @@
package com.velocitypowered.api.event.proxy.server; package com.velocitypowered.api.event.proxy.server;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo; 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. * @param registeredServer A {@link RegisteredServer} that has been registered.
* @since 3.3.0 * @since 3.3.0
*/ */
@Beta
public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) { public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) {
public ServerRegisteredEvent { public ServerRegisteredEvent {
Preconditions.checkNotNull(registeredServer, "registeredServer"); Preconditions.checkNotNull(registeredServer, "registeredServer");

View File

@ -7,7 +7,6 @@
package com.velocitypowered.api.event.proxy.server; package com.velocitypowered.api.event.proxy.server;
import com.google.common.annotations.Beta;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import com.velocitypowered.api.proxy.server.ServerInfo; 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. * @param unregisteredServer A {@link RegisteredServer} that has been unregistered.
* @since 3.3.0 * @since 3.3.0
*/ */
@Beta
public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) { public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) {
public ServerUnregisteredEvent { public ServerUnregisteredEvent {
Preconditions.checkNotNull(unregisteredServer, "unregisteredServer"); Preconditions.checkNotNull(unregisteredServer, "unregisteredServer");

View File

@ -89,7 +89,8 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"),
MINECRAFT_1_21(767, "1.21", "1.21.1"), MINECRAFT_1_21(767, "1.21", "1.21.1"),
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"), 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; private static final int SNAPSHOT_BIT = 30;

View File

@ -7,6 +7,7 @@
package com.velocitypowered.api.proxy; package com.velocitypowered.api.proxy;
import com.velocitypowered.api.network.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -60,4 +61,11 @@ public interface InboundConnection {
* @return the protocol state of the connection * @return the protocol state of the connection
*/ */
ProtocolState getProtocolState(); ProtocolState getProtocolState();
/**
* Returns the initial intent for the connection.
*
* @return the intent of the connection
*/
HandshakeIntent getHandshakeIntent();
} }

View File

@ -41,6 +41,13 @@ public interface ProxyServer extends Audience {
*/ */
void shutdown(); 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. * Closes all listening endpoints for this server.
* This includes the main minecraft listener and query channel. * This includes the main minecraft listener and query channel.

View File

@ -40,6 +40,7 @@ public interface TabList {
* Adds a {@link TabListEntry} to the {@link Player}'s tab list. * Adds a {@link TabListEntry} to the {@link Player}'s tab list.
* *
* @param entry to add to the 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); 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. * Adds a {@link Iterable} of {@link TabListEntry}'s to the {@link Player}'s tab list.
* *
* @param entries to add to the 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<TabListEntry> entries) { default void addEntries(Iterable<TabListEntry> entries) {
for (TabListEntry entry : 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. * Adds an array of {@link TabListEntry}'s to the {@link Player}'s tab list.
* *
* @param entries to add to the 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) { default void addEntries(TabListEntry... entries) {
for (TabListEntry entry : entries) { for (TabListEntry entry : entries) {
@ -187,6 +190,26 @@ public interface TabList {
* @deprecated Internal usage. Use {@link TabListEntry.Builder} instead. * @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
*/ */
@Deprecated @Deprecated
TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, default 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) {
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, boolean showHat);
} }

View File

@ -80,7 +80,7 @@ public interface TabListEntry extends KeyIdentifiable {
* <li>150-300 will display 4 bars</li> * <li>150-300 will display 4 bars</li>
* <li>300-600 will display 3 bars</li> * <li>300-600 will display 3 bars</li>
* <li>600-1000 will display 2 bars</li> * <li>600-1000 will display 2 bars</li>
* <li>A latency move than 1 second will display 1 bar</li> * <li>A latency greater than 1 second will display 1 bar</li>
* </ul> * </ul>
* *
* @return latency set for {@code this} entry * @return latency set for {@code this} entry
@ -160,6 +160,27 @@ public interface TabListEntry extends KeyIdentifiable {
return this; 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}. * Returns a {@link Builder} to create a {@link TabListEntry}.
* *
@ -183,6 +204,7 @@ public interface TabListEntry extends KeyIdentifiable {
private int gameMode = 0; private int gameMode = 0;
private boolean listed = true; private boolean listed = true;
private int listOrder = 0; private int listOrder = 0;
private boolean showHat;
private @Nullable ChatSession chatSession; private @Nullable ChatSession chatSession;
@ -268,7 +290,7 @@ public interface TabListEntry extends KeyIdentifiable {
* Sets whether this entry should be visible. * Sets whether this entry should be visible.
* *
* @param listed to set * @param listed to set
* @return ${code this}, for chaining * @return {@code this}, for chaining
* @see TabListEntry#isListed() * @see TabListEntry#isListed()
*/ */
public Builder listed(boolean listed) { 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. * Sets the order/priority of this entry in the tab list.
* *
* @param order to set * @param order to set
* @return ${code this}, for chaining * @return {@code this}, for chaining
* @sinceMinecraft 1.21.2 * @sinceMinecraft 1.21.2
* @see TabListEntry#getListOrder() * @see TabListEntry#getListOrder()
*/ */
@ -289,6 +311,18 @@ public interface TabListEntry extends KeyIdentifiable {
return this; 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}. * Constructs the {@link TabListEntry} specified by {@code this} {@link Builder}.
* *
@ -301,7 +335,7 @@ public interface TabListEntry extends KeyIdentifiable {
if (profile == null) { if (profile == null) {
throw new IllegalStateException("The GameProfile must be set when building a TabListEntry"); 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);
} }
} }
} }

View File

@ -15,6 +15,7 @@ import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.kyori.adventure.builder.AbstractBuilder; import net.kyori.adventure.builder.AbstractBuilder;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/** /**
* Contains the parameters used to ping a {@link RegisteredServer}. * 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(); public static final PingOptions DEFAULT = PingOptions.builder().build();
private final ProtocolVersion protocolVersion; private final ProtocolVersion protocolVersion;
private final long timeout; private final long timeout;
private final String virtualHost;
private PingOptions(final Builder builder) { private PingOptions(final Builder builder) {
this.protocolVersion = builder.protocolVersion; this.protocolVersion = builder.protocolVersion;
this.timeout = builder.timeout; this.timeout = builder.timeout;
this.virtualHost = builder.virtualHost;
} }
/** /**
@ -54,6 +57,16 @@ public final class PingOptions {
return this.timeout; 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. * Create a new builder to assign values to a new PingOptions.
* *
@ -68,10 +81,9 @@ public final class PingOptions {
if (o == null) { if (o == null) {
return false; return false;
} }
if (!(o instanceof PingOptions)) { if (!(o instanceof final PingOptions other)) {
return false; return false;
} }
final PingOptions other = (PingOptions) o;
return Objects.equals(this.protocolVersion, other.protocolVersion) return Objects.equals(this.protocolVersion, other.protocolVersion)
&& Objects.equals(this.timeout, other.timeout); && Objects.equals(this.timeout, other.timeout);
} }
@ -97,6 +109,7 @@ public final class PingOptions {
public static final class Builder implements AbstractBuilder<PingOptions> { public static final class Builder implements AbstractBuilder<PingOptions> {
private ProtocolVersion protocolVersion = ProtocolVersion.UNKNOWN; private ProtocolVersion protocolVersion = ProtocolVersion.UNKNOWN;
private long timeout = 0; private long timeout = 0;
private String virtualHost = null;
private Builder() { private Builder() {
} }
@ -146,6 +159,18 @@ public final class PingOptions {
return this; 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. * Create a new {@link PingOptions} with the values of this Builder.
* *

View File

@ -14,6 +14,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.api.util.ModInfo;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; 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) { public Builder version(Version version) {
this.version = Preconditions.checkNotNull(version, "version"); this.version = Preconditions.checkNotNull(version, "version");
return this; 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) { public Builder onlinePlayers(int onlinePlayers) {
this.onlinePlayers = onlinePlayers; this.onlinePlayers = onlinePlayers;
return this; return this;
} }
/**
* Uses the modified {@code maximumPlayers} number in the response.
* <b>This will not modify the actual maximum players that can join the server.</b>
*
* @param maximumPlayers number for maximum players to set
* @return this builder, for chaining
*/
public Builder maximumPlayers(int maximumPlayers) { public Builder maximumPlayers(int maximumPlayers) {
this.maximumPlayers = maximumPlayers; this.maximumPlayers = maximumPlayers;
return this; 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) { public Builder samplePlayers(SamplePlayer... players) {
this.samplePlayers.addAll(Arrays.asList(players)); this.samplePlayers.addAll(Arrays.asList(players));
return this; 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<SamplePlayer> 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) { public Builder modType(String modType) {
this.modType = Preconditions.checkNotNull(modType, "modType"); this.modType = Preconditions.checkNotNull(modType, "modType");
return this; 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) { public Builder mods(ModInfo.Mod... mods) {
this.mods.addAll(Arrays.asList(mods)); this.mods.addAll(Arrays.asList(mods));
return this; return this;
@ -194,7 +243,7 @@ public final class ServerPing {
* Uses the modified {@code mods} list in the response. * Uses the modified {@code mods} list in the response.
* *
* @param mods the mods list to use * @param mods the mods list to use
* @return this build, for chaining * @return this builder, for chaining
*/ */
public Builder mods(ModInfo mods) { public Builder mods(ModInfo mods) {
Preconditions.checkNotNull(mods, "mods"); Preconditions.checkNotNull(mods, "mods");
@ -204,36 +253,74 @@ public final class ServerPing {
return this; return this;
} }
/**
* Clears the current list of mods to use in the response.
*
* @return this builder, for chaining
*/
public Builder clearMods() { public Builder clearMods() {
this.mods.clear(); this.mods.clear();
return this; return this;
} }
/**
* Clears the current list of PlayerSamples to use in the response.
*
* @return this builder, for chaining
*/
public Builder clearSamplePlayers() { public Builder clearSamplePlayers() {
this.samplePlayers.clear(); this.samplePlayers.clear();
return this; return this;
} }
/**
* Defines the server as mod incompatible in the response.
*
* @return this builder, for chaining
*/
public Builder notModCompatible() { public Builder notModCompatible() {
this.nullOutModinfo = true; this.nullOutModinfo = true;
return this; return this;
} }
/**
* Enables nulling Players in the response.
* This will display the player count as {@code ???}.
*
* @return this builder, for chaining
*/
public Builder nullPlayers() { public Builder nullPlayers() {
this.nullOutPlayers = true; this.nullOutPlayers = true;
return this; 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) { public Builder description(net.kyori.adventure.text.Component description) {
this.description = Preconditions.checkNotNull(description, "description"); this.description = Preconditions.checkNotNull(description, "description");
return this; 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) { public Builder favicon(Favicon favicon) {
this.favicon = Preconditions.checkNotNull(favicon, "favicon"); this.favicon = Preconditions.checkNotNull(favicon, "favicon");
return this; return this;
} }
/**
* Clears the current favicon used in the response.
*
* @return this builder, for chaining
*/
public Builder clearFavicon() { public Builder clearFavicon() {
this.favicon = null; this.favicon = null;
return this; return this;

View File

@ -76,9 +76,17 @@ public final class ModInfo {
private final String id; private final String id;
private final String version; 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) { public Mod(String id, String version) {
this.id = Preconditions.checkNotNull(id, "id"); this.id = Preconditions.checkNotNull(id, "id");
this.version = Preconditions.checkNotNull(version, "version"); 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() { public String getId() {

View File

@ -3,7 +3,7 @@ configurate3 = "3.7.3"
configurate4 = "4.1.2" configurate4 = "4.1.2"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.24.1" log4j = "2.24.1"
netty = "4.1.114.Final" netty = "4.1.119.Final"
[plugins] [plugins]
indra-publishing = "net.kyori.indra.publishing:2.0.6" 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" spotless = "com.diffplug.spotless:6.25.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom: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.17.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" 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 = "com.google.auto.service:auto-service:1.0.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations: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" 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" caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
checker-qual = "org.checkerframework:checker-qual:3.42.0" checker-qual = "org.checkerframework:checker-qual:3.42.0"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" 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" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:5.10.2" junit = "org.junit.jupiter:junit-jupiter:5.10.2"
jspecify = "org.jspecify:jspecify:0.3.0" 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" guava = "com.google.guava:guava:25.1-jre"
gson = "com.google.code.gson:gson:2.10.1" gson = "com.google.code.gson:gson:2.10.1"
guice = "com.google.inject:guice:6.0.0" guice = "com.google.inject:guice:6.0.0"

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists 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 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

9
gradlew vendored
View File

@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (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. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@ -84,7 +86,8 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # 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. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum 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. # 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: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,

4
gradlew.bat vendored
View File

@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@ -34,7 +36,7 @@ set APP_HOME=%DIRNAME%
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 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. @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 @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome

View File

@ -26,6 +26,15 @@ Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_init(JNIEnv *env,
return 0; 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; CCCryptorRef cryptor = NULL;
CCCryptorStatus result = CCCryptorCreateWithMode(encrypt ? kCCEncrypt : kCCDecrypt, CCCryptorStatus result = CCCryptorCreateWithMode(encrypt ? kCCEncrypt : kCCDecrypt,
kCCModeCFB8, kCCModeCFB8,

View File

@ -32,6 +32,15 @@ Java_com_velocitypowered_natives_encryption_OpenSslCipherImpl_init(JNIEnv *env,
return 0; 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, int result = EVP_CipherInit(ctx, EVP_aes_128_cfb8(), (byte*) keyBytes, (byte*) keyBytes,
encrypt); encrypt);
if (result != 1) { if (result != 1) {

View File

@ -57,7 +57,8 @@ public class JavaVelocityCompressor implements VelocityCompressor {
inflater.setInput(source.nioBuffer()); inflater.setInput(source.nioBuffer());
try { try {
while (!inflater.finished() && inflater.getBytesWritten() < uncompressedSize) { final int readable = source.readableBytes();
while (!inflater.finished() && inflater.getBytesRead() < readable) {
if (!destination.isWritable()) { if (!destination.isWritable()) {
destination.ensureWritable(ZLIB_BUFFER_SIZE); destination.ensureWritable(ZLIB_BUFFER_SIZE);
} }

View File

@ -48,6 +48,15 @@ public class JavaVelocityCipher implements VelocityCipher {
private JavaVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException { private JavaVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException {
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding"); 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, this.cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key,
new IvParameterSpec(key.getEncoded())); new IvParameterSpec(key.getEncoded()));
} }

View File

@ -51,7 +51,11 @@ tasks {
exclude("it/unimi/dsi/fastutil/ints/*Int2Short*") exclude("it/unimi/dsi/fastutil/ints/*Int2Short*")
exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*") exclude("it/unimi/dsi/fastutil/ints/*Int2Reference*")
exclude("it/unimi/dsi/fastutil/ints/IntAVL*") 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/*IntBi*")
exclude("it/unimi/dsi/fastutil/ints/Int*Pair") exclude("it/unimi/dsi/fastutil/ints/Int*Pair")
exclude("it/unimi/dsi/fastutil/ints/IntLinked*") exclude("it/unimi/dsi/fastutil/ints/IntLinked*")

View File

@ -65,7 +65,8 @@ public class Metrics {
logger::info, logger::info,
config.isLogErrorsEnabled(), config.isLogErrorsEnabled(),
config.isLogSentDataEnabled(), config.isLogSentDataEnabled(),
config.isLogResponseStatusTextEnabled() config.isLogResponseStatusTextEnabled(),
false
); );
if (!config.didExistBefore()) { if (!config.didExistBefore()) {

View File

@ -236,6 +236,15 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
registerTranslations(); 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); serverKeyPair = EncryptionUtils.createRsaKeyPair(1024);
cm.logChannelInformation(); cm.logChannelInformation();
@ -795,6 +804,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return channelRegistrar; return channelRegistrar;
} }
@Override
public boolean isShuttingDown() {
return shutdownInProgress.get();
}
@Override @Override
public InetSocketAddress getBoundAddress() { public InetSocketAddress getBoundAddress() {
if (configuration == null) { if (configuration == null) {

View File

@ -17,6 +17,7 @@
package com.velocitypowered.proxy.command.builtin; package com.velocitypowered.proxy.command.builtin;
import com.google.gson.JsonSyntaxException;
import com.mojang.brigadier.Command; import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; 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.command.CommandSource;
import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage; 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. * Shuts down the proxy.
@ -53,11 +55,22 @@ public final class ShutdownCommand {
StringArgumentType.greedyString()) StringArgumentType.greedyString())
.executes(context -> { .executes(context -> {
String reason = context.getArgument("reason", String.class); String reason = context.getArgument("reason", String.class);
server.shutdown(true, MiniMessage.miniMessage().deserialize( Component reasonComponent = null;
MiniMessage.miniMessage().serialize(
LegacyComponentSerializer.legacy('&').deserialize(reason) 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; return Command.SINGLE_SUCCESS;
}) })
).build()); ).build());

View File

@ -82,7 +82,7 @@ public final class VelocityCommand {
.executes(new Heap()) .executes(new Heap())
.build(); .build();
final LiteralCommandNode<CommandSource> info = BrigadierCommand.literalArgumentBuilder("info") final LiteralCommandNode<CommandSource> 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)) .executes(new Info(server))
.build(); .build();
final LiteralCommandNode<CommandSource> plugins = BrigadierCommand final LiteralCommandNode<CommandSource> plugins = BrigadierCommand

View File

@ -407,6 +407,10 @@ public class VelocityConfiguration implements ProxyConfig {
return forceKeyAuthentication; return forceKeyAuthentication;
} }
public boolean isEnableReusePort() {
return advanced.isEnableReusePort();
}
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
@ -716,6 +720,8 @@ public class VelocityConfiguration implements ProxyConfig {
private boolean logPlayerConnections = true; private boolean logPlayerConnections = true;
@Expose @Expose
private boolean acceptTransfers = false; private boolean acceptTransfers = false;
@Expose
private boolean enableReusePort = false;
private Advanced() { private Advanced() {
} }
@ -741,6 +747,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.logCommandExecutions = config.getOrElse("log-command-executions", false); this.logCommandExecutions = config.getOrElse("log-command-executions", false);
this.logPlayerConnections = config.getOrElse("log-player-connections", true); this.logPlayerConnections = config.getOrElse("log-player-connections", true);
this.acceptTransfers = config.getOrElse("accepts-transfers", false); 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; return this.acceptTransfers;
} }
public boolean isEnableReusePort() {
return enableReusePort;
}
@Override @Override
public String toString() { public String toString() {
return "Advanced{" return "Advanced{"
@ -821,6 +832,7 @@ public class VelocityConfiguration implements ProxyConfig {
+ ", logCommandExecutions=" + logCommandExecutions + ", logCommandExecutions=" + logCommandExecutions
+ ", logPlayerConnections=" + logPlayerConnections + ", logPlayerConnections=" + logPlayerConnections
+ ", acceptTransfers=" + acceptTransfers + ", acceptTransfers=" + acceptTransfers
+ ", enableReusePort=" + enableReusePort
+ '}'; + '}';
} }
} }

View File

@ -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) { static String getBungeeCordChannel(ProtocolVersion version) {
return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId() return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId()
: LEGACY_CHANNEL.getId(); : LEGACY_CHANNEL.getId();
@ -331,6 +346,9 @@ public class BungeeCordMessageResponder {
ByteBufDataInput in = new ByteBufDataInput(message.content()); ByteBufDataInput in = new ByteBufDataInput(message.content());
String subChannel = in.readUTF(); String subChannel = in.readUTF();
switch (subChannel) { switch (subChannel) {
case "GetPlayerServer":
this.processGetPlayerServer(in);
break;
case "ForwardToPlayer": case "ForwardToPlayer":
this.processForwardToPlayer(in); this.processForwardToPlayer(in);
break; break;

View File

@ -17,6 +17,7 @@
package com.velocitypowered.proxy.connection.backend; 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.connection.PreTransferEvent;
import com.velocitypowered.api.event.player.CookieRequestEvent; import com.velocitypowered.api.event.player.CookieRequestEvent;
import com.velocitypowered.api.event.player.CookieStoreEvent; 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.ServerResourcePackRemoveEvent;
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.api.proxy.player.ResourcePackInfo;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection; 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.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import java.io.IOException; import java.io.IOException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -261,7 +265,29 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion())); serverConn.getPlayer().getProtocolVersion()));
} else { } else {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
serverConn.getPlayer().getConnection().write(packet.retain()); 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; return true;
} }

View File

@ -97,7 +97,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
// Initiate a regular connection and move over to it. // Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(), ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(),
mcConnection, inbound.getVirtualHost().orElse(null), inbound.getRawVirtualHost().orElse(null), onlineMode, mcConnection, inbound.getVirtualHost().orElse(null), inbound.getRawVirtualHost().orElse(null), onlineMode,
inbound.getIdentifiedKey()); inbound.getHandshakeIntent(), inbound.getIdentifiedKey());
this.connectedPlayer = player; this.connectedPlayer = player;
if (!server.canRegisterConnection(player)) { if (!server.canRegisterConnection(player)) {
player.disconnect0( player.disconnect0(
@ -106,7 +106,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
} }
if (server.getConfiguration().isLogPlayerConnections()) {
logger.info("{} has connected", player); logger.info("{} has connected", player);
}
return server.getEventManager() return server.getEventManager()
.fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS)) .fire(new PermissionsSetupEvent(player, ConnectedPlayer.DEFAULT_PERMISSIONS))

View File

@ -17,14 +17,17 @@
package com.velocitypowered.proxy.connection.client; 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.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.configuration.PlayerConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerConfigurationEvent;
import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent;
import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; 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.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import com.velocitypowered.proxy.protocol.MinecraftPacket; 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.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -123,8 +127,32 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
brandChannel = packet.getChannel(); brandChannel = packet.getChannel();
// Client sends `minecraft:brand` packet immediately after Login, // Client sends `minecraft:brand` packet immediately after Login,
// but at this time the backend server may not be ready // but at this time the backend server may not be ready
} else if (BungeeCordMessageResponder.isBungeeCordMessage(packet)) {
return true;
} else if (serverConn != null) { } else if (serverConn != null) {
byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
serverConn.ensureConnected().write(packet.retain()); 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; return true;
} }

View File

@ -170,6 +170,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void deactivated() { public void deactivated() {
player.discardChatQueue();
for (PluginMessagePacket message : loginPluginMessages) { for (PluginMessagePacket message : loginPluginMessages) {
ReferenceCountUtil.release(message); ReferenceCountUtil.release(message);
} }
@ -426,6 +427,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return true; 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 @Override
public void handleGeneric(MinecraftPacket packet) { public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer(); VelocityServerConnection serverConnection = player.getConnectedServer();

View File

@ -38,6 +38,7 @@ import com.velocitypowered.api.event.player.PlayerModInfoEvent;
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnterConfigurationEvent; 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.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.permission.PermissionFunction;
@ -156,6 +157,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private final MinecraftConnection connection; private final MinecraftConnection connection;
private final @Nullable InetSocketAddress virtualHost; private final @Nullable InetSocketAddress virtualHost;
private final @Nullable String rawVirtualHost; private final @Nullable String rawVirtualHost;
private final HandshakeIntent handshakeIntent;
private GameProfile profile; private GameProfile profile;
private PermissionFunction permissionFunction; private PermissionFunction permissionFunction;
private int tryIndex = 0; private int tryIndex = 0;
@ -188,17 +190,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private @Nullable Locale effectiveLocale; private @Nullable Locale effectiveLocale;
private final @Nullable IdentifiedKey playerKey; private final @Nullable IdentifiedKey playerKey;
private @Nullable ClientSettingsPacket clientSettingsPacket; private @Nullable ClientSettingsPacket clientSettingsPacket;
private final ChatQueue chatQueue; private volatile ChatQueue chatQueue;
private final ChatBuilderFactory chatBuilderFactory; private final ChatBuilderFactory chatBuilderFactory;
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode, @Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
@Nullable IdentifiedKey playerKey) { HandshakeIntent handshakeIntent, @Nullable IdentifiedKey playerKey) {
this.server = server; this.server = server;
this.profile = profile; this.profile = profile;
this.connection = connection; this.connection = connection;
this.virtualHost = virtualHost; this.virtualHost = virtualHost;
this.rawVirtualHost = rawVirtualHost; this.rawVirtualHost = rawVirtualHost;
this.handshakeIntent = handshakeIntent;
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
this.connectionPhase = connection.getType().getInitialClientPhase(); this.connectionPhase = connection.getType().getInitialClientPhase();
this.onlineMode = onlineMode; this.onlineMode = onlineMode;
@ -233,6 +236,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return chatQueue; 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() { public BundleDelimiterHandler getBundleHandler() {
return this.bundleHandler; 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"); throw new IllegalStateException("Can only send server links in CONFIGURATION or PLAY protocol");
} }
connection.write(new ClientboundServerLinksPacket(List.copyOf(links).stream() connection.write(new ClientboundServerLinksPacket(links.stream()
.map(l -> new ClientboundServerLinksPacket.ServerLink(l, getProtocolVersion())).toList())); .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 @Override
@ -1284,6 +1304,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
public void switchToConfigState() { public void switchToConfigState() {
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer())) server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> { .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()) { if (bundleHandler.isInBundleSession()) {
bundleHandler.toggleBundleSession(); bundleHandler.toggleBundleSession();
connection.write(BundleDelimiterPacket.INSTANCE); connection.write(BundleDelimiterPacket.INSTANCE);
@ -1335,6 +1360,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return connection.getState().toProtocolState(); return connection.getState().toProtocolState();
} }
@Override
public HandshakeIntent getHandshakeIntent() {
return handshakeIntent;
}
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect; private final RegisteredServer toConnect;

View File

@ -277,5 +277,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
public ProtocolState getProtocolState() { public ProtocolState getProtocolState() {
return connection.getState().toProtocolState(); return connection.getState().toProtocolState();
} }
@Override
public HandshakeIntent getHandshakeIntent() {
return HandshakeIntent.STATUS;
}
} }
} }

View File

@ -17,6 +17,7 @@
package com.velocitypowered.proxy.connection.client; package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.network.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.InboundConnection; import com.velocitypowered.api.proxy.InboundConnection;
@ -98,6 +99,11 @@ public final class InitialInboundConnection implements VelocityInboundConnection
return connection.getState().toProtocolState(); return connection.getState().toProtocolState();
} }
@Override
public HandshakeIntent getHandshakeIntent() {
return handshake.getIntent();
}
/** /**
* Disconnects the connection from the server. * Disconnects the connection from the server.
* *

View File

@ -17,6 +17,7 @@
package com.velocitypowered.proxy.connection.client; package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.api.network.HandshakeIntent;
import com.velocitypowered.api.network.ProtocolState; import com.velocitypowered.api.network.ProtocolState;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.LoginPhaseConnection; import com.velocitypowered.api.proxy.LoginPhaseConnection;
@ -177,4 +178,9 @@ public class LoginInboundConnection implements LoginPhaseConnection, KeyIdentifi
public ProtocolState getProtocolState() { public ProtocolState getProtocolState() {
return delegate.getProtocolState(); return delegate.getProtocolState();
} }
@Override
public HandshakeIntent getHandshakeIntent() {
return delegate.getHandshakeIntent();
}
} }

View File

@ -64,6 +64,7 @@ class LegacyForgeUtil {
if (discriminator == MOD_LIST_DISCRIMINATOR) { if (discriminator == MOD_LIST_DISCRIMINATOR) {
ImmutableList.Builder<ModInfo.Mod> mods = ImmutableList.builder(); ImmutableList.Builder<ModInfo.Mod> mods = ImmutableList.builder();
int modCount = ProtocolUtils.readVarInt(contents); int modCount = ProtocolUtils.readVarInt(contents);
Preconditions.checkArgument(modCount < 1024, "Oversized mods list");
for (int index = 0; index < modCount; index++) { for (int index = 0; index < modCount; index++) {
String id = ProtocolUtils.readString(contents); String id = ProtocolUtils.readString(contents);

View File

@ -63,7 +63,7 @@ public class ServerListPingHandler {
} }
private CompletableFuture<ServerPing> attemptPingPassthrough(VelocityInboundConnection connection, private CompletableFuture<ServerPing> attemptPingPassthrough(VelocityInboundConnection connection,
PingPassthroughMode mode, List<String> servers, ProtocolVersion responseProtocolVersion) { PingPassthroughMode mode, List<String> servers, ProtocolVersion responseProtocolVersion, String virtualHostStr) {
ServerPing fallback = constructLocalPing(connection.getProtocolVersion()); ServerPing fallback = constructLocalPing(connection.getProtocolVersion());
List<CompletableFuture<ServerPing>> pings = new ArrayList<>(); List<CompletableFuture<ServerPing>> pings = new ArrayList<>();
for (String s : servers) { for (String s : servers) {
@ -73,7 +73,7 @@ public class ServerListPingHandler {
} }
VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get(); VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get();
pings.add(vrs.ping(connection.getConnection().eventLoop(), PingOptions.builder() pings.add(vrs.ping(connection.getConnection().eventLoop(), PingOptions.builder()
.version(responseProtocolVersion).build())); .version(responseProtocolVersion).virtualHost(virtualHostStr).build()));
} }
if (pings.isEmpty()) { if (pings.isEmpty()) {
return CompletableFuture.completedFuture(fallback); return CompletableFuture.completedFuture(fallback);
@ -155,7 +155,7 @@ public class ServerListPingHandler {
.orElse(""); .orElse("");
List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault( List<String> serversToTry = server.getConfiguration().getForcedHosts().getOrDefault(
virtualHostStr, server.getConfiguration().getAttemptConnectionOrder()); virtualHostStr, server.getConfiguration().getAttemptConnectionOrder());
return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion); return attemptPingPassthrough(connection, passthroughMode, serversToTry, shownVersion, virtualHostStr);
} }
} }
} }

View File

@ -23,8 +23,6 @@ import com.velocitypowered.proxy.util.except.QuietDecoderException;
import it.unimi.dsi.fastutil.Pair; import it.unimi.dsi.fastutil.Pair;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.Key; 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) { public static String encodeUrlEncoded(byte[] data) {
return MIME_SPECIAL_ENCODER.encodeToString(data); return MIME_SPECIAL_ENCODER.encodeToString(data);
} }
@ -155,22 +117,6 @@ public enum EncryptionUtils {
return Base64.getMimeDecoder().decode(toParse); 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<String, String> 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. * Encodes an RSA key as String cer format.
* *

View File

@ -350,8 +350,9 @@ public class VelocityEventManager implements EventManager {
asyncType = AsyncType.ALWAYS; asyncType = AsyncType.ALWAYS;
} }
// The default value of 0 will fall back to PostOrder, the default PostOrder (NORMAL) is also 0
final short order; final short order;
if (subscribe.order() == PostOrder.CUSTOM) { if (subscribe.priority() != 0) {
order = subscribe.priority(); order = subscribe.priority();
} else { } else {
order = (short) POST_ORDER_MAP.get(subscribe.order()); order = (short) POST_ORDER_MAP.get(subscribe.order());

View File

@ -18,6 +18,8 @@
package com.velocitypowered.proxy.network; package com.velocitypowered.proxy.network;
import com.google.common.base.Preconditions; 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.ListenerBoundEvent;
import com.velocitypowered.api.event.proxy.ListenerCloseEvent; import com.velocitypowered.api.event.proxy.ListenerCloseEvent;
import com.velocitypowered.api.network.ListenerType; 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.Bootstrap;
import io.netty.bootstrap.ServerBootstrap; import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel; import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelOption; import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.unix.UnixChannelOption;
import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.concurrent.MultithreadEventExecutorGroup;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.util.HashMap; import java.util.Collection;
import java.util.Map; import java.util.Map;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 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, private static final WriteBufferWaterMark SERVER_WRITE_MARK = new WriteBufferWaterMark(1 << 20,
1 << 21); 1 << 21);
private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class); private static final Logger LOGGER = LogManager.getLogger(ConnectionManager.class);
private final Map<InetSocketAddress, Endpoint> endpoints = new HashMap<>(); private final Multimap<InetSocketAddress, Endpoint> endpoints = HashMultimap.create();
private final TransportType transportType; private final TransportType transportType;
private final EventLoopGroup bossGroup; private final EventLoopGroup bossGroup;
private final EventLoopGroup workerGroup; private final EventLoopGroup workerGroup;
@ -93,7 +98,6 @@ public final class ConnectionManager {
public void bind(final InetSocketAddress address) { public void bind(final InetSocketAddress address) {
final ServerBootstrap bootstrap = new ServerBootstrap() final ServerBootstrap bootstrap = new ServerBootstrap()
.channelFactory(this.transportType.serverSocketChannelFactory) .channelFactory(this.transportType.serverSocketChannelFactory)
.group(this.bossGroup, this.workerGroup)
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK) .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, SERVER_WRITE_MARK)
.childHandler(this.serverChannelInitializer.get()) .childHandler(this.serverChannelInitializer.get())
.childOption(ChannelOption.TCP_NODELAY, true) .childOption(ChannelOption.TCP_NODELAY, true)
@ -104,26 +108,50 @@ public final class ConnectionManager {
bootstrap.option(ChannelOption.TCP_FASTOPEN, 3); bootstrap.option(ChannelOption.TCP_FASTOPEN, 3);
} }
bootstrap.bind() 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 -> { .addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel(); final Channel channel = future.channel();
if (future.isSuccess()) { if (future.isSuccess()) {
this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT)); 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 // Warn people with console access that HAProxy is in use, see PR: #1436
if (this.server.getConfiguration().isProxyProtocol()) { if (this.server.getConfiguration().isProxyProtocol()) {
LOGGER.warn("Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", channel.localAddress()); LOGGER.warn(
"Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.",
channel.localAddress());
} }
LOGGER.info("Listening on {}", channel.localAddress());
// Fire the proxy bound event after the socket is bound // Fire the proxy bound event after the socket is bound
server.getEventManager().fireAndForget( server.getEventManager().fireAndForget(
new ListenerBoundEvent(address, ListenerType.MINECRAFT)); new ListenerBoundEvent(address, ListenerType.MINECRAFT));
}
} else { } else {
LOGGER.error("Can't bind to {}", address, future.cause()); LOGGER.error("Can't bind to {}", address, future.cause());
} }
}); });
f.syncUninterruptibly();
if (!f.isSuccess()) {
break;
}
}
} }
/** /**
@ -181,18 +209,21 @@ public final class ConnectionManager {
* @param oldBind the endpoint to close * @param oldBind the endpoint to close
*/ */
public void close(InetSocketAddress oldBind) { public void close(InetSocketAddress oldBind) {
Endpoint endpoint = endpoints.remove(oldBind); Collection<Endpoint> 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 // 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. // 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();
for (Endpoint endpoint : endpoints) {
Channel serverChannel = endpoint.getChannel(); Channel serverChannel = endpoint.getChannel();
Preconditions.checkState(serverChannel != null, "Endpoint %s not registered", oldBind);
LOGGER.info("Closing endpoint {}", serverChannel.localAddress()); LOGGER.info("Closing endpoint {}", serverChannel.localAddress());
serverChannel.close().syncUninterruptibly(); serverChannel.close().syncUninterruptibly();
} }
}
/** /**
* Closes all the currently registered endpoints. * Closes all the currently registered endpoints.
@ -200,14 +231,17 @@ public final class ConnectionManager {
* @param interrupt should closing forward interruptions * @param interrupt should closing forward interruptions
*/ */
public void closeEndpoints(boolean interrupt) { public void closeEndpoints(boolean interrupt) {
for (final Map.Entry<InetSocketAddress, Endpoint> entry : this.endpoints.entrySet()) { for (final Map.Entry<InetSocketAddress, Collection<Endpoint>> entry : this.endpoints.asMap()
.entrySet()) {
final InetSocketAddress address = entry.getKey(); final InetSocketAddress address = entry.getKey();
final Endpoint endpoint = entry.getValue(); final Collection<Endpoint> endpoints = entry.getValue();
ListenerType type = endpoints.iterator().next().getType();
// Fire proxy close event to notify plugins of socket close. We block since plugins // 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. // 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();
for (Endpoint endpoint : endpoints) {
LOGGER.info("Closing endpoint {}", address); LOGGER.info("Closing endpoint {}", address);
if (interrupt) { if (interrupt) {
try { try {
@ -220,6 +254,7 @@ public final class ConnectionManager {
endpoint.getChannel().close().syncUninterruptibly(); endpoint.getChannel().close().syncUninterruptibly();
} }
} }
}
this.endpoints.clear(); this.endpoints.clear();
} }

View File

@ -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;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2; 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_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_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
@ -377,7 +378,8 @@ public enum StateRegistry {
map(0x0D, MINECRAFT_1_17, false), map(0x0D, MINECRAFT_1_17, false),
map(0x0A, MINECRAFT_1_19, false), map(0x0A, MINECRAFT_1_19, false),
map(0x0B, MINECRAFT_1_19_4, 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( clientbound.register(
LegacyChatPacket.class, LegacyChatPacket.class,
LegacyChatPacket::new, LegacyChatPacket::new,
@ -398,7 +400,8 @@ public enum StateRegistry {
map(0x0E, MINECRAFT_1_19, false), map(0x0E, MINECRAFT_1_19, false),
map(0x0D, MINECRAFT_1_19_3, false), map(0x0D, MINECRAFT_1_19_3, false),
map(0x0F, MINECRAFT_1_19_4, 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( clientbound.register(
AvailableCommandsPacket.class, AvailableCommandsPacket.class,
AvailableCommandsPacket::new, AvailableCommandsPacket::new,
@ -410,10 +413,12 @@ public enum StateRegistry {
map(0x0F, MINECRAFT_1_19, false), map(0x0F, MINECRAFT_1_19, false),
map(0x0E, MINECRAFT_1_19_3, false), map(0x0E, MINECRAFT_1_19_3, false),
map(0x10, MINECRAFT_1_19_4, 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( clientbound.register(
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, 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( clientbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@ -430,7 +435,8 @@ public enum StateRegistry {
map(0x15, MINECRAFT_1_19_3, false), map(0x15, MINECRAFT_1_19_3, false),
map(0x17, MINECRAFT_1_19_4, false), map(0x17, MINECRAFT_1_19_4, false),
map(0x18, MINECRAFT_1_20_2, 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( clientbound.register(
DisconnectPacket.class, DisconnectPacket.class,
() -> new DisconnectPacket(this), () -> new DisconnectPacket(this),
@ -447,7 +453,8 @@ public enum StateRegistry {
map(0x17, MINECRAFT_1_19_3, false), map(0x17, MINECRAFT_1_19_3, false),
map(0x1A, MINECRAFT_1_19_4, false), map(0x1A, MINECRAFT_1_19_4, false),
map(0x1B, MINECRAFT_1_20_2, 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( clientbound.register(
KeepAlivePacket.class, KeepAlivePacket.class,
KeepAlivePacket::new, KeepAlivePacket::new,
@ -465,7 +472,8 @@ public enum StateRegistry {
map(0x23, MINECRAFT_1_19_4, false), map(0x23, MINECRAFT_1_19_4, false),
map(0x24, MINECRAFT_1_20_2, false), map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, 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( clientbound.register(
JoinGamePacket.class, JoinGamePacket.class,
JoinGamePacket::new, JoinGamePacket::new,
@ -483,7 +491,8 @@ public enum StateRegistry {
map(0x28, MINECRAFT_1_19_4, false), map(0x28, MINECRAFT_1_19_4, false),
map(0x29, MINECRAFT_1_20_2, false), map(0x29, MINECRAFT_1_20_2, false),
map(0x2B, MINECRAFT_1_20_5, 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( clientbound.register(
RespawnPacket.class, RespawnPacket.class,
RespawnPacket::new, RespawnPacket::new,
@ -504,13 +513,15 @@ public enum StateRegistry {
map(0x43, MINECRAFT_1_20_2, true), map(0x43, MINECRAFT_1_20_2, true),
map(0x45, MINECRAFT_1_20_3, true), map(0x45, MINECRAFT_1_20_3, true),
map(0x47, MINECRAFT_1_20_5, 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( clientbound.register(
RemoveResourcePackPacket.class, RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new, RemoveResourcePackPacket::new,
map(0x43, MINECRAFT_1_20_3, false), map(0x43, MINECRAFT_1_20_3, false),
map(0x45, MINECRAFT_1_20_5, 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( clientbound.register(
ResourcePackRequestPacket.class, ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new, ResourcePackRequestPacket::new,
@ -531,7 +542,8 @@ public enum StateRegistry {
map(0x42, MINECRAFT_1_20_2, false), map(0x42, MINECRAFT_1_20_2, false),
map(0x44, MINECRAFT_1_20_3, false), map(0x44, MINECRAFT_1_20_3, false),
map(0x46, MINECRAFT_1_20_5, 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( clientbound.register(
HeaderAndFooterPacket.class, HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new, HeaderAndFooterPacket::new,
@ -553,7 +565,8 @@ public enum StateRegistry {
map(0x68, MINECRAFT_1_20_2, true), map(0x68, MINECRAFT_1_20_2, true),
map(0x6A, MINECRAFT_1_20_3, true), map(0x6A, MINECRAFT_1_20_3, true),
map(0x6D, MINECRAFT_1_20_5, 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( clientbound.register(
LegacyTitlePacket.class, LegacyTitlePacket.class,
LegacyTitlePacket::new, LegacyTitlePacket::new,
@ -574,7 +587,8 @@ public enum StateRegistry {
map(0x5F, MINECRAFT_1_20_2, true), map(0x5F, MINECRAFT_1_20_2, true),
map(0x61, MINECRAFT_1_20_3, true), map(0x61, MINECRAFT_1_20_3, true),
map(0x63, MINECRAFT_1_20_5, 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( clientbound.register(
TitleTextPacket.class, TitleTextPacket.class,
TitleTextPacket::new, TitleTextPacket::new,
@ -586,7 +600,8 @@ public enum StateRegistry {
map(0x61, MINECRAFT_1_20_2, true), map(0x61, MINECRAFT_1_20_2, true),
map(0x63, MINECRAFT_1_20_3, true), map(0x63, MINECRAFT_1_20_3, true),
map(0x65, MINECRAFT_1_20_5, 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( clientbound.register(
TitleActionbarPacket.class, TitleActionbarPacket.class,
TitleActionbarPacket::new, TitleActionbarPacket::new,
@ -598,7 +613,8 @@ public enum StateRegistry {
map(0x48, MINECRAFT_1_20_2, true), map(0x48, MINECRAFT_1_20_2, true),
map(0x4A, MINECRAFT_1_20_3, true), map(0x4A, MINECRAFT_1_20_3, true),
map(0x4C, MINECRAFT_1_20_5, 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( clientbound.register(
TitleTimesPacket.class, TitleTimesPacket.class,
TitleTimesPacket::new, TitleTimesPacket::new,
@ -610,7 +626,8 @@ public enum StateRegistry {
map(0x62, MINECRAFT_1_20_2, true), map(0x62, MINECRAFT_1_20_2, true),
map(0x64, MINECRAFT_1_20_3, true), map(0x64, MINECRAFT_1_20_3, true),
map(0x66, MINECRAFT_1_20_5, 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( clientbound.register(
TitleClearPacket.class, TitleClearPacket.class,
TitleClearPacket::new, TitleClearPacket::new,
@ -618,7 +635,8 @@ public enum StateRegistry {
map(0x0D, MINECRAFT_1_19, true), map(0x0D, MINECRAFT_1_19, true),
map(0x0C, MINECRAFT_1_19_3, true), map(0x0C, MINECRAFT_1_19_3, true),
map(0x0E, MINECRAFT_1_19_4, 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( clientbound.register(
LegacyPlayerListItemPacket.class, LegacyPlayerListItemPacket.class,
LegacyPlayerListItemPacket::new, LegacyPlayerListItemPacket::new,
@ -638,7 +656,8 @@ public enum StateRegistry {
map(0x39, MINECRAFT_1_19_4, false), map(0x39, MINECRAFT_1_19_4, false),
map(0x3B, MINECRAFT_1_20_2, false), map(0x3B, MINECRAFT_1_20_2, false),
map(0x3D, MINECRAFT_1_20_5, 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( clientbound.register(
UpsertPlayerInfoPacket.class, UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new, UpsertPlayerInfoPacket::new,
@ -646,11 +665,13 @@ public enum StateRegistry {
map(0x3A, MINECRAFT_1_19_4, false), map(0x3A, MINECRAFT_1_19_4, false),
map(0x3C, MINECRAFT_1_20_2, false), map(0x3C, MINECRAFT_1_20_2, false),
map(0x3E, MINECRAFT_1_20_5, 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( clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false), 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( clientbound.register(
SystemChatPacket.class, SystemChatPacket.class,
SystemChatPacket::new, SystemChatPacket::new,
@ -661,7 +682,8 @@ public enum StateRegistry {
map(0x67, MINECRAFT_1_20_2, true), map(0x67, MINECRAFT_1_20_2, true),
map(0x69, MINECRAFT_1_20_3, true), map(0x69, MINECRAFT_1_20_3, true),
map(0x6C, MINECRAFT_1_20_5, 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( clientbound.register(
PlayerChatCompletionPacket.class, PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new, PlayerChatCompletionPacket::new,
@ -669,7 +691,8 @@ public enum StateRegistry {
map(0x14, MINECRAFT_1_19_3, true), map(0x14, MINECRAFT_1_19_3, true),
map(0x16, MINECRAFT_1_19_4, true), map(0x16, MINECRAFT_1_19_4, true),
map(0x17, MINECRAFT_1_20_2, 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( clientbound.register(
ServerDataPacket.class, ServerDataPacket.class,
ServerDataPacket::new, ServerDataPacket::new,
@ -680,14 +703,16 @@ public enum StateRegistry {
map(0x47, MINECRAFT_1_20_2, false), map(0x47, MINECRAFT_1_20_2, false),
map(0x49, MINECRAFT_1_20_3, false), map(0x49, MINECRAFT_1_20_3, false),
map(0x4B, MINECRAFT_1_20_5, 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( clientbound.register(
StartUpdatePacket.class, StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE, () -> StartUpdatePacket.INSTANCE,
map(0x65, MINECRAFT_1_20_2, false), map(0x65, MINECRAFT_1_20_2, false),
map(0x67, MINECRAFT_1_20_3, false), map(0x67, MINECRAFT_1_20_3, false),
map(0x69, MINECRAFT_1_20_5, 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( clientbound.register(
BundleDelimiterPacket.class, BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE, () -> BundleDelimiterPacket.INSTANCE,

View File

@ -52,6 +52,9 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int claimedUncompressedSize = ProtocolUtils.readVarInt(in); int claimedUncompressedSize = ProtocolUtils.readVarInt(in);
if (claimedUncompressedSize == 0) { if (claimedUncompressedSize == 0) {
int actualUncompressedSize = in.readableBytes();
checkFrame(actualUncompressedSize < threshold, "Actual uncompressed size %s is greater than"
+ " threshold %s", actualUncompressedSize, threshold);
// This message is not compressed. // This message is not compressed.
out.add(in.retain()); out.add(in.retain());
return; return;

View File

@ -58,6 +58,13 @@ public class ArgumentIdentifier {
this.versionById = ImmutableMap.copyOf(temp); this.versionById = ImmutableMap.copyOf(temp);
} }
@Override
public String toString() {
return "ArgumentIdentifier{" +
"identifier='" + identifier + '\'' +
'}';
}
public String getIdentifier() { public String getIdentifier() {
return identifier; return identifier;
} }

View File

@ -22,6 +22,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.id;
import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet; import static com.velocitypowered.proxy.protocol.packet.brigadier.ArgumentIdentifier.mapSet;
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE; import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
@ -254,17 +255,20 @@ public class ArgumentPropertyRegistry {
register(id("minecraft:resource_key", mapSet(MINECRAFT_1_20_5, 46), mapSet(MINECRAFT_1_20_3, 45), mapSet(MINECRAFT_1_19_3, 44)), register(id("minecraft:resource_key", mapSet(MINECRAFT_1_20_5, 46), mapSet(MINECRAFT_1_20_3, 45), mapSet(MINECRAFT_1_19_3, 44)),
RegistryKeyArgumentList.ResourceKey.class, RegistryKeyArgumentList.ResourceKey.class,
RegistryKeyArgumentList.ResourceKey.Serializer.REGISTRY); RegistryKeyArgumentList.ResourceKey.Serializer.REGISTRY);
register(id("minecraft:resource_selector", mapSet(MINECRAFT_1_21_5, 47)),
RegistryKeyArgumentList.ResourceSelector.class,
RegistryKeyArgumentList.ResourceSelector.Serializer.REGISTRY);
empty(id("minecraft:template_mirror", mapSet(MINECRAFT_1_20_5, 47), mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19 empty(id("minecraft:template_mirror", mapSet(MINECRAFT_1_21_5, 48), mapSet(MINECRAFT_1_20_5, 47), mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19
empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_20_5, 48), mapSet(MINECRAFT_1_20_3, 47), mapSet(MINECRAFT_1_19, 46))); // 1.19 empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_21_5, 49), mapSet(MINECRAFT_1_20_5, 48), mapSet(MINECRAFT_1_20_3, 47), mapSet(MINECRAFT_1_19, 46))); // 1.19
empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_20_3, 49), mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4 empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_21_5, 50), mapSet(MINECRAFT_1_20_3, 49), mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4
empty(id("minecraft:uuid", mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48), mapSet(MINECRAFT_1_19_4, 48), empty(id("minecraft:uuid", mapSet(MINECRAFT_1_21_5, 54),mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48), mapSet(MINECRAFT_1_19_4, 48),
mapSet(MINECRAFT_1_19, 47))); // added in 1.16 mapSet(MINECRAFT_1_19, 47))); // added in 1.16
empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_20_5, 50))); empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_21_5, 51), mapSet(MINECRAFT_1_20_5, 50)));
empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_20_5, 51))); empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_21_5, 52), mapSet(MINECRAFT_1_20_5, 51)));
empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_20_5, 52))); empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_21_5, 53), mapSet(MINECRAFT_1_20_5, 52)));
// Crossstitch support // Crossstitch support
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD); register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);

View File

@ -67,23 +67,23 @@ public final class RegistryKeyArgumentList {
} }
} }
public static class Resource extends RegistryKeyArgument { public static class ResourceSelector extends RegistryKeyArgument {
public Resource(String identifier) { public ResourceSelector(String identifier) {
super(identifier); super(identifier);
} }
public static class Serializer implements ArgumentPropertySerializer<Resource> { public static class Serializer implements ArgumentPropertySerializer<ResourceSelector> {
static final Resource.Serializer REGISTRY = new Resource.Serializer(); static final ResourceSelector.Serializer REGISTRY = new ResourceSelector.Serializer();
@Override @Override
public Resource deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { public ResourceSelector deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
return new Resource(ProtocolUtils.readString(buf)); return new ResourceSelector(ProtocolUtils.readString(buf));
} }
@Override @Override
public void serialize(Resource object, ByteBuf buf, ProtocolVersion protocolVersion) { public void serialize(ResourceSelector object, ByteBuf buf, ProtocolVersion protocolVersion) {
ProtocolUtils.writeString(buf, object.getIdentifier()); ProtocolUtils.writeString(buf, object.getIdentifier());
} }
} }

View File

@ -32,13 +32,15 @@ import java.util.function.Function;
* A precisely ordered queue which allows for outside entries into the ordered queue through * A precisely ordered queue which allows for outside entries into the ordered queue through
* piggybacking timestamps. * piggybacking timestamps.
*/ */
public class ChatQueue { public class ChatQueue implements AutoCloseable {
private final Object internalLock = new Object(); private final Object internalLock = new Object();
private final ConnectedPlayer player; private final ConnectedPlayer player;
private final ChatState chatState = new ChatState(); private final ChatState chatState = new ChatState();
private CompletableFuture<Void> head = CompletableFuture.completedFuture(null); private CompletableFuture<Void> head = CompletableFuture.completedFuture(null);
private volatile boolean closed;
/** /**
* Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}. * Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}.
* *
@ -50,8 +52,14 @@ public class ChatQueue {
private void queueTask(Task task) { private void queueTask(Task task) {
synchronized (internalLock) { synchronized (internalLock) {
if (closed) {
throw new IllegalStateException("ChatQueue has already been closed");
}
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected(); MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
head = head.thenCompose(v -> { head = head.thenCompose(v -> {
if (closed) {
return CompletableFuture.completedFuture(null);
}
try { try {
return task.update(chatState, smc).exceptionally(ignored -> null); return task.update(chatState, smc).exceptionally(ignored -> null);
} catch (Throwable ignored) { } catch (Throwable ignored) {
@ -102,9 +110,9 @@ public class ChatQueue {
}); });
} }
private static <T extends MinecraftPacket> CompletableFuture<Void> writePacket(T packet, MinecraftConnection smc) { private <T extends MinecraftPacket> CompletableFuture<Void> writePacket(T packet, MinecraftConnection smc) {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
if (!smc.isClosed()) { if (!closed && !smc.isClosed()) {
ChannelFuture future = smc.write(packet); ChannelFuture future = smc.write(packet);
if (future != null) { if (future != null) {
future.awaitUninterruptibly(); future.awaitUninterruptibly();
@ -113,6 +121,11 @@ public class ChatQueue {
}, smc.eventLoop()); }, smc.eventLoop());
} }
@Override
public void close() {
closed = true;
}
private interface Task { private interface Task {
CompletableFuture<Void> update(ChatState chatState, MinecraftConnection smc); CompletableFuture<Void> update(ChatState chatState, MinecraftConnection smc);
} }
@ -174,7 +187,7 @@ public class ChatQueue {
} }
public LastSeenMessages createLastSeen() { public LastSeenMessages createLastSeen() {
return new LastSeenMessages(0, lastSeenMessages); return new LastSeenMessages(0, lastSeenMessages, (byte) 0);
} }
} }
} }

View File

@ -43,7 +43,6 @@ import net.kyori.adventure.nbt.LongBinaryTag;
import net.kyori.adventure.nbt.ShortBinaryTag; import net.kyori.adventure.nbt.ShortBinaryTag;
import net.kyori.adventure.nbt.StringBinaryTag; import net.kyori.adventure.nbt.StringBinaryTag;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -106,15 +105,13 @@ public class ComponentHolder {
public BinaryTag getBinaryTag() { public BinaryTag getBinaryTag() {
if (binaryTag == null) { if (binaryTag == null) {
// TODO: replace this with adventure-text-serializer-nbt // TODO: replace this with adventure-text-serializer-nbt
binaryTag = serialize(GsonComponentSerializer.gson().serializeToTree(getComponent())); binaryTag = serialize(ProtocolUtils.getJsonChatSerializer(version).serializeToTree(getComponent()));
} }
return binaryTag; return binaryTag;
} }
public static BinaryTag serialize(JsonElement json) { public static BinaryTag serialize(JsonElement json) {
if (json instanceof JsonPrimitive) { if (json instanceof JsonPrimitive jsonPrimitive) {
JsonPrimitive jsonPrimitive = (JsonPrimitive) json;
if (jsonPrimitive.isNumber()) { if (jsonPrimitive.isNumber()) {
Number number = json.getAsNumber(); Number number = json.getAsNumber();

View File

@ -17,6 +17,7 @@
package com.velocitypowered.proxy.protocol.packet.chat; package com.velocitypowered.proxy.protocol.packet.chat;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import java.util.Arrays; import java.util.Arrays;
@ -26,30 +27,38 @@ public class LastSeenMessages {
public static final int WINDOW_SIZE = 20; public static final int WINDOW_SIZE = 20;
private static final int DIV_FLOOR = -Math.floorDiv(-WINDOW_SIZE, 8); private static final int DIV_FLOOR = -Math.floorDiv(-WINDOW_SIZE, 8);
private int offset; private final int offset;
private BitSet acknowledged; private final BitSet acknowledged;
private byte checksum;
public LastSeenMessages() { public LastSeenMessages() {
this.offset = 0; this(0, new BitSet(), (byte) 0);
this.acknowledged = new BitSet();
} }
public LastSeenMessages(int offset, BitSet acknowledged) { public LastSeenMessages(int offset, BitSet acknowledged, byte checksum) {
this.offset = offset; this.offset = offset;
this.acknowledged = acknowledged; this.acknowledged = acknowledged;
this.checksum = checksum;
} }
public LastSeenMessages(ByteBuf buf) { public LastSeenMessages(ByteBuf buf, ProtocolVersion protocolVersion) {
this.offset = ProtocolUtils.readVarInt(buf); this.offset = ProtocolUtils.readVarInt(buf);
byte[] bytes = new byte[DIV_FLOOR]; byte[] bytes = new byte[DIV_FLOOR];
buf.readBytes(bytes); buf.readBytes(bytes);
this.acknowledged = BitSet.valueOf(bytes); this.acknowledged = BitSet.valueOf(bytes);
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_21_5)) {
this.checksum = buf.readByte();
}
} }
public void encode(ByteBuf buf) { public void encode(ByteBuf buf, ProtocolVersion protocolVersion) {
ProtocolUtils.writeVarInt(buf, offset); ProtocolUtils.writeVarInt(buf, offset);
buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR)); buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR));
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_21_5)) {
buf.writeByte(this.checksum);
}
} }
public int getOffset() { public int getOffset() {
@ -61,7 +70,7 @@ public class LastSeenMessages {
} }
public LastSeenMessages offset(final int offset) { public LastSeenMessages offset(final int offset) {
return new LastSeenMessages(this.offset + offset, acknowledged); return new LastSeenMessages(this.offset + offset, acknowledged, checksum);
} }
@Override @Override
@ -69,6 +78,7 @@ public class LastSeenMessages {
return "LastSeenMessages{" + return "LastSeenMessages{" +
"offset=" + offset + "offset=" + offset +
", acknowledged=" + acknowledged + ", acknowledged=" + acknowledged +
", checksum=" + checksum +
'}'; '}';
} }
} }

View File

@ -91,7 +91,8 @@ public class LegacyChatPacket implements MinecraftPacket {
@Override @Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
message = ProtocolUtils.readString(buf); message = ProtocolUtils.readString(buf, direction == ProtocolUtils.Direction.CLIENTBOUND
? 262144 : version.noLessThan(ProtocolVersion.MINECRAFT_1_11) ? 256 : 100);
if (direction == ProtocolUtils.Direction.CLIENTBOUND if (direction == ProtocolUtils.Direction.CLIENTBOUND
&& version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) { && version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
type = buf.readByte(); type = buf.readByte();

View File

@ -74,7 +74,7 @@ public class SessionPlayerChatPacket implements MinecraftPacket {
} else { } else {
this.signature = new byte[0]; this.signature = new byte[0];
} }
this.lastSeenMessages = new LastSeenMessages(buf); this.lastSeenMessages = new LastSeenMessages(buf, protocolVersion);
} }
@Override @Override
@ -87,7 +87,7 @@ public class SessionPlayerChatPacket implements MinecraftPacket {
if (this.signed) { if (this.signed) {
buf.writeBytes(this.signature); buf.writeBytes(this.signature);
} }
this.lastSeenMessages.encode(buf); this.lastSeenMessages.encode(buf, protocolVersion);
} }
@Override @Override

View File

@ -45,7 +45,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket {
this.timeStamp = Instant.ofEpochMilli(buf.readLong()); this.timeStamp = Instant.ofEpochMilli(buf.readLong());
this.salt = buf.readLong(); this.salt = buf.readLong();
this.argumentSignatures = new ArgumentSignatures(buf); this.argumentSignatures = new ArgumentSignatures(buf);
this.lastSeenMessages = new LastSeenMessages(buf); this.lastSeenMessages = new LastSeenMessages(buf, protocolVersion);
this.argumentSignatures = new ArgumentSignatures(); this.argumentSignatures = new ArgumentSignatures();
} }
@ -56,7 +56,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket {
buf.writeLong(this.timeStamp.toEpochMilli()); buf.writeLong(this.timeStamp.toEpochMilli());
buf.writeLong(this.salt); buf.writeLong(this.salt);
this.argumentSignatures.encode(buf); this.argumentSignatures.encode(buf);
this.lastSeenMessages.encode(buf); this.lastSeenMessages.encode(buf, protocolVersion);
} }
public String getCommand() { public String getCommand() {

View File

@ -68,12 +68,6 @@ public class ClientboundServerLinksPacket implements MinecraftPacket {
public record ServerLink(int id, ComponentHolder displayName, String url) { public record ServerLink(int id, ComponentHolder displayName, String url) {
public ServerLink(com.velocitypowered.api.util.ServerLink link, ProtocolVersion protocolVersion) {
this(link.getBuiltInType().map(Enum::ordinal).orElse(-1),
link.getCustomLabel().map(c -> new ComponentHolder(protocolVersion, c)).orElse(null),
link.getUrl().toString());
}
private static ServerLink read(ByteBuf buf, ProtocolVersion version) { private static ServerLink read(ByteBuf buf, ProtocolVersion version) {
if (buf.readBoolean()) { if (buf.readBoolean()) {
return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf)); return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf));

View File

@ -43,20 +43,23 @@ public class PingSessionHandler implements MinecraftSessionHandler {
private final MinecraftConnection connection; private final MinecraftConnection connection;
private final ProtocolVersion version; private final ProtocolVersion version;
private boolean completed = false; private boolean completed = false;
private final String virtualHostString;
PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server, PingSessionHandler(CompletableFuture<ServerPing> result, RegisteredServer server,
MinecraftConnection connection, ProtocolVersion version) { MinecraftConnection connection, ProtocolVersion version, String virtualHostString) {
this.result = result; this.result = result;
this.server = server; this.server = server;
this.connection = connection; this.connection = connection;
this.version = version; this.version = version;
this.virtualHostString = virtualHostString;
} }
@Override @Override
public void activated() { public void activated() {
HandshakePacket handshake = new HandshakePacket(); HandshakePacket handshake = new HandshakePacket();
handshake.setIntent(HandshakeIntent.STATUS); handshake.setIntent(HandshakeIntent.STATUS);
handshake.setServerAddress(server.getServerInfo().getAddress().getHostString()); handshake.setServerAddress(this.virtualHostString == null || this.virtualHostString.isEmpty()
? server.getServerInfo().getAddress().getHostString() : this.virtualHostString);
handshake.setPort(server.getServerInfo().getAddress().getPort()); handshake.setPort(server.getServerInfo().getAddress().getPort());
handshake.setProtocolVersion(version); handshake.setProtocolVersion(version);
connection.delayedWrite(handshake); connection.delayedWrite(handshake);

View File

@ -129,7 +129,7 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud
if (future.isSuccess()) { if (future.isSuccess()) {
MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class);
PingSessionHandler handler = new PingSessionHandler(pingFuture, PingSessionHandler handler = new PingSessionHandler(pingFuture,
VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion()); VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion(), pingOptions.getVirtualHost());
conn.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler); conn.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler);
} else { } else {
pingFuture.completeExceptionally(future.cause()); pingFuture.completeExceptionally(future.cause());

View File

@ -166,7 +166,7 @@ public class KeyedVelocityTabList implements InternalTabList {
@Override @Override
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, public 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) {
return buildEntry(profile, displayName, latency, gameMode, chatSession, listed); return buildEntry(profile, displayName, latency, gameMode, chatSession, listed);
} }

View File

@ -90,7 +90,7 @@ public class VelocityTabList implements InternalTabList {
} else { } else {
entry = new VelocityTabListEntry(this, entry1.getProfile(), entry = new VelocityTabListEntry(this, entry1.getProfile(),
entry1.getDisplayNameComponent().orElse(null), entry1.getDisplayNameComponent().orElse(null),
entry1.getLatency(), entry1.getGameMode(), entry1.getChatSession(), entry1.isListed(), entry1.getListOrder()); entry1.getLatency(), entry1.getGameMode(), entry1.getChatSession(), entry1.isListed(), entry1.getListOrder(), entry1.isShowHat());
} }
EnumSet<UpsertPlayerInfoPacket.Action> actions = EnumSet EnumSet<UpsertPlayerInfoPacket.Action> actions = EnumSet
@ -134,6 +134,11 @@ public class VelocityTabList implements InternalTabList {
actions.add(UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER); actions.add(UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER);
playerInfoEntry.setListOrder(entry.getListOrder()); playerInfoEntry.setListOrder(entry.getListOrder());
} }
if (!Objects.equals(previousEntry.isShowHat(), entry.isShowHat())
&& player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_4)) {
actions.add(UpsertPlayerInfoPacket.Action.UPDATE_HAT);
playerInfoEntry.setShowHat(entry.isShowHat());
}
if (!Objects.equals(previousEntry.getChatSession(), entry.getChatSession())) { if (!Objects.equals(previousEntry.getChatSession(), entry.getChatSession())) {
ChatSession from = entry.getChatSession(); ChatSession from = entry.getChatSession();
if (from != null) { if (from != null) {
@ -173,6 +178,10 @@ public class VelocityTabList implements InternalTabList {
actions.add(UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER); actions.add(UpsertPlayerInfoPacket.Action.UPDATE_LIST_ORDER);
playerInfoEntry.setListOrder(entry.getListOrder()); playerInfoEntry.setListOrder(entry.getListOrder());
} }
if (!entry.isShowHat() && player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_4)) {
actions.add(UpsertPlayerInfoPacket.Action.UPDATE_HAT);
playerInfoEntry.setShowHat(entry.isShowHat());
}
} }
return entry; return entry;
}); });
@ -218,9 +227,9 @@ public class VelocityTabList implements InternalTabList {
@Override @Override
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode, int gameMode,
@Nullable ChatSession chatSession, boolean listed, int listOrder) { @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) {
return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, chatSession, return new VelocityTabListEntry(this, profile, displayName, latency, gameMode, chatSession,
listed, listOrder); listed, listOrder, showHat);
} }
@Override @Override
@ -258,7 +267,8 @@ public class VelocityTabList implements InternalTabList {
-1, -1,
null, null,
false, false,
0 0,
true
) )
); );
} else { } else {

View File

@ -40,6 +40,7 @@ public class VelocityTabListEntry implements TabListEntry {
private int gameMode; private int gameMode;
private boolean listed; private boolean listed;
private int listOrder; private int listOrder;
private boolean showHat;
private @Nullable ChatSession session; private @Nullable ChatSession session;
/** /**
@ -47,7 +48,7 @@ public class VelocityTabListEntry implements TabListEntry {
*/ */
public VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, Component displayName, public VelocityTabListEntry(VelocityTabList tabList, GameProfile profile, Component displayName,
int latency, int latency,
int gameMode, @Nullable ChatSession session, boolean listed, int listOrder) { int gameMode, @Nullable ChatSession session, boolean listed, int listOrder, boolean showHat) {
this.tabList = tabList; this.tabList = tabList;
this.profile = profile; this.profile = profile;
this.displayName = displayName; this.displayName = displayName;
@ -56,6 +57,7 @@ public class VelocityTabListEntry implements TabListEntry {
this.session = session; this.session = session;
this.listed = listed; this.listed = listed;
this.listOrder = listOrder; this.listOrder = listOrder;
this.showHat = showHat;
} }
@Override @Override
@ -173,4 +175,24 @@ public class VelocityTabListEntry implements TabListEntry {
void setListOrderWithoutUpdate(int listOrder) { void setListOrderWithoutUpdate(int listOrder) {
this.listOrder = listOrder; this.listOrder = listOrder;
} }
@Override
public boolean isShowHat() {
return showHat;
}
@Override
public VelocityTabListEntry setShowHat(boolean showHat) {
this.showHat = showHat;
if (tabList.getPlayer().getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21_4)) {
UpsertPlayerInfoPacket.Entry upsertEntry = this.tabList.createRawEntry(this);
upsertEntry.setShowHat(showHat);
tabList.emitActionRaw(UpsertPlayerInfoPacket.Action.UPDATE_HAT, upsertEntry);
}
return this;
}
void setShowHatWithoutUpdate(boolean showHat) {
this.showHat = showHat;
}
} }

View File

@ -35,6 +35,8 @@ public class VelocityTabListEntryLegacy extends KeyedVelocityTabListEntry {
@Override @Override
public TabListEntry setDisplayName(@Nullable Component displayName) { public TabListEntry setDisplayName(@Nullable Component displayName) {
getTabList().removeEntry(getProfile().getId()); // We have to remove first if updating getTabList().removeEntry(getProfile().getId()); // We have to remove first if updating
return super.setDisplayName(displayName); setDisplayNameInternal(displayName);
getTabList().addEntry(this);
return this;
} }
} }

View File

@ -19,6 +19,8 @@ package com.velocitypowered.proxy.tablist;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.api.proxy.player.ChatSession;
import com.velocitypowered.api.proxy.player.TabListEntry; import com.velocitypowered.api.proxy.player.TabListEntry;
import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.GameProfile;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
@ -133,9 +135,22 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList {
} }
} }
@Override
public TabListEntry buildEntry(GameProfile profile,
net.kyori.adventure.text.@Nullable Component displayName,
int latency, int gameMode, @Nullable IdentifiedKey key) {
return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode);
}
@Override @Override
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode) { int gameMode, @Nullable ChatSession chatSession, boolean listed) {
return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode);
}
@Override
public TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder, boolean showHat) {
return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode); return new VelocityTabListEntryLegacy(this, profile, displayName, latency, gameMode);
} }
} }

View File

@ -145,6 +145,12 @@ log-player-connections = true
# Transfer packet (Minecraft 1.20.5) to be received. # Transfer packet (Minecraft 1.20.5) to be received.
accepts-transfers = false accepts-transfers = false
# Enables support for SO_REUSEPORT. This may help the proxy scale better on multicore systems
# with a lot of incoming connections, and provide better CPU utilization than the existing
# strategy of having a single thread accepting connections and distributing them to worker
# threads. Disabled by default. Requires Linux or macOS.
enable-reuse-port = false
[query] [query]
# Whether to enable responding to GameSpy 4 query responses or not. # Whether to enable responding to GameSpy 4 query responses or not.
enabled = false enabled = false

View File

@ -17,7 +17,7 @@ pluginManagement {
} }
plugins { plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
} }
rootProject.name = "velocity" rootProject.name = "velocity"