Compare commits

...

71 Commits

Author SHA1 Message Date
ff50d6eabf docs: 完善配置文档 2025-07-30 16:51:02 +08:00
0f52dbaaf8 feat: 魔改网易登录接口
Some checks failed
Java CI with Gradle / build (push) Failing after 2m28s
2025-07-24 13:32:18 +08:00
Pantera (Mad_Daniel)
e99407132f Add version information for 1.21.8 (#1612) 2025-07-18 03:26:24 +01:00
Shane Freeder
81deb1fff8 Update maven publishing repo name 2025-06-30 15:22:39 +01:00
Jones
59560ebad1 1.21.7 Support (#1598)
* Support 1.21.7 RC 1

* Use snapshot protocol for RC 1

* Support 1.21.7 RC 2

* Set release protocol for 1.21.7

* Update api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java

---------

Co-authored-by: Nassim Jahnke <nassim@njahnke.dev>
2025-06-30 16:09:39 +02:00
Limbo
67a6600c05 New Crowdin updates (#1283)
* New translations messages.properties (Bulgarian)

* New translations messages.properties (Spanish)
2025-06-29 23:31:24 -07:00
Christoph Loy
f3e30558e4 Gradle deprecation fixes & upgrades (#1594)
* Fix Gradle deprecations

By using test suites, we explicitely configure the relevant dependencies
on the test sourceset. This is not done by merely configuring the test task.

* Switch to maintained version of Shadow

* Update to Gradle 8.14.2
2025-06-28 16:28:29 -07:00
Riley Park
e46ab6ad7d build: publish using fill (#1599) 2025-06-28 16:12:00 -07:00
Gero
b6fd48f282 Update to adventure 4.22.0 (#1595) 2025-06-27 16:56:05 +01:00
SpigotRCE
10e75b6d55 feat: property support for max clientside channels (#1557) 2025-06-21 10:05:04 +02:00
Shane Freeder
fe69214e77 Downgrade netty (Fixes #1591) 2025-06-17 18:46:22 +01:00
Gero
020c7fe6f5 1.21.6 (#1580) 2025-06-17 14:52:50 +01:00
Shane Freeder
44bc15db40 Add system property to skip packet compression threshold validation 2025-06-14 20:29:12 +01:00
Tofik ♡
549b8d624e Update some dependencies (#1576) 2025-06-14 19:53:22 +01:00
Shane Freeder
669fda298c Only apply max known pack restrictions Serverbound 2025-06-11 10:12:48 +01:00
AR
21ecd344ba Fix HoverEvent.showEntity() in protocol versions prior to 1.21.5 (#1578) 2025-05-25 21:24:21 +01:00
Shane Freeder
8c8162dbf6 Discard known packs if we don't have a target 2025-05-24 18:28:54 +01:00
Alex
5e20ec19ff Stabilize and expose suggestions API (#1406)
* Expose suggestions API

* Improve javadoc of suggestions api
2025-05-23 14:23:38 +01:00
Shane Freeder
5eb83760cd Attempt to improve partial read situations during early connections 2025-05-22 14:26:48 +01:00
Shane Freeder
8fea43d6ba Add some means to quickly overlook deframing issues 2025-05-22 13:13:06 +01:00
Kezz
678c7aa3a4 Modern-ify Adventure uses and fix bug in TranslatableMapper (#1575)
* fix: Don't ignore the player's locale in message translation

* feature: Use PointersSupplier to save constructing a Pointers instance for every player

* fix: Don't use a custom implementation of Identity for players

We don't need to carry about this object for every player.

* chore: Stop using deprecated TranslationRegistry

* fix: Simplify TranslatableMapper and fix bugs

- The fallback string is not intended to be translated, so don't do that.
- Check if the string can be translated in the default locale before using the closest mapper as devs may have their own strings.
- Remove the hardcoded check for TranslationRegistry instance as devs (and us now) can use non-TranslationRegistry translator instances.
2025-05-21 18:06:26 +01:00
Shane Freeder
e13c8c340f Increase limit to account for strings being strings 2025-05-09 22:17:04 +01:00
Shane Freeder
dc659538d3 Set netty allocator earlier and more globally 2025-05-09 20:19:29 +01:00
Shane Freeder
eb099d1220 Merge branches 'dev/3.0.0' and 'dev/3.0.0' of github.com:PaperMC/Velocity into dev/3.0.0 2025-05-09 15:55:40 +01:00
chris
1ad1f3b215 Use pooled netty allocator instead of default adaptive allocator (#1570) 2025-05-08 11:30:55 +01:00
Timon Seidel
063065b21a fix: adventure 4.21.0 adaptation (#1569) 2025-05-08 09:55:16 +01:00
Shane Freeder
00016ba4e1 Validate handshake packet length early 2025-05-08 00:15:50 +01:00
Timon Seidel
b411a0fa09 chore: bump adventure to 4.21.0 (#1564)
feat: support for 1.21.5+ hover and click events
2025-04-30 12:44:12 -07:00
Aaron
1561ba2e38 Bump adventure to 4.20.0 (#1544) 2025-04-28 16:13:48 -05:00
Shane Freeder
bd2bb6325e Validate state transition 2025-04-17 18:59:49 +01:00
booky
3f0a85d794 Fix MinecraftChannelIdentifier parsing to align with vanilla (#1552) 2025-04-14 13:52:02 -04:00
Shane Freeder
74d05211d6 Also validate length before caring to invest time into processing 2025-04-12 16:52:31 +01:00
Shane Freeder
7ad06614fe Appease checkstyle gods 2025-04-12 16:22:06 +01:00
Shane Freeder
163a85a468 Merge branch 'cleanup/plugin-message-channel-handling' into dev/3.0.0 2025-04-12 16:20:21 +01:00
Shane Freeder
a51711e4bb Use an ImmutableList Builder 2025-04-12 16:20:07 +01:00
Andrew Steinborn
ae312339a3 Disable io_uring transport by default 2025-04-11 00:35:49 -04:00
Shane Freeder
a429bb53ce Merge remote-tracking branch 'origin/dev/3.0.0' into cleanup/plugin-message-channel-handling 2025-04-09 10:06:40 +01:00
Andrew Steinborn
a549880df1 Bump to Netty 4.2.0 (#1380) 2025-04-09 01:21:08 -04:00
Shane Freeder
9c1be72db0 Fix tests 2025-04-06 21:25:08 +01:00
Shane Freeder
747f70d80a Appease checkstyle 2025-04-06 21:25:03 +01:00
Shane Freeder
b482443e79 Fix spot 2025-04-06 21:22:22 +01:00
Shane Freeder
676ec9cb21 preliminary cleanup of plugin message channel handling 2025-04-06 21:09:14 +01:00
微莹·纤绫
aae97dce3d Track plugin message channels that registered by clients (#1276) 2025-04-06 19:40:17 +01:00
Shane Freeder
c72a3eefde Check if kicking on command rate limit is enabled 2025-04-03 22:51:26 +01:00
SpigotRCE
86b88cf4b7 fix: typo 2025-04-03 18:59:16 +05:30
Bridge
7ffa43f0e2 feat: implement command rate limiter (#1524) 2025-04-03 13:26:00 +01:00
BBaoVanC
b3e218bd7d Show proxy-wide online players in server ping (#811)
* Show proxy-wide online players in server ping

* Reflow arguments in VelocityConfiguration constructor
2025-04-01 10:48:00 -05:00
Timon Seidel
9324a52ce0 fix: server link's custom labels not being translated (#1537) 2025-03-31 12:58:43 -05:00
Timon Seidel
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
Timon Seidel
c3d10bd410 feat: add function to directly pass collections in ServerPing.Builder (#1538) 2025-03-30 12:49:36 -05:00
Aaron
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
Gegy
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
Jones
d9f1016bd5 Validate uncompressed packet size (#1527)
* Validate uncompressed packet size

* Fix debug using incorrect value
2025-03-14 15:26:59 +00:00
Warrior
c9aa1cca09 Enable use tab in javadocs (#1525) 2025-03-10 15:11:01 +00:00
Riley Park
f980037bfd chore(proxy): require explicitly setting velocity.command.info permission to true 2025-03-06 19:14:18 -08:00
Andrew Steinborn
8f3dea5427 Bump Netty version 2025-02-28 22:21:32 -05:00
Gero
b8fe3577c9 Use correct component serializer in ComponentHolder#getBinaryTag 2025-02-28 15:01:30 +01:00
Andrew Steinborn
d4ea40a4a2 Add support for SO_REUSEPORT 2025-02-27 23:42:39 -05:00
Adrian
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
ST3V1K
58816c804a fix: problem with PluginMessageEvents for configuration phase (#1517) 2025-02-22 16:08:37 +00:00
Tommy
e69213f987 respect log-player-connections flag in config for connection messages (#1503) 2025-02-17 12:07:06 +00:00
Shane Freeder
f986eb51ec Do not print an exception if a client closed before switching to config state 2025-02-13 12:12:33 +00:00
Shane Freeder
9d1332d3a3 Add PluginMessageEvents for configuration phase 2025-02-13 11:53:30 +00:00
Andre_601
83c1749eed Add javadoc for ServerPing.Builder (#1500)
* Add javadoc for ServerPing.Builder

* Address codestyle reports
2025-02-12 14:35:38 -05:00
Kezz
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
Nassim Jahnke
1652d44f5f Fix checkstyle 💩 2025-01-31 19:35:43 +01:00
Nassim Jahnke
6815808d32 Improve fml mod list parsing 2025-01-31 19:11:34 +01:00
Outfluencer
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
scratchyone
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
Isaac - The456
876b9c3601 Fix ShutdownCommand message styling (#1427)
* Fix ShutdownCommand message

* Fix checkstyle violation.
2025-01-26 23:03:37 -05:00
kyngs
6995f415d3 Expose shutdownInProgress to the API. (#1485)
Co-authored-by: kyngs <kyngs@users.noreply.github.com>
2025-01-26 07:49:35 -05:00
92 changed files with 1903 additions and 453 deletions

View File

@@ -14,10 +14,10 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
- name: Set up JDK 17 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 21
distribution: 'temurin' distribution: 'zulu'
- 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

@@ -7,8 +7,10 @@
package com.velocitypowered.api.command; package com.velocitypowered.api.command;
import com.mojang.brigadier.suggestion.Suggestions;
import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.command.CommandExecuteEvent;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@@ -116,6 +118,27 @@ public interface CommandManager {
*/ */
CompletableFuture<Boolean> executeImmediatelyAsync(CommandSource source, String cmdLine); CompletableFuture<Boolean> executeImmediatelyAsync(CommandSource source, String cmdLine);
/**
* Asynchronously collects suggestions to fill in the given command {@code cmdLine}.
* Returns only the raw completion suggestions without tooltips.
*
* @param source the source to execute the command for
* @param cmdLine the partially completed command
* @return a {@link CompletableFuture} eventually completed with a {@link List}, possibly empty
*/
CompletableFuture<List<String>> offerSuggestions(CommandSource source, String cmdLine);
/**
* Asynchronously collects suggestions to fill in the given command {@code cmdLine}.
* Returns the brigadier {@link Suggestions} with tooltips for each result.
*
* @param source the source to execute the command for
* @param cmdLine the partially completed command
* @return a {@link CompletableFuture} eventually completed with {@link Suggestions}, possibly
* empty
*/
CompletableFuture<Suggestions> offerBrigadierSuggestions(CommandSource source, String cmdLine);
/** /**
* Returns an immutable collection of the case-insensitive aliases registered * Returns an immutable collection of the case-insensitive aliases registered
* on this manager. * on this manager.

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

@@ -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,10 @@ 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, "1.21.5"),
MINECRAFT_1_21_6(771, "1.21.6"),
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8");
private static final int SNAPSHOT_BIT = 30; private static final int SNAPSHOT_BIT = 30;

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

@@ -148,4 +148,59 @@ public interface ProxyConfig {
* @return read timeout (in milliseconds) * @return read timeout (in milliseconds)
*/ */
int getReadTimeout(); int getReadTimeout();
/**
* Get the rate limit for how fast a player can execute commands.
*
* @return the command rate limit (in milliseconds)
*/
int getCommandRatelimit();
/**
* Get whether we should forward commands to the backend if the player is rate limited.
*
* @return whether to forward commands if rate limited
*/
boolean isForwardCommandsIfRateLimited();
/**
* Get the kick limit for commands that are rate limited.
* If this limit is 0 or less, the player will be not be kicked.
*
* @return the rate limited command rate limit
*/
int getKickAfterRateLimitedCommands();
/**
* Get whether the proxy should kick players who are command rate limited.
*
* @return whether to kick players who are rate limited
*/
default boolean isKickOnCommandRateLimit() {
return getKickAfterRateLimitedCommands() > 0;
}
/**
* Get the rate limit for how fast a player can tab complete.
*
* @return the tab complete rate limit (in milliseconds)
*/
int getTabCompleteRatelimit();
/**
* Get the kick limit for tab completes that are rate limited.
* If this limit is 0 or less, the player will be not be kicked.
*
* @return the rate limited command rate limit
*/
int getKickAfterRateLimitedTabCompletes();
/**
* Get whether the proxy should kick players who are tab complete rate limited.
*
* @return whether to kick players who are rate limited
*/
default boolean isKickOnTabCompleteRateLimit() {
return getKickAfterRateLimitedTabCompletes() > 0;
}
} }

View File

@@ -11,7 +11,6 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import java.util.Objects; import java.util.Objects;
import java.util.regex.Pattern;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@@ -21,8 +20,6 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/ */
public final class MinecraftChannelIdentifier implements ChannelIdentifier { public final class MinecraftChannelIdentifier implements ChannelIdentifier {
private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9/\\-_]*");
private final String namespace; private final String namespace;
private final String name; private final String name;
@@ -39,7 +36,7 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
* @return a new channel identifier * @return a new channel identifier
*/ */
public static MinecraftChannelIdentifier forDefaultNamespace(String name) { public static MinecraftChannelIdentifier forDefaultNamespace(String name) {
return new MinecraftChannelIdentifier("minecraft", name); return new MinecraftChannelIdentifier(Key.MINECRAFT_NAMESPACE, name);
} }
/** /**
@@ -52,14 +49,10 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
public static MinecraftChannelIdentifier create(String namespace, String name) { public static MinecraftChannelIdentifier create(String namespace, String name) {
checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty"); checkArgument(!Strings.isNullOrEmpty(namespace), "namespace is null or empty");
checkArgument(name != null, "namespace is null or empty"); checkArgument(name != null, "namespace is null or empty");
checkArgument(VALID_IDENTIFIER_REGEX.matcher(namespace).matches(), checkArgument(Key.parseableNamespace(namespace),
"namespace is not valid, must match: %s got %s", "namespace is not valid, must match: [a-z0-9_.-] got %s", namespace);
VALID_IDENTIFIER_REGEX.toString(), checkArgument(Key.parseableValue(name),
namespace); "name is not valid, must match: [a-z0-9/._-] got %s", name);
checkArgument(VALID_IDENTIFIER_REGEX.matcher(name).matches(),
"name is not valid, must match: %s got %s",
VALID_IDENTIFIER_REGEX.toString(),
name);
return new MinecraftChannelIdentifier(namespace, name); return new MinecraftChannelIdentifier(namespace, name);
} }
@@ -72,10 +65,9 @@ public final class MinecraftChannelIdentifier implements ChannelIdentifier {
public static MinecraftChannelIdentifier from(String identifier) { public static MinecraftChannelIdentifier from(String identifier) {
int colonPos = identifier.indexOf(':'); int colonPos = identifier.indexOf(':');
if (colonPos == -1) { if (colonPos == -1) {
throw new IllegalArgumentException("Identifier does not contain a colon."); return create(Key.MINECRAFT_NAMESPACE, identifier);
} } else if (colonPos == 0) {
if (colonPos + 1 == identifier.length()) { return create(Key.MINECRAFT_NAMESPACE, identifier.substring(1));
throw new IllegalArgumentException("Identifier is empty.");
} }
String namespace = identifier.substring(0, colonPos); String namespace = identifier.substring(0, colonPos);
String name = identifier.substring(colonPos + 1); String name = identifier.substring(colonPos + 1);

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
default TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency,
int gameMode, @Nullable ChatSession chatSession, boolean listed, int listOrder) {
return buildEntry(profile, displayName, latency, gameMode, chatSession, listed, listOrder, true);
}
/**
* Represents an entry in a {@link Player}'s tab list.
*
* @param profile the profile
* @param displayName the display name
* @param latency the latency
* @param gameMode the game mode
* @param chatSession the chat session
* @param listed the visible status of entry
* @param listOrder the order/priority of entry in the tab list
* @param showHat the visibility of this entry's hat layer
* @return the entry
* @deprecated Internal usage. Use {@link TabListEntry.Builder} instead.
*/
@Deprecated
TabListEntry buildEntry(GameProfile profile, @Nullable Component displayName, int latency, 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);
} }

View File

@@ -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;
@@ -159,31 +160,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;
@@ -193,7 +242,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");
@@ -203,36 +252,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;
@@ -429,6 +516,10 @@ public final class ServerPing {
*/ */
public static final class SamplePlayer { public static final class SamplePlayer {
public static final SamplePlayer ANONYMOUS = new SamplePlayer(
"Anonymous Player",
new UUID(0L, 0L)
);
private final String name; private final String name;
private final UUID id; private final UUID id;

View File

@@ -19,7 +19,7 @@ public final class GameProfile {
private final UUID id; private final UUID id;
private final String undashedId; private final String undashedId;
private final String name; private String name;
private final List<Property> properties; private final List<Property> properties;
/** /**
@@ -80,6 +80,10 @@ public final class GameProfile {
return name; return name;
} }
public void setName(String name) {
this.name = name;
}
/** /**
* Returns an immutable list of profile properties associated with this profile. * Returns an immutable list of profile properties associated with this profile.
* *

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

@@ -47,17 +47,25 @@ class MinecraftChannelIdentifierTest {
create("velocity", "test/test2"); create("velocity", "test/test2");
} }
@Test
void fromIdentifierDefaultNamespace() {
assertEquals("minecraft", from("test").getNamespace());
assertEquals("minecraft", from(":test").getNamespace());
}
@Test
void fromIdentifierAllowsEmptyName() {
from("minecraft:");
from(":");
from("");
}
@Test @Test
void fromIdentifierThrowsOnBadValues() { void fromIdentifierThrowsOnBadValues() {
assertAll( assertAll(
() -> assertThrows(IllegalArgumentException.class, () -> from("")),
() -> assertThrows(IllegalArgumentException.class, () -> from(":")),
() -> assertThrows(IllegalArgumentException.class, () -> from(":a")),
() -> assertThrows(IllegalArgumentException.class, () -> from("a:")),
() -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")), () -> assertThrows(IllegalArgumentException.class, () -> from("hello:$$$$$$")),
() -> assertThrows(IllegalArgumentException.class, () -> from("he/llo:wor/ld")),
() -> assertThrows(IllegalArgumentException.class, () -> from("hello::")) () -> assertThrows(IllegalArgumentException.class, () -> from("hello::"))
); );
} }
} }

View File

@@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
// This interface is needed as a workaround to get an instance of ExecOperations
interface Injected {
@get:Inject
val execOps: ExecOperations
}
val currentShortRevision = ByteArrayOutputStream().use { val currentShortRevision = ByteArrayOutputStream().use {
exec { val execOps = objects.newInstance<Injected>().execOps
execOps.exec {
executable = "git" executable = "git"
args = listOf("rev-parse", "HEAD") args = listOf("rev-parse", "HEAD")
standardOutput = it standardOutput = it

View File

@@ -8,7 +8,7 @@ extensions.configure<PublishingExtension> {
maven { maven {
credentials(PasswordCredentials::class.java) credentials(PasswordCredentials::class.java)
name = "paper" name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined
val base = "https://repo.papermc.io/repository/maven" val base = "https://repo.papermc.io/repository/maven"
val releasesRepoUrl = "$base-releases/" val releasesRepoUrl = "$base-releases/"
val snapshotsRepoUrl = "$base-snapshots/" val snapshotsRepoUrl = "$base-snapshots/"

View File

@@ -20,11 +20,11 @@ subprojects {
testImplementation(rootProject.libs.junit) testImplementation(rootProject.libs.junit)
} }
tasks { testing.suites.named<JvmTestSuite>("test") {
test { useJUnitJupiter()
useJUnitPlatform() targets.all {
reports { testTask.configure {
junitXml.required.set(true) reports.junitXml.required = true
} }
} }
} }

View File

@@ -2,23 +2,24 @@
configurate3 = "3.7.3" 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.3"
netty = "4.1.114.Final" netty = "4.2.1.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.3"
indra-publishing = "net.kyori.indra.publishing:2.0.6" indra-publishing = "net.kyori.indra.publishing:2.0.6"
shadow = "io.github.goooler.shadow:8.1.5" shadow = "com.gradleup.shadow:8.3.6"
spotless = "com.diffplug.spotless:6.25.0" spotless = "com.diffplug.spotless:6.25.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:4.18.0" adventure-bom = "net.kyori:adventure-bom:4.23.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.18.0" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.23.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.8"
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"
@@ -33,11 +34,11 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.15" fastutil = "it.unimi.dsi:fastutil:8.5.15"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-jansi:3.27.1" jline = "org.jline:jline-terminal-jansi:3.30.2"
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"
@@ -54,8 +55,9 @@ netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty"
netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" } netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.6.7" nightconfig = "com.electronwill.night-config:toml:3.6.7"
slf4j = "org.slf4j:slf4j-api:2.0.12" slf4j = "org.slf4j:slf4j-api:2.0.17"
snakeyaml = "org.yaml:snakeyaml:1.33" snakeyaml = "org.yaml:snakeyaml:1.33"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"

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.11.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View File

@@ -1,9 +1,11 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import io.papermc.fill.model.BuildChannel
plugins { plugins {
application application
id("velocity-init-manifest") id("velocity-init-manifest")
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
alias(libs.plugins.fill)
} }
application { application {
@@ -100,6 +102,7 @@ tasks {
runShadow { runShadow {
workingDir = file("run").also(File::mkdirs) workingDir = file("run").also(File::mkdirs)
standardInput = System.`in` standardInput = System.`in`
jvmArgs("-Dvelocity.packet-decode-logging=true")
} }
named<JavaExec>("run") { named<JavaExec>("run") {
workingDir = file("run").also(File::mkdirs) workingDir = file("run").also(File::mkdirs)
@@ -107,6 +110,24 @@ tasks {
} }
} }
val projectVersion = version as String
fill {
project("velocity")
build {
channel = BuildChannel.STABLE
versionFamily("3.0.0")
version(projectVersion)
downloads {
register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile }
nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" }
}
}
}
}
dependencies { dependencies {
implementation(project(":velocity-api")) implementation(project(":velocity-api"))
implementation(project(":velocity-native")) implementation(project(":velocity-native"))
@@ -121,6 +142,9 @@ dependencies {
implementation(libs.netty.transport.native.epoll) implementation(libs.netty.transport.native.epoll)
implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-x86_64") }) implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-x86_64") })
implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-aarch_64") }) implementation(variantOf(libs.netty.transport.native.epoll) { classifier("linux-aarch_64") })
implementation(libs.netty.transport.native.iouring)
implementation(variantOf(libs.netty.transport.native.iouring) { classifier("linux-x86_64") })
implementation(variantOf(libs.netty.transport.native.iouring) { classifier("linux-aarch_64") })
implementation(libs.netty.transport.native.kqueue) implementation(libs.netty.transport.native.kqueue)
implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-x86_64") }) implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-x86_64") })
implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-aarch_64") }) implementation(variantOf(libs.netty.transport.native.kqueue) { classifier("osx-aarch_64") })

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

@@ -47,6 +47,11 @@ public class Velocity {
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir")); System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir"));
} }
// Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator
if (System.getProperty("io.netty.allocator.type") == null) {
System.setProperty("io.netty.allocator.type", "pooled");
}
// Disable the resource leak detector by default as it reduces performance. Allow the user to // Disable the resource leak detector by default as it reduces performance. Allow the user to
// override this if desired. // override this if desired.
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) { if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {

View File

@@ -52,6 +52,7 @@ import com.velocitypowered.proxy.connection.util.ServerListPingHandler;
import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.console.VelocityConsole;
import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.crypto.EncryptionUtils;
import com.velocitypowered.proxy.event.VelocityEventManager; import com.velocitypowered.proxy.event.VelocityEventManager;
import com.velocitypowered.proxy.netease.NeteaseDataManager;
import com.velocitypowered.proxy.network.ConnectionManager; import com.velocitypowered.proxy.network.ConnectionManager;
import com.velocitypowered.proxy.plugin.VelocityPluginManager; import com.velocitypowered.proxy.plugin.VelocityPluginManager;
import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer;
@@ -75,11 +76,13 @@ import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.KeyPair; import java.security.KeyPair;
import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -103,7 +106,7 @@ import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationRegistry; import net.kyori.adventure.translation.TranslationStore;
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.bstats.MetricsBase; import org.bstats.MetricsBase;
@@ -162,7 +165,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>(); private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>(); private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final VelocityConsole console; private final VelocityConsole console;
private @MonotonicNonNull Ratelimiter ipAttemptLimiter; private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
private @MonotonicNonNull Ratelimiter<UUID> tabCompleteRateLimiter;
private final VelocityEventManager eventManager; private final VelocityEventManager eventManager;
private final VelocityScheduler scheduler; private final VelocityScheduler scheduler;
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar(); private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
@@ -282,6 +287,14 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
new GlistCommand(this).register(); new GlistCommand(this).register();
new SendCommand(this).register(); new SendCommand(this).register();
try {
NeteaseDataManager.init();
} catch (Exception e) {
logger.error("Failed to initialize NeteaseDataManager, please check your config.", e);
LogManager.shutdown();
System.exit(1);
}
this.doStartupConfigLoad(); this.doStartupConfigLoad();
for (ServerInfo cliServer : options.getServers()) { for (ServerInfo cliServer : options.getServers()) {
@@ -295,6 +308,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
} }
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit()); ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit());
tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit());
loadPlugins(); loadPlugins();
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance // Go ahead and fire the proxy initialization event. We block since plugins should have a chance
@@ -332,8 +347,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
} }
private void registerTranslations() { private void registerTranslations() {
final TranslationRegistry translationRegistry = TranslationRegistry final TranslationStore.StringBased<MessageFormat> translationRegistry =
.create(Key.key("velocity", "translations")); TranslationStore.messageFormat(Key.key("velocity", "translations"));
translationRegistry.defaultLocale(Locale.US); translationRegistry.defaultLocale(Locale.US);
try { try {
ResourceUtils.visitResources(VelocityServer.class, path -> { ResourceUtils.visitResources(VelocityServer.class, path -> {
@@ -654,10 +669,18 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return cm.createHttpClient(); return cm.createHttpClient();
} }
public Ratelimiter getIpAttemptLimiter() { public @MonotonicNonNull Ratelimiter<InetAddress> getIpAttemptLimiter() {
return ipAttemptLimiter; return ipAttemptLimiter;
} }
public @MonotonicNonNull Ratelimiter<UUID> getCommandRateLimiter() {
return commandRateLimiter;
}
public @MonotonicNonNull Ratelimiter<UUID> getTabCompleteRateLimiter() {
return tabCompleteRateLimiter;
}
/** /**
* Checks if the {@code connection} can be registered with the proxy. * Checks if the {@code connection} can be registered with the proxy.
* *
@@ -804,6 +827,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

@@ -300,27 +300,14 @@ public class VelocityCommandManager implements CommandManager {
); );
} }
/** @Override
* Returns suggestions to fill in the given command.
*
* @param source the source to execute the command for
* @param cmdLine the partially completed command
* @return a {@link CompletableFuture} eventually completed with a {@link List}, possibly empty
*/
public CompletableFuture<List<String>> offerSuggestions(final CommandSource source, public CompletableFuture<List<String>> offerSuggestions(final CommandSource source,
final String cmdLine) { final String cmdLine) {
return offerBrigadierSuggestions(source, cmdLine) return offerBrigadierSuggestions(source, cmdLine)
.thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText)); .thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText));
} }
/** @Override
* Returns suggestions to fill in the given command.
*
* @param source the source to execute the command for
* @param cmdLine the partially completed command
* @return a {@link CompletableFuture} eventually completed with {@link Suggestions}, possibly
* empty
*/
public CompletableFuture<Suggestions> offerBrigadierSuggestions( public CompletableFuture<Suggestions> offerBrigadierSuggestions(
final CommandSource source, final String cmdLine) { final CommandSource source, final String cmdLine) {
Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(source, "source");

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

@@ -78,6 +78,8 @@ public class VelocityConfiguration implements ProxyConfig {
private boolean onlineModeKickExistingPlayers = false; private boolean onlineModeKickExistingPlayers = false;
@Expose @Expose
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
@Expose
private boolean samplePlayersInPing = false;
private final Servers servers; private final Servers servers;
private final ForcedHosts forcedHosts; private final ForcedHosts forcedHosts;
@Expose @Expose
@@ -105,8 +107,9 @@ public class VelocityConfiguration implements ProxyConfig {
boolean preventClientProxyConnections, boolean announceForge, boolean preventClientProxyConnections, boolean announceForge,
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
boolean enablePlayerAddressLogging, Servers servers, ForcedHosts forcedHosts, boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication) { ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
boolean forceKeyAuthentication) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
@@ -117,6 +120,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.forwardingSecret = forwardingSecret; this.forwardingSecret = forwardingSecret;
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers; this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
this.pingPassthrough = pingPassthrough; this.pingPassthrough = pingPassthrough;
this.samplePlayersInPing = samplePlayersInPing;
this.enablePlayerAddressLogging = enablePlayerAddressLogging; this.enablePlayerAddressLogging = enablePlayerAddressLogging;
this.servers = servers; this.servers = servers;
this.forcedHosts = forcedHosts; this.forcedHosts = forcedHosts;
@@ -230,6 +234,11 @@ public class VelocityConfiguration implements ProxyConfig {
valid = false; valid = false;
} }
if (advanced.commandRateLimit < 0) {
logger.error("Invalid command rate limit {}", advanced.commandRateLimit);
valid = false;
}
loadFavicon(); loadFavicon();
return valid; return valid;
@@ -351,6 +360,31 @@ public class VelocityConfiguration implements ProxyConfig {
return advanced.getReadTimeout(); return advanced.getReadTimeout();
} }
@Override
public int getCommandRatelimit() {
return advanced.getCommandRateLimit();
}
@Override
public int getTabCompleteRatelimit() {
return advanced.getTabCompleteRateLimit();
}
@Override
public int getKickAfterRateLimitedTabCompletes() {
return advanced.getKickAfterRateLimitedTabCompletes();
}
@Override
public boolean isForwardCommandsIfRateLimited() {
return advanced.isForwardCommandsIfRateLimited();
}
@Override
public int getKickAfterRateLimitedCommands() {
return advanced.getKickAfterRateLimitedCommands();
}
public boolean isProxyProtocol() { public boolean isProxyProtocol() {
return advanced.isProxyProtocol(); return advanced.isProxyProtocol();
} }
@@ -371,6 +405,10 @@ public class VelocityConfiguration implements ProxyConfig {
return pingPassthrough; return pingPassthrough;
} }
public boolean getSamplePlayersInPing() {
return samplePlayersInPing;
}
public boolean isPlayerAddressLoggingEnabled() { public boolean isPlayerAddressLoggingEnabled() {
return enablePlayerAddressLogging; return enablePlayerAddressLogging;
} }
@@ -407,6 +445,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)
@@ -503,6 +545,8 @@ public class VelocityConfiguration implements ProxyConfig {
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough", final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED); PingPassthroughMode.DISABLED);
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565"); final String bind = config.getOrElse("bind", "0.0.0.0:25565");
final int maxPlayers = config.getIntOrElse("show-max-players", 500); final int maxPlayers = config.getIntOrElse("show-max-players", 500);
final boolean onlineMode = config.getOrElse("online-mode", true); final boolean onlineMode = config.getOrElse("online-mode", true);
@@ -533,6 +577,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingSecret, forwardingSecret,
kickExisting, kickExisting,
pingPassthroughMode, pingPassthroughMode,
samplePlayersInPing,
enablePlayerAddressLogging, enablePlayerAddressLogging,
new Servers(serversConfig), new Servers(serversConfig),
new ForcedHosts(forcedHostsConfig), new ForcedHosts(forcedHostsConfig),
@@ -716,6 +761,18 @@ 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;
@Expose
private int commandRateLimit = 50;
@Expose
private boolean forwardCommandsIfRateLimited = true;
@Expose
private int kickAfterRateLimitedCommands = 5;
@Expose
private int tabCompleteRateLimit = 50;
@Expose
private int kickAfterRateLimitedTabCompletes = 10;
private Advanced() { private Advanced() {
} }
@@ -741,6 +798,12 @@ 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);
this.commandRateLimit = config.getIntOrElse("command-rate-limit", 25);
this.forwardCommandsIfRateLimited = config.getOrElse("forward-commands-if-rate-limited", true);
this.kickAfterRateLimitedCommands = config.getIntOrElse("kick-after-rate-limited-commands", 0);
this.tabCompleteRateLimit = config.getIntOrElse("tab-complete-rate-limit", 10); // very lenient
this.kickAfterRateLimitedTabCompletes = config.getIntOrElse("kick-after-rate-limited-tab-completes", 0);
} }
} }
@@ -804,6 +867,30 @@ public class VelocityConfiguration implements ProxyConfig {
return this.acceptTransfers; return this.acceptTransfers;
} }
public boolean isEnableReusePort() {
return enableReusePort;
}
public int getCommandRateLimit() {
return commandRateLimit;
}
public boolean isForwardCommandsIfRateLimited() {
return forwardCommandsIfRateLimited;
}
public int getKickAfterRateLimitedCommands() {
return kickAfterRateLimitedCommands;
}
public int getTabCompleteRateLimit() {
return tabCompleteRateLimit;
}
public int getKickAfterRateLimitedTabCompletes() {
return kickAfterRateLimitedTabCompletes;
}
@Override @Override
public String toString() { public String toString() {
return "Advanced{" return "Advanced{"
@@ -821,6 +908,7 @@ public class VelocityConfiguration implements ProxyConfig {
+ ", logCommandExecutions=" + logCommandExecutions + ", logCommandExecutions=" + logCommandExecutions
+ ", logPlayerConnections=" + logPlayerConnections + ", logPlayerConnections=" + logPlayerConnections
+ ", acceptTransfers=" + acceptTransfers + ", acceptTransfers=" + acceptTransfers
+ ", enableReusePort=" + enableReusePort
+ '}'; + '}';
} }
} }

View File

@@ -46,6 +46,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueInboundHandler; import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueInboundHandler;
import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler; import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler;
@@ -84,6 +85,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
private static final Logger logger = LogManager.getLogger(MinecraftConnection.class); private static final Logger logger = LogManager.getLogger(MinecraftConnection.class);
private final Channel channel; private final Channel channel;
public boolean pendingConfigurationSwitch = false;
private SocketAddress remoteAddress; private SocketAddress remoteAddress;
private StateRegistry state; private StateRegistry state;
private Map<StateRegistry, MinecraftSessionHandler> sessionHandlers; private Map<StateRegistry, MinecraftSessionHandler> sessionHandlers;
@@ -367,6 +369,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
ensureInEventLoop(); ensureInEventLoop();
this.state = state; this.state = state;
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
.get(MinecraftVarintFrameDecoder.class);
if (frameDecoder != null) {
frameDecoder.setState(state);
}
// If the connection is LEGACY (<1.6), the decoder and encoder are not set. // If the connection is LEGACY (<1.6), the decoder and encoder are not set.
final MinecraftEncoder minecraftEncoder = this.channel.pipeline() final MinecraftEncoder minecraftEncoder = this.channel.pipeline()
.get(MinecraftEncoder.class); .get(MinecraftEncoder.class);

View File

@@ -47,6 +47,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket; import com.velocitypowered.proxy.protocol.packet.AvailableCommandsPacket;
import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
@@ -149,6 +150,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
MinecraftConnection smc = serverConn.ensureConnected(); MinecraftConnection smc = serverConn.ensureConnected();
smc.setAutoReading(false); smc.setAutoReading(false);
// Even when not auto reading messages are still decoded. Decode them with the correct state // Even when not auto reading messages are still decoded. Decode them with the correct state
smc.getChannel().pipeline().get(MinecraftVarintFrameDecoder.class).setState(StateRegistry.CONFIG);
smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.CONFIG); smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.CONFIG);
serverConn.getPlayer().switchToConfigState(); serverConn.getPlayer().switchToConfigState();
return true; return true;

View File

@@ -20,6 +20,7 @@ package com.velocitypowered.proxy.connection.backend;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.ServerConnection;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
@@ -316,9 +317,9 @@ public class BungeeCordMessageResponder {
}); });
} }
static String getBungeeCordChannel(ProtocolVersion version) { static ChannelIdentifier getBungeeCordChannel(ProtocolVersion version) {
return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL.getId() return version.noLessThan(ProtocolVersion.MINECRAFT_1_13) ? MODERN_CHANNEL
: LEGACY_CHANNEL.getId(); : LEGACY_CHANNEL;
} }
// Note: this method will always release the buffer! // Note: this method will always release the buffer!
@@ -329,8 +330,8 @@ public class BungeeCordMessageResponder {
// Note: this method will always release the buffer! // Note: this method will always release the buffer!
private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) { private static void sendServerResponse(ConnectedPlayer player, ByteBuf buf) {
MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected(); MinecraftConnection serverConnection = player.ensureAndGetCurrentServer().ensureConnected();
String chan = getBungeeCordChannel(serverConnection.getProtocolVersion()); ChannelIdentifier chan = getBungeeCordChannel(serverConnection.getProtocolVersion());
PluginMessagePacket msg = new PluginMessagePacket(chan, buf); PluginMessagePacket msg = new PluginMessagePacket(chan.getId(), buf);
serverConnection.write(msg); serverConnection.write(msg);
} }

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;
@@ -38,6 +40,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
@@ -54,6 +57,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;
@@ -228,6 +233,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
final ConnectedPlayer player = serverConn.getPlayer(); final ConnectedPlayer player = serverConn.getPlayer();
final ClientConfigSessionHandler configHandler = (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler(); final ClientConfigSessionHandler configHandler = (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler();
smc.getChannel().pipeline().get(MinecraftVarintFrameDecoder.class).setState(StateRegistry.PLAY);
smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY);
//noinspection DataFlowIssue //noinspection DataFlowIssue
configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> { configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> {
@@ -261,7 +267,29 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(),
serverConn.getPlayer().getProtocolVersion())); serverConn.getPlayer().getProtocolVersion()));
} else { } else {
serverConn.getPlayer().getConnection().write(packet.retain()); byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
serverConn.getPlayer().getConnection().write(packet.retain());
return true;
}
// Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool
this.serverConn.getConnection().setAutoReading(false);
this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, bytes))
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && !serverConn.getPlayer().getConnection().isClosed()) {
serverConn.getPlayer().getConnection().write(new PluginMessagePacket(
pme.getIdentifier().getId(), Unpooled.wrappedBuffer(bytes)));
}
this.serverConn.getConnection().setAutoReading(true);
}, serverConn.ensureConnected().eventLoop()).exceptionally((ex) -> {
logger.error("Exception while handling plugin message {}", packet, ex);
return null;
});
} }
return true; return true;
} }

View File

@@ -106,7 +106,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
return CompletableFuture.completedFuture(null); return CompletableFuture.completedFuture(null);
} }
logger.info("{} has connected", player); if (server.getConfiguration().isLogPlayerConnections()) {
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) {
serverConn.ensureConnected().write(packet.retain()); byte[] bytes = ByteBufUtil.getBytes(packet.content());
ChannelIdentifier id = this.server.getChannelRegistrar().getFromId(packet.getChannel());
if (id == null) {
serverConn.ensureConnected().write(packet.retain());
return true;
}
// Handling this stuff async means that we should probably pause
// the connection while we toss this off into another pool
serverConn.getPlayer().getConnection().setAutoReading(false);
this.server.getEventManager()
.fire(new PluginMessageEvent(serverConn.getPlayer(), serverConn, id, bytes))
.thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && serverConn.getConnection() != null) {
serverConn.ensureConnected().write(new PluginMessagePacket(
pme.getIdentifier().getId(), Unpooled.wrappedBuffer(bytes)));
}
serverConn.getPlayer().getConnection().setAutoReading(true);
}, player.getConnection().eventLoop()).exceptionally((ex) -> {
logger.error("Exception while handling plugin message packet for {}", player, ex);
return null;
});
} }
return true; return true;
} }
@@ -142,7 +170,11 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(KnownPacksPacket packet) { public boolean handle(KnownPacksPacket packet) {
callConfigurationEvent().thenRun(() -> { callConfigurationEvent().thenRun(() -> {
player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet); VelocityServerConnection targetServer =
player.getConnectionInFlightOrConnectedServer();
if (targetServer != null) {
targetServer.ensureConnected().write(packet);
}
}).exceptionally(ex -> { }).exceptionally(ex -> {
logger.error("Error forwarding known packs response to backend:", ex); logger.error("Error forwarding known packs response to backend:", ex);
return null; return null;

View File

@@ -30,8 +30,6 @@ import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.ConnectionTypes;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
@@ -74,6 +72,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.CharacterUtil; import com.velocitypowered.proxy.util.CharacterUtil;
import com.velocitypowered.proxy.util.except.QuietRuntimeException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
@@ -113,6 +112,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private CompletableFuture<Void> configSwitchFuture; private CompletableFuture<Void> configSwitchFuture;
private int failedTabCompleteAttempts;
/** /**
* Constructs a client play session handler. * Constructs a client play session handler.
* *
@@ -160,7 +161,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void activated() { public void activated() {
configSwitchFuture = new CompletableFuture<>(); configSwitchFuture = new CompletableFuture<>();
Collection<String> channels = Collection<ChannelIdentifier> channels =
server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion());
if (!channels.isEmpty()) { if (!channels.isEmpty()) {
PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels); PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels);
@@ -170,6 +171,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);
} }
@@ -307,20 +309,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
logger.warn("A plugin message was received while the backend server was not " logger.warn("A plugin message was received while the backend server was not "
+ "ready. Channel: {}. Packet discarded.", packet.getChannel()); + "ready. Channel: {}. Packet discarded.", packet.getChannel());
} else if (PluginMessageUtil.isRegister(packet)) { } else if (PluginMessageUtil.isRegister(packet)) {
List<String> channels = PluginMessageUtil.getChannels(packet); List<ChannelIdentifier> channels =
List<ChannelIdentifier> channelIdentifiers = new ArrayList<>(); PluginMessageUtil.getChannels(this.player.getClientsideChannels().size(), packet,
for (String channel : channels) { this.player.getProtocolVersion());
try { player.getClientsideChannels().addAll(channels);
channelIdentifiers.add(MinecraftChannelIdentifier.from(channel));
} catch (IllegalArgumentException e) {
channelIdentifiers.add(new LegacyChannelIdentifier(channel));
}
}
server.getEventManager() server.getEventManager()
.fireAndForget( .fireAndForget(
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers))); new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain()); backendConn.write(packet.retain());
} else if (PluginMessageUtil.isUnregister(packet)) { } else if (PluginMessageUtil.isUnregister(packet)) {
player.getClientsideChannels()
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()));
backendConn.write(packet.retain()); backendConn.write(packet.retain());
} else if (PluginMessageUtil.isMcBrand(packet)) { } else if (PluginMessageUtil.isMcBrand(packet)) {
String brand = PluginMessageUtil.readBrandMessage(packet.content()); String brand = PluginMessageUtil.readBrandMessage(packet.content());
@@ -394,10 +393,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(FinishedUpdatePacket packet) { public boolean handle(FinishedUpdatePacket packet) {
if (!player.getConnection().pendingConfigurationSwitch) {
throw new QuietRuntimeException("Not expecting reconfiguration");
}
// Complete client switch // Complete client switch
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
VelocityServerConnection serverConnection = player.getConnectedServer(); VelocityServerConnection serverConnection = player.getConnectedServer();
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection)); server.getEventManager()
.fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
if (serverConnection != null) { if (serverConnection != null) {
MinecraftConnection smc = serverConnection.ensureConnected(); MinecraftConnection smc = serverConnection.ensureConnected();
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
@@ -444,6 +447,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();
@@ -577,11 +587,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Tell the server about the proxy's plugin message channels. // Tell the server about the proxy's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion(); ProtocolVersion serverVersion = serverMc.getProtocolVersion();
final Collection<String> channels = server.getChannelRegistrar() final Collection<ChannelIdentifier> channels = server.getChannelRegistrar()
.getChannelsForProtocol(serverMc.getProtocolVersion()); .getChannelsForProtocol(serverMc.getProtocolVersion());
if (!channels.isEmpty()) { if (!channels.isEmpty()) {
serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels));
} }
// Tell the server about this client's plugin message channels.
if (!player.getClientsideChannels().isEmpty()) {
serverMc.delayedWrite(constructChannelsPacket(serverVersion, player.getClientsideChannels()));
}
// If we had plugin messages queued during login/FML handshake, send them now. // If we had plugin messages queued during login/FML handshake, send them now.
PluginMessagePacket pm; PluginMessagePacket pm;
@@ -663,6 +677,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return false; return false;
} }
if (!server.getTabCompleteRateLimiter().attempt(player.getUniqueId())) {
if (server.getConfiguration().isKickOnTabCompleteRateLimit()
&& failedTabCompleteAttempts++ >= server.getConfiguration().getKickAfterRateLimitedTabCompletes()) {
player.disconnect(Component.translatable("velocity.kick.tab-complete-rate-limit"));
}
return true;
}
failedTabCompleteAttempts = 0;
server.getCommandManager().offerBrigadierSuggestions(player, command) server.getCommandManager().offerBrigadierSuggestions(player, command)
.thenAcceptAsync(suggestions -> { .thenAcceptAsync(suggestions -> {
if (suggestions.isEmpty()) { if (suggestions.isEmpty()) {

View File

@@ -99,6 +99,7 @@ import com.velocitypowered.proxy.tablist.VelocityTabListLegacy;
import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
import com.velocitypowered.proxy.util.DurationUtils; import com.velocitypowered.proxy.util.DurationUtils;
import com.velocitypowered.proxy.util.TranslatableMapper; import com.velocitypowered.proxy.util.TranslatableMapper;
import com.velocitypowered.proxy.util.collect.CappedSet;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@@ -122,6 +123,7 @@ import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers; import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.resource.ResourcePackInfoLike; import net.kyori.adventure.resource.ResourcePackInfoLike;
import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike; import net.kyori.adventure.resource.ResourcePackRequestLike;
@@ -144,13 +146,23 @@ import org.jetbrains.annotations.NotNull;
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable, public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable,
VelocityInboundConnection { VelocityInboundConnection {
public static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = Integer.getInteger("velocity.max-clientside-plugin-channels", 1024);
private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE =
PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build(); PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build();
static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
private static final ComponentLogger logger = ComponentLogger.logger(ConnectedPlayer.class); private static final ComponentLogger logger = ComponentLogger.logger(ConnectedPlayer.class);
private final Identity identity = new IdentityImpl(); private static final @NotNull PointersSupplier<ConnectedPlayer> POINTERS_SUPPLIER =
PointersSupplier.<ConnectedPlayer>builder()
.resolving(Identity.UUID, Player::getUniqueId)
.resolving(Identity.NAME, Player::getUsername)
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
.resolving(Identity.LOCALE, Player::getEffectiveLocale)
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
.resolving(FacetPointers.TYPE, player -> Type.PLAYER)
.build();
/** /**
* The actual Minecraft connection. This is actually a wrapper object around the Netty channel. * The actual Minecraft connection. This is actually a wrapper object around the Netty channel.
*/ */
@@ -173,24 +185,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private final InternalTabList tabList; private final InternalTabList tabList;
private final VelocityServer server; private final VelocityServer server;
private ClientConnectionPhase connectionPhase; private ClientConnectionPhase connectionPhase;
private final Collection<ChannelIdentifier> clientsideChannels;
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>(); private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
private @MonotonicNonNull List<String> serversToTry = null; private @MonotonicNonNull List<String> serversToTry = null;
private final ResourcePackHandler resourcePackHandler; private final ResourcePackHandler resourcePackHandler;
private final BundleDelimiterHandler bundleHandler = new BundleDelimiterHandler(this); private final BundleDelimiterHandler bundleHandler = new BundleDelimiterHandler(this);
private final @NotNull Pointers pointers =
Player.super.pointers().toBuilder()
.withDynamic(Identity.UUID, this::getUniqueId)
.withDynamic(Identity.NAME, this::getUsername)
.withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername()))
.withDynamic(Identity.LOCALE, this::getEffectiveLocale)
.withStatic(PermissionChecker.POINTER, getPermissionChecker())
.withStatic(FacetPointers.TYPE, Type.PLAYER).build();
private @Nullable String clientBrand; private @Nullable String clientBrand;
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,
@@ -205,6 +210,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
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;
this.clientsideChannels = CappedSet.create(MAX_CLIENTSIDE_PLUGIN_CHANNELS);
if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) { if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
this.tabList = new VelocityTabList(this); this.tabList = new VelocityTabList(this);
@@ -236,13 +242,24 @@ 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;
} }
@Override @Override
public @NonNull Identity identity() { public @NonNull Identity identity() {
return this.identity; return Identity.identity(this.getUniqueId());
} }
@Override @Override
@@ -348,7 +365,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
@Override @Override
public @NotNull Pointers pointers() { public @NotNull Pointers pointers() {
return this.pointers; return POINTERS_SUPPLIER.view(this);
} }
@Override @Override
@@ -381,14 +398,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
/** /**
* Translates the message in the user's locale. * Translates the message in the user's locale, falling back to the default locale if not set.
* *
* @param message the message to translate * @param message the message to translate
* @return the translated message * @return the translated message
*/ */
public Component translateMessage(Component message) { public Component translateMessage(Component message) {
Locale locale = ClosestLocaleMatcher.INSTANCE Locale locale = this.getEffectiveLocale();
.lookupClosest(getEffectiveLocale() == null ? Locale.getDefault() : getEffectiveLocale()); if (locale == null && settings != null) {
locale = settings.getLocale();
}
if (locale == null) {
locale = Locale.getDefault();
}
locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(locale);
return GlobalTranslator.render(message, locale); return GlobalTranslator.render(message, locale);
} }
@@ -1090,8 +1113,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
@@ -1287,11 +1316,17 @@ 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);
} }
connection.write(StartUpdatePacket.INSTANCE); connection.write(StartUpdatePacket.INSTANCE);
connection.pendingConfigurationSwitch = true;
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
// Make sure we don't send any play packets to the player after update start // Make sure we don't send any play packets to the player after update start
connection.addPlayPacketQueueHandler(); connection.addPlayPacketQueueHandler();
@@ -1320,19 +1355,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.connectionPhase = connectionPhase; this.connectionPhase = connectionPhase;
} }
/**
* Return all the plugin message channels that registered by client.
*
* @return the channels
*/
public Collection<ChannelIdentifier> getClientsideChannels() {
return clientsideChannels;
}
@Override @Override
public @Nullable IdentifiedKey getIdentifiedKey() { public @Nullable IdentifiedKey getIdentifiedKey() {
return playerKey; return playerKey;
} }
private class IdentityImpl implements Identity {
@Override
public @NonNull UUID uuid() {
return ConnectedPlayer.this.getUniqueId();
}
}
@Override @Override
public ProtocolState getProtocolState() { public ProtocolState getProtocolState() {
return connection.getState().toProtocolState(); return connection.getState().toProtocolState();

View File

@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.connection.client; package com.velocitypowered.proxy.connection.client;
import static com.google.common.net.UrlEscapers.urlFormParameterEscaper;
import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON; import static com.velocitypowered.proxy.VelocityServer.GENERAL_GSON;
import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY; import static com.velocitypowered.proxy.connection.VelocityConstants.EMPTY_BYTE_ARRAY;
import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa; import static com.velocitypowered.proxy.crypto.EncryptionUtils.decryptRsa;
@@ -25,6 +24,7 @@ import static com.velocitypowered.proxy.crypto.EncryptionUtils.generateServerId;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.primitives.Longs; import com.google.common.primitives.Longs;
import com.google.gson.JsonObject;
import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent;
import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@@ -34,6 +34,8 @@ 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.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl;
import com.velocitypowered.proxy.netease.AuthResponse;
import com.velocitypowered.proxy.netease.NeteaseDataManager;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
@@ -50,6 +52,7 @@ import java.net.http.HttpResponse;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
@@ -198,22 +201,21 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
} }
byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret()); final byte[] decryptedSharedSecret = decryptRsa(serverKeyPair, packet.getSharedSecret());
String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic()); final String serverId = generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
final String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString();
String playerIp = ((InetSocketAddress) mcConnection.getRemoteAddress()).getHostString(); JsonObject data = new JsonObject();
String url = String.format(MOJANG_HASJOINED_URL, data.addProperty("username", login.getUsername());
urlFormParameterEscaper().escape(login.getUsername()), serverId); data.addProperty("serverId", serverId);
data.addProperty("gameID", NeteaseDataManager.getConfig().getGameId());
if (server.getConfiguration().shouldPreventClientProxyConnections()) { HttpRequest httpRequest = HttpRequest.newBuilder()
url += "&ip=" + urlFormParameterEscaper().escape(playerIp); .headers("Content-Type", "application/json")
} .uri(URI.create(NeteaseDataManager.getConfig().getAuthUrl()))
.POST(HttpRequest.BodyPublishers.ofString(data.toString()))
final HttpRequest httpRequest = HttpRequest.newBuilder()
.setHeader("User-Agent",
server.getVersion().getName() + "/" + server.getVersion().getVersion())
.uri(URI.create(url))
.build(); .build();
final HttpClient httpClient = server.createHttpClient(); final HttpClient httpClient = server.createHttpClient();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.whenCompleteAsync((response, throwable) -> { .whenCompleteAsync((response, throwable) -> {
@@ -240,31 +242,53 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
return; return;
} }
if (response.statusCode() == 200) { try {
final GameProfile profile = GENERAL_GSON.fromJson(response.body(), if (response.statusCode() == 200) {
GameProfile.class); final AuthResponse authResponse = GENERAL_GSON.fromJson(response.body(), AuthResponse.class);
// Not so fast, now we verify the public key for 1.19.1+ if (authResponse.getCode() != 0) {
if (inbound.getIdentifiedKey() != null inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
&& inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2 logger.error("Error authenticating {} with netease", login.getUsername());
&& inbound.getIdentifiedKey() instanceof final IdentifiedKeyImpl key) { } else {
if (!key.internalAddHolder(profile.getId())) { AuthResponse.ResponseEntity entity = authResponse.getEntity();
inbound.disconnect( if (entity.getName() == null || entity.getName().isEmpty()) {
Component.translatable("multiplayer.disconnect.invalid_public_key")); entity.setName(login.getUsername());
}
if (entity.getProperties() == null) {
entity.setProperties(new ArrayList<>());
}
if (entity.getId() == null) {
inbound.disconnect(
Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)
);
} else {
GameProfile profile = new GameProfile(entity.getId(), entity.getName(), entity.getProperties());
// Not so fast, now we verify the public key for 1.19.1+
if (inbound.getIdentifiedKey() != null
&& inbound.getIdentifiedKey().getKeyRevision() == IdentifiedKey.Revision.LINKED_V2
&& inbound.getIdentifiedKey() instanceof final IdentifiedKeyImpl key) {
if (!key.internalAddHolder(profile.getId())) {
inbound.disconnect(
Component.translatable("multiplayer.disconnect.invalid_public_key"));
}
}
// All went well, initialize the session.
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, profile, true));
}
} }
} else if (response.statusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(
Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED));
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
response.statusCode(), login.getUsername(), playerIp);
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
} }
// All went well, initialize the session. } catch (Exception e) {
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, logger.error("Got an unexpected error", e);
new AuthSessionHandler(server, inbound, profile, true));
} else if (response.statusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect(
Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED));
} else {
// Something else went wrong
logger.error(
"Got an unexpected error code {} whilst contacting Mojang to log in {} ({})",
response.statusCode(), login.getUsername(), playerIp);
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
} }
}, mcConnection.eventLoop()) }, mcConnection.eventLoop())
.thenRun(() -> { .thenRun(() -> {

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

@@ -30,10 +30,12 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.server.VelocityRegisteredServer;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/** /**
* Common utilities for handling server list ping results. * Common utilities for handling server list ping results.
@@ -51,11 +53,27 @@ public class ServerListPingHandler {
version = ProtocolVersion.MAXIMUM_VERSION; version = ProtocolVersion.MAXIMUM_VERSION;
} }
VelocityConfiguration configuration = server.getConfiguration(); VelocityConfiguration configuration = server.getConfiguration();
List<ServerPing.SamplePlayer> samplePlayers;
if (configuration.getSamplePlayersInPing()) {
List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream()
.map(p -> {
if (p.getPlayerSettings().isClientListingAllowed()) {
return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId());
} else {
return ServerPing.SamplePlayer.ANONYMOUS;
}
})
.collect(Collectors.toList());
Collections.shuffle(unshuffledPlayers);
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount()));
} else {
samplePlayers = ImmutableList.of();
}
return new ServerPing( return new ServerPing(
new ServerPing.Version(version.getProtocol(), new ServerPing.Version(version.getProtocol(),
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING), "Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
ImmutableList.of()), samplePlayers),
configuration.getMotd(), configuration.getMotd(),
configuration.getFavicon().orElse(null), configuration.getFavicon().orElse(null),
configuration.isAnnounceForge() ? ModInfo.DEFAULT : null configuration.isAnnounceForge() ? ModInfo.DEFAULT : null
@@ -63,7 +81,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 +91,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 +173,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

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.netease;
import com.velocitypowered.api.util.GameProfile;
import java.util.List;
/**
* netease auth response.
*/
public class AuthResponse {
private Integer code = 0;
private String message;
private String details;
private ResponseEntity entity;
/**
* default constructor.
*
* @param code -
* @param message -
* @param details -
* @param entity the game profile
*/
public AuthResponse(Integer code, String message, String details, ResponseEntity entity) {
this.code = code;
this.message = message;
this.details = details;
this.entity = entity;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
public ResponseEntity getEntity() {
return entity;
}
public void setEntity(ResponseEntity entity) {
this.entity = entity;
}
/**
* netease auth response entity.
*/
public static class ResponseEntity {
private String id;
private String name;
private List<GameProfile.Property> properties;
/**
* default constructor.
*
* @param id -
* @param name -
* @param properties -
*/
public ResponseEntity(String id, String name, List<GameProfile.Property> properties) {
this.id = id;
this.name = name;
this.properties = properties;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<GameProfile.Property> getProperties() {
return properties;
}
public void setProperties(List<GameProfile.Property> properties) {
this.properties = properties;
}
@Override
public String toString() {
return "ResponseEntity{"
+ "id='" + id + '\''
+ ", name='" + name + '\''
+ ", properties=" + properties
+ '}';
}
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.netease;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import org.jetbrains.annotations.NotNull;
/**
* netease data manager.
*/
public class NeteaseDataManager {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
private static Config config;
/**
* init netease data manager.
*
* @throws IOException -
*/
public static void init() throws IOException {
File file = new File("netease.json");
if (file.exists()) {
String s = Files.readString(file.toPath(), StandardCharsets.UTF_8);
config = GSON.fromJson(s, Config.class);
} else {
config = new Config();
Files.writeString(file.toPath(), GSON.toJson(config), StandardCharsets.UTF_8);
}
}
public static Config getConfig() {
return config;
}
/**
* netease config.
*/
public static class Config {
/**
* 认证链接
*
* <p>正式环境http://x19authserver.nie.netease.com/check
*
* <p>测试环境http://x19authexpr.nie.netease.com/check
*
* <p>1.20版本请使用以下接口:
*
* <p>正式环境https://x19apigatewayobt.nie.netease.com/pcauth/check
*
* <p>测试环境https://x19apigatewayexpr.nie.netease.com/pcauth/check
*
* <p>另有外网测试认证接口:
*
* <p>http://x19authtest.nie.netease.com/check
*
* <p>对应接入test版bc认证通常情况下不使用需要启用时会另行沟通
*/
@SuppressWarnings("JavadocLinkAsPlainText")
@NotNull
private String authUrl = "http://192.168.0.100:9999/check";
/**
* 网络服游戏 id在开发者平台中可以查看.
*/
@NotNull
private String gameId = "12345678901234567";
public @NotNull String getAuthUrl() {
return authUrl;
}
public void setAuthUrl(@NotNull String authUrl) {
this.authUrl = authUrl;
}
public @NotNull String getGameId() {
return gameId;
}
public void setGameId(@NotNull String gameId) {
this.gameId = gameId;
}
}
}

View File

@@ -51,7 +51,7 @@ public class BackendChannelInitializer extends ChannelInitializer<Channel> {
@Override @Override
protected void initChannel(Channel ch) { protected void initChannel(Channel ch) {
ch.pipeline() ch.pipeline()
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.CLIENTBOUND))
.addLast(READ_TIMEOUT, .addLast(READ_TIMEOUT,
new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(),
TimeUnit.MILLISECONDS)) TimeUnit.MILLISECONDS))

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()) {
.addListener((ChannelFutureListener) future -> { // We don't need a boss group, since each worker will bind to the socket
final Channel channel = future.channel(); bootstrap.option(UnixChannelOption.SO_REUSEPORT, true)
if (future.isSuccess()) { .group(this.workerGroup);
this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT)); } else {
bootstrap.group(this.bossGroup, this.workerGroup);
}
// Warn people with console access that HAProxy is in use, see PR: #1436 final int binds = server.getConfiguration().isEnableReusePort()
if (this.server.getConfiguration().isProxyProtocol()) { ? ((MultithreadEventExecutorGroup) this.workerGroup).executorCount() : 1;
LOGGER.warn("Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", channel.localAddress());
for (int bind = 0; bind < binds; bind++) {
// Wait for each bind to open. If we encounter any errors, don't try to bind again.
int finalBind = bind;
ChannelFuture f = bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT));
LOGGER.info("Listening on {}", channel.localAddress());
if (finalBind == 0) {
// Warn people with console access that HAProxy is in use, see PR: #1436
if (this.server.getConfiguration().isProxyProtocol()) {
LOGGER.warn(
"Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.",
channel.localAddress());
}
// Fire the proxy bound event after the socket is bound
server.getEventManager().fireAndForget(
new ListenerBoundEvent(address, ListenerType.MINECRAFT));
}
} else {
LOGGER.error("Can't bind to {}", address, future.cause());
} }
});
f.syncUninterruptibly();
LOGGER.info("Listening on {}", channel.localAddress()); if (!f.isSuccess()) {
break;
// Fire the proxy bound event after the socket is bound }
server.getEventManager().fireAndForget( }
new ListenerBoundEvent(address, ListenerType.MINECRAFT));
} else {
LOGGER.error("Can't bind to {}", address, future.cause());
}
});
} }
/** /**
@@ -181,17 +209,20 @@ 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();
Channel serverChannel = endpoint.getChannel(); for (Endpoint endpoint : endpoints) {
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(); }
} }
/** /**
@@ -200,24 +231,28 @@ 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();
LOGGER.info("Closing endpoint {}", address); for (Endpoint endpoint : endpoints) {
if (interrupt) { LOGGER.info("Closing endpoint {}", address);
try { if (interrupt) {
endpoint.getChannel().close().sync(); try {
} catch (final InterruptedException e) { endpoint.getChannel().close().sync();
LOGGER.info("Interrupted whilst closing endpoint", e); } catch (final InterruptedException e) {
Thread.currentThread().interrupt(); LOGGER.info("Interrupted whilst closing endpoint", e);
Thread.currentThread().interrupt();
}
} else {
endpoint.getChannel().close().syncUninterruptibly();
} }
} else {
endpoint.getChannel().close().syncUninterruptibly();
} }
} }
this.endpoints.clear(); this.endpoints.clear();

View File

@@ -58,7 +58,7 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
protected void initChannel(final Channel ch) { protected void initChannel(final Channel ch) {
ch.pipeline() ch.pipeline()
.addLast(LEGACY_PING_DECODER, new LegacyPingDecoder()) .addLast(LEGACY_PING_DECODER, new LegacyPingDecoder())
.addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.SERVERBOUND))
.addLast(READ_TIMEOUT, .addLast(READ_TIMEOUT,
new ReadTimeoutHandler(this.server.getConfiguration().getReadTimeout(), new ReadTimeoutHandler(this.server.getConfiguration().getReadTimeout(),
TimeUnit.MILLISECONDS)) TimeUnit.MILLISECONDS))

View File

@@ -20,25 +20,32 @@ package com.velocitypowered.proxy.network;
import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory; import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory;
import io.netty.channel.ChannelFactory; import io.netty.channel.ChannelFactory;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.IoHandlerFactory;
import io.netty.channel.MultiThreadIoEventLoopGroup;
import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollDatagramChannel; import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollIoHandler;
import io.netty.channel.epoll.EpollServerSocketChannel; import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueue; import io.netty.channel.kqueue.KQueue;
import io.netty.channel.kqueue.KQueueDatagramChannel; import io.netty.channel.kqueue.KQueueDatagramChannel;
import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.kqueue.KQueueIoHandler;
import io.netty.channel.kqueue.KQueueServerSocketChannel; import io.netty.channel.kqueue.KQueueServerSocketChannel;
import io.netty.channel.kqueue.KQueueSocketChannel; import io.netty.channel.kqueue.KQueueSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.channel.uring.IoUring;
import io.netty.channel.uring.IoUringDatagramChannel;
import io.netty.channel.uring.IoUringIoHandler;
import io.netty.channel.uring.IoUringServerSocketChannel;
import io.netty.channel.uring.IoUringSocketChannel;
import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadFactory;
import java.util.function.BiFunction; import java.util.function.Supplier;
/** /**
* Enumerates the supported transports for Velocity. * Enumerates the supported transports for Velocity.
@@ -47,32 +54,36 @@ public enum TransportType {
NIO("NIO", NioServerSocketChannel::new, NIO("NIO", NioServerSocketChannel::new,
NioSocketChannel::new, NioSocketChannel::new,
NioDatagramChannel::new, NioDatagramChannel::new,
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type))), NioIoHandler::newFactory),
EPOLL("epoll", EpollServerSocketChannel::new, EPOLL("epoll", EpollServerSocketChannel::new,
EpollSocketChannel::new, EpollSocketChannel::new,
EpollDatagramChannel::new, EpollDatagramChannel::new,
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type))), EpollIoHandler::newFactory),
KQUEUE("kqueue", KQueueServerSocketChannel::new, KQUEUE("kqueue", KQueueServerSocketChannel::new,
KQueueSocketChannel::new, KQueueSocketChannel::new,
KQueueDatagramChannel::new, KQueueDatagramChannel::new,
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type))); KQueueIoHandler::newFactory),
IO_URING("io_uring", IoUringServerSocketChannel::new,
IoUringSocketChannel::new,
IoUringDatagramChannel::new,
IoUringIoHandler::newFactory);
final String name; final String name;
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory; final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory;
final ChannelFactory<? extends SocketChannel> socketChannelFactory; final ChannelFactory<? extends SocketChannel> socketChannelFactory;
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory; final ChannelFactory<? extends DatagramChannel> datagramChannelFactory;
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory; final Supplier<IoHandlerFactory> ioHandlerFactorySupplier;
TransportType(final String name, TransportType(final String name,
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory, final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory,
final ChannelFactory<? extends SocketChannel> socketChannelFactory, final ChannelFactory<? extends SocketChannel> socketChannelFactory,
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory, final ChannelFactory<? extends DatagramChannel> datagramChannelFactory,
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory) { final Supplier<IoHandlerFactory> ioHandlerFactorySupplier) {
this.name = name; this.name = name;
this.serverSocketChannelFactory = serverSocketChannelFactory; this.serverSocketChannelFactory = serverSocketChannelFactory;
this.socketChannelFactory = socketChannelFactory; this.socketChannelFactory = socketChannelFactory;
this.datagramChannelFactory = datagramChannelFactory; this.datagramChannelFactory = datagramChannelFactory;
this.eventLoopGroupFactory = eventLoopGroupFactory; this.ioHandlerFactorySupplier = ioHandlerFactorySupplier;
} }
@Override @Override
@@ -80,8 +91,15 @@ public enum TransportType {
return this.name; return this.name;
} }
/**
* Creates a new event loop group for the given type.
*
* @param type the type of event loop group to create
* @return the event loop group
*/
public EventLoopGroup createEventLoopGroup(final Type type) { public EventLoopGroup createEventLoopGroup(final Type type) {
return this.eventLoopGroupFactory.apply(this.name, type); return new MultiThreadIoEventLoopGroup(
0, createThreadFactory(this.name, type), this.ioHandlerFactorySupplier.get());
} }
private static ThreadFactory createThreadFactory(final String name, final Type type) { private static ThreadFactory createThreadFactory(final String name, final Type type) {
@@ -98,6 +116,10 @@ public enum TransportType {
return NIO; return NIO;
} }
if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) {
return IO_URING;
}
if (Epoll.isAvailable()) { if (Epoll.isAvailable()) {
return EPOLL; return EPOLL;
} }

View File

@@ -47,7 +47,7 @@ import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.json.JSONOptions; import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer; import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer;
import net.kyori.option.OptionState; import net.kyori.option.OptionSchema;
/** /**
* Utilities for writing and reading data in the Minecraft protocol. * Utilities for writing and reading data in the Minecraft protocol.
@@ -60,14 +60,17 @@ public enum ProtocolUtils {
.downsampleColors() .downsampleColors()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionState.optionState() OptionSchema.globalSchema().stateBuilder()
// before 1.16 // before 1.16
.value(JSONOptions.EMIT_RGB, Boolean.FALSE) .value(JSONOptions.EMIT_RGB, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.LEGACY_ONLY) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.VALUE_FIELD)
.value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.CAMEL_CASE)
// before 1.20.3 // before 1.20.3
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build() .build()
) )
.build(); .build();
@@ -75,14 +78,37 @@ public enum ProtocolUtils {
GsonComponentSerializer.builder() GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionState.optionState() OptionSchema.globalSchema().stateBuilder()
// after 1.16 // after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE) .value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.MODERN_ONLY) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
.value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.CAMEL_CASE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, true)
// before 1.20.3 // before 1.20.3
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build()
)
.build();
private static final GsonComponentSerializer PRE_1_21_5_SERIALIZER =
GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionSchema.globalSchema().stateBuilder()
// after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
.value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.CAMEL_CASE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, true)
// after 1.20.3
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build() .build()
) )
.build(); .build();
@@ -90,14 +116,18 @@ public enum ProtocolUtils {
GsonComponentSerializer.builder() GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionState.optionState() OptionSchema.globalSchema().stateBuilder()
// after 1.16 // after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE) .value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.MODERN_ONLY) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.SNAKE_CASE)
.value(JSONOptions.EMIT_CLICK_EVENT_TYPE, JSONOptions.ClickEventValueMode.SNAKE_CASE)
// after 1.20.3 // after 1.20.3
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE) .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE)
// after 1.21.5
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.FALSE)
.build() .build()
) )
.build(); .build();
@@ -713,9 +743,12 @@ public enum ProtocolUtils {
* @return the appropriate {@link GsonComponentSerializer} * @return the appropriate {@link GsonComponentSerializer}
*/ */
public static GsonComponentSerializer getJsonChatSerializer(ProtocolVersion version) { public static GsonComponentSerializer getJsonChatSerializer(ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_5)) {
return MODERN_SERIALIZER; return MODERN_SERIALIZER;
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
return PRE_1_21_5_SERIALIZER;
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
return PRE_1_20_3_SERIALIZER; return PRE_1_20_3_SERIALIZER;
} }

View File

@@ -39,6 +39,8 @@ 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_21_6;
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;
@@ -255,7 +257,8 @@ public enum StateRegistry {
map(0x09, MINECRAFT_1_19_4, false), map(0x09, MINECRAFT_1_19_4, false),
map(0x0A, MINECRAFT_1_20_2, false), map(0x0A, MINECRAFT_1_20_2, false),
map(0x0B, MINECRAFT_1_20_5, false), map(0x0B, MINECRAFT_1_20_5, false),
map(0x0D, MINECRAFT_1_21_2, false)); map(0x0D, MINECRAFT_1_21_2, false),
map(0x0E, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
LegacyChatPacket.class, LegacyChatPacket.class,
LegacyChatPacket::new, LegacyChatPacket::new,
@@ -268,7 +271,8 @@ public enum StateRegistry {
ChatAcknowledgementPacket.class, ChatAcknowledgementPacket.class,
ChatAcknowledgementPacket::new, ChatAcknowledgementPacket::new,
map(0x03, MINECRAFT_1_19_3, false), map(0x03, MINECRAFT_1_19_3, false),
map(0x04, MINECRAFT_1_21_2, false)); map(0x04, MINECRAFT_1_21_2, false),
map(0x05, MINECRAFT_1_21_6, false));
serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new, serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new,
map(0x03, MINECRAFT_1_19, false), map(0x03, MINECRAFT_1_19, false),
map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false));
@@ -278,16 +282,19 @@ public enum StateRegistry {
serverbound.register(SessionPlayerCommandPacket.class, SessionPlayerCommandPacket::new, serverbound.register(SessionPlayerCommandPacket.class, SessionPlayerCommandPacket::new,
map(0x04, MINECRAFT_1_19_3, false), map(0x04, MINECRAFT_1_19_3, false),
map(0x05, MINECRAFT_1_20_5, false), map(0x05, MINECRAFT_1_20_5, false),
map(0x06, MINECRAFT_1_21_2, false)); map(0x06, MINECRAFT_1_21_2, false),
map(0x07, MINECRAFT_1_21_6, false));
serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new, serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new,
map(0x04, MINECRAFT_1_20_5, false), map(0x04, MINECRAFT_1_20_5, false),
map(0x05, MINECRAFT_1_21_2, false)); map(0x05, MINECRAFT_1_21_2, false),
map(0x06, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
SessionPlayerChatPacket.class, SessionPlayerChatPacket.class,
SessionPlayerChatPacket::new, SessionPlayerChatPacket::new,
map(0x05, MINECRAFT_1_19_3, false), map(0x05, MINECRAFT_1_19_3, false),
map(0x06, MINECRAFT_1_20_5, false), map(0x06, MINECRAFT_1_20_5, false),
map(0x07, MINECRAFT_1_21_2, false)); map(0x07, MINECRAFT_1_21_2, false),
map(0x08, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
ClientSettingsPacket.class, ClientSettingsPacket.class,
ClientSettingsPacket::new, ClientSettingsPacket::new,
@@ -302,11 +309,13 @@ public enum StateRegistry {
map(0x08, MINECRAFT_1_19_4, false), map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, false), map(0x09, MINECRAFT_1_20_2, false),
map(0x0A, MINECRAFT_1_20_5, false), map(0x0A, MINECRAFT_1_20_5, false),
map(0x0C, MINECRAFT_1_21_2, false)); map(0x0C, MINECRAFT_1_21_2, false),
map(0x0D, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new,
map(0x11, MINECRAFT_1_20_5, false), map(0x11, MINECRAFT_1_20_5, false),
map(0x13, MINECRAFT_1_21_2, false)); map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -324,7 +333,8 @@ public enum StateRegistry {
map(0x0F, MINECRAFT_1_20_2, false), map(0x0F, MINECRAFT_1_20_2, false),
map(0x10, MINECRAFT_1_20_3, false), map(0x10, MINECRAFT_1_20_3, false),
map(0x12, MINECRAFT_1_20_5, false), map(0x12, MINECRAFT_1_20_5, false),
map(0x14, MINECRAFT_1_21_2, false)); map(0x14, MINECRAFT_1_21_2, false),
map(0x15, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
KeepAlivePacket.class, KeepAlivePacket.class,
KeepAlivePacket::new, KeepAlivePacket::new,
@@ -343,7 +353,8 @@ public enum StateRegistry {
map(0x14, MINECRAFT_1_20_2, false), map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, false), map(0x15, MINECRAFT_1_20_3, false),
map(0x18, MINECRAFT_1_20_5, false), map(0x18, MINECRAFT_1_20_5, false),
map(0x1A, MINECRAFT_1_21_2, false)); map(0x1A, MINECRAFT_1_21_2, false),
map(0x1B, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
ResourcePackResponsePacket.class, ResourcePackResponsePacket.class,
ResourcePackResponsePacket::new, ResourcePackResponsePacket::new,
@@ -360,12 +371,14 @@ public enum StateRegistry {
map(0x28, MINECRAFT_1_20_3, false), map(0x28, MINECRAFT_1_20_3, false),
map(0x2B, MINECRAFT_1_20_5, false), map(0x2B, MINECRAFT_1_20_5, false),
map(0x2D, MINECRAFT_1_21_2, false), map(0x2D, MINECRAFT_1_21_2, false),
map(0x2F, MINECRAFT_1_21_4, false)); map(0x2F, MINECRAFT_1_21_4, false),
map(0x30, MINECRAFT_1_21_6, false));
serverbound.register( serverbound.register(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, false), map(0x0B, MINECRAFT_1_20_2, false),
map(0x0C, MINECRAFT_1_20_5, false), map(0x0C, MINECRAFT_1_20_5, false),
map(0x0E, MINECRAFT_1_21_2, false)); map(0x0E, MINECRAFT_1_21_2, false),
map(0x0F, MINECRAFT_1_21_6, false));
clientbound.register( clientbound.register(
BossBarPacket.class, BossBarPacket.class,
@@ -376,7 +389,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,
@@ -397,7 +411,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,
@@ -409,10 +424,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,
@@ -429,7 +446,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),
@@ -446,7 +464,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,
@@ -464,7 +483,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,
@@ -482,7 +502,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,
@@ -503,13 +524,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,
@@ -530,7 +553,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,
@@ -552,7 +576,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,
@@ -573,7 +598,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,
@@ -585,7 +611,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,
@@ -597,7 +624,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,
@@ -609,7 +637,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,
@@ -617,7 +646,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,
@@ -637,7 +667,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,
@@ -645,11 +676,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,
@@ -660,7 +693,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,
@@ -668,7 +702,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,
@@ -679,14 +714,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

@@ -39,6 +39,7 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int UNCOMPRESSED_CAP = private static final int UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap") Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
private int threshold; private int threshold;
private final VelocityCompressor compressor; private final VelocityCompressor compressor;
@@ -52,6 +53,11 @@ 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) {
if (!SKIP_COMPRESSION_VALIDATION) {
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

@@ -135,7 +135,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
private String getExtraConnectionDetail(int packetId) { private String getExtraConnectionDetail(int packetId) {
return "Direction " + direction + " Protocol " + registry.version + " State " + state return "Direction " + direction + " Protocol " + registry.version + " State " + state
+ " ID " + Integer.toHexString(packetId); + " ID 0x" + Integer.toHexString(packetId);
} }
public void setProtocolVersion(ProtocolVersion protocolVersion) { public void setProtocolVersion(ProtocolVersion protocolVersion) {

View File

@@ -19,21 +19,51 @@ package com.velocitypowered.proxy.protocol.netty;
import static io.netty.util.ByteProcessor.FIND_NON_NUL; import static io.netty.util.ByteProcessor.FIND_NON_NUL;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.util.except.QuietDecoderException; import com.velocitypowered.proxy.util.except.QuietDecoderException;
import com.velocitypowered.proxy.util.except.QuietRuntimeException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/** /**
* Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding. * Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding.
*/ */
public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
private static final Logger LOGGER = LogManager.getLogger(MinecraftVarintFrameDecoder.class);
private static final QuietRuntimeException FRAME_DECODER_FAILED =
new QuietRuntimeException("A packet frame decoder failed. For more information, launch "
+ "Velocity with -Dvelocity.packet-decode-logging=true to see more.");
private static final QuietDecoderException BAD_PACKET_LENGTH = private static final QuietDecoderException BAD_PACKET_LENGTH =
new QuietDecoderException("Bad packet length"); new QuietDecoderException("Bad packet length");
private static final QuietDecoderException VARINT_TOO_BIG = private static final QuietDecoderException VARINT_TOO_BIG =
new QuietDecoderException("VarInt too big"); new QuietDecoderException("VarInt too big");
private static final QuietDecoderException UNKNOWN_PACKET =
new QuietDecoderException("Unknown packet");
private final ProtocolUtils.Direction direction;
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
private StateRegistry state;
/**
* Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}.
*
* @param direction the direction from which we decode from
*/
public MinecraftVarintFrameDecoder(ProtocolUtils.Direction direction) {
this.direction = direction;
this.registry = StateRegistry.HANDSHAKE.getProtocolRegistry(
direction, ProtocolVersion.MINIMUM_VERSION);
this.state = StateRegistry.HANDSHAKE;
}
@Override @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
@@ -62,6 +92,43 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
throw BAD_PACKET_LENGTH; throw BAD_PACKET_LENGTH;
} }
if (length > 0) {
if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) {
StateRegistry.PacketRegistry.ProtocolRegistry registry =
state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION);
final int index = in.readerIndex();
final int packetId = readRawVarInt21(in);
// Index hasn't changed, we've read nothing
if (index == in.readerIndex()) {
in.resetReaderIndex();
return;
}
final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
MinecraftPacket packet = registry.createPacket(packetId);
// We handle every packet in this phase, if you said something we don't know, something is really wrong
if (packet == null) {
throw UNKNOWN_PACKET;
}
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse
// the packet if needed, so, we'll take advantage of the existing methods
int expectedMinLen = packet.expectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.expectedMaxLength(in, direction, registry.version);
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
}
if (payloadLength < expectedMinLen) {
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes());
}
in.readerIndex(index);
}
}
// note that zero-length packets are ignored // note that zero-length packets are ignored
if (length > 0) { if (length > 0) {
if (in.readableBytes() < length) { if (in.readableBytes() < length) {
@@ -72,6 +139,16 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
} }
} }
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (MinecraftDecoder.DEBUG) {
LOGGER.atWarn()
.withThrowable(cause)
.log("Exception caught while decoding frame for {}", ctx.channel().remoteAddress());
}
super.exceptionCaught(ctx, cause);
}
/** /**
* Reads a VarInt from the buffer of up to 21 bits in size. * Reads a VarInt from the buffer of up to 21 bits in size.
* *
@@ -141,4 +218,26 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
} }
return result | (tmp & 0x7F) << 14; return result | (tmp & 0x7F) << 14;
} }
private Exception handleOverflow(MinecraftPacket packet, int expected, int actual) {
if (MinecraftDecoder.DEBUG) {
return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too "
+ "big (expected " + expected + " bytes, got " + actual + " bytes)");
} else {
return FRAME_DECODER_FAILED;
}
}
private Exception handleUnderflow(MinecraftPacket packet, int expected, int actual) {
if (MinecraftDecoder.DEBUG) {
return new CorruptedFrameException("Packet sent for " + packet.getClass() + " was too "
+ "small (expected " + expected + " bytes, got " + actual + " bytes)");
} else {
return FRAME_DECODER_FAILED;
}
}
public void setState(StateRegistry stateRegistry) {
this.state = stateRegistry;
}
} }

View File

@@ -50,12 +50,14 @@ import java.util.Deque;
import java.util.Iterator; import java.util.Iterator;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
public class AvailableCommandsPacket implements MinecraftPacket { public class AvailableCommandsPacket implements MinecraftPacket {
private static final Command<CommandSource> PLACEHOLDER_COMMAND = source -> 0; private static final Command<CommandSource> PLACEHOLDER_COMMAND = source -> 0;
private static final Predicate<CommandSource> PLACEHOLDER_REQUIREMENT = source -> true;
private static final byte NODE_TYPE_ROOT = 0x00; private static final byte NODE_TYPE_ROOT = 0x00;
private static final byte NODE_TYPE_LITERAL = 0x01; private static final byte NODE_TYPE_LITERAL = 0x01;
@@ -65,6 +67,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
private static final byte FLAG_EXECUTABLE = 0x04; private static final byte FLAG_EXECUTABLE = 0x04;
private static final byte FLAG_IS_REDIRECT = 0x08; private static final byte FLAG_IS_REDIRECT = 0x08;
private static final byte FLAG_HAS_SUGGESTIONS = 0x10; private static final byte FLAG_HAS_SUGGESTIONS = 0x10;
private static final byte FLAG_IS_RESTRICTED = 0x20;
private @MonotonicNonNull RootCommandNode<CommandSource> rootNode; private @MonotonicNonNull RootCommandNode<CommandSource> rootNode;
@@ -146,6 +149,9 @@ public class AvailableCommandsPacket implements MinecraftPacket {
if (node.getCommand() != null) { if (node.getCommand() != null) {
flags |= FLAG_EXECUTABLE; flags |= FLAG_EXECUTABLE;
} }
if (node.getRequirement() == PLACEHOLDER_REQUIREMENT) {
flags |= FLAG_IS_RESTRICTED;
}
if (node instanceof LiteralCommandNode<?>) { if (node instanceof LiteralCommandNode<?>) {
flags |= NODE_TYPE_LITERAL; flags |= NODE_TYPE_LITERAL;
@@ -289,6 +295,11 @@ public class AvailableCommandsPacket implements MinecraftPacket {
args.executes(PLACEHOLDER_COMMAND); args.executes(PLACEHOLDER_COMMAND);
} }
// If restricted, add empty requirement
if ((flags & FLAG_IS_RESTRICTED) != 0) {
args.requires(PLACEHOLDER_REQUIREMENT);
}
this.built = args.build(); this.built = args.build();
} }
} }

View File

@@ -106,4 +106,16 @@ public class HandshakePacket implements MinecraftPacket {
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
} }
@Override
public int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 7;
}
@Override
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) {
return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3);
}
} }

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,8 @@ 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.api.network.ProtocolVersion.MINECRAFT_1_21_6;
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;
@@ -163,6 +165,7 @@ public class ArgumentPropertyRegistry {
return i; return i;
} }
} }
throw new IllegalArgumentException("Argument type identifier " + id + " unknown.");
} else { } else {
String identifier = ProtocolUtils.readString(buf); String identifier = ProtocolUtils.readString(buf);
for (ArgumentIdentifier i : byIdentifier.keySet()) { for (ArgumentIdentifier i : byIdentifier.keySet()) {
@@ -206,65 +209,79 @@ public class ArgumentPropertyRegistry {
empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14))); empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14)));
empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15))); empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15)));
empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16))); empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16)));
empty(id("minecraft:component", mapSet(MINECRAFT_1_19, 17))); empty(id("minecraft:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17)));
empty(id("minecraft:style", mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3 empty(id("minecraft:style", mapSet(MINECRAFT_1_21_6, 19), mapSet(MINECRAFT_1_20_3, 18))); // added 1.20.3
empty(id("minecraft:message", mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18))); empty(id("minecraft:message", mapSet(MINECRAFT_1_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18)));
empty(id("minecraft:nbt_compound_tag", mapSet(MINECRAFT_1_20_3, 20), mapSet(MINECRAFT_1_19, 19))); // added in 1.14 empty(id("minecraft:nbt_compound_tag", mapSet(MINECRAFT_1_21_6, 21), mapSet(MINECRAFT_1_20_3, 20), mapSet(MINECRAFT_1_19, 19))); // added in 1.14
empty(id("minecraft:nbt_tag", mapSet(MINECRAFT_1_20_3, 21), mapSet(MINECRAFT_1_19, 20))); // added in 1.14 empty(id("minecraft:nbt_tag", mapSet(MINECRAFT_1_21_6, 22), mapSet(MINECRAFT_1_20_3, 21), mapSet(MINECRAFT_1_19, 20))); // added in 1.14
empty(id("minecraft:nbt_path", mapSet(MINECRAFT_1_20_3, 22), mapSet(MINECRAFT_1_19, 21))); empty(id("minecraft:nbt_path", mapSet(MINECRAFT_1_21_6, 23), mapSet(MINECRAFT_1_20_3, 22), mapSet(MINECRAFT_1_19, 21)));
empty(id("minecraft:objective", mapSet(MINECRAFT_1_20_3, 23), mapSet(MINECRAFT_1_19, 22))); empty(id("minecraft:objective", mapSet(MINECRAFT_1_21_6, 24), mapSet(MINECRAFT_1_20_3, 23), mapSet(MINECRAFT_1_19, 22)));
empty(id("minecraft:objective_criteria", mapSet(MINECRAFT_1_20_3, 24), mapSet(MINECRAFT_1_19, 23))); empty(id("minecraft:objective_criteria", mapSet(MINECRAFT_1_21_6, 25), mapSet(MINECRAFT_1_20_3, 24), mapSet(MINECRAFT_1_19, 23)));
empty(id("minecraft:operation", mapSet(MINECRAFT_1_20_3, 25), mapSet(MINECRAFT_1_19, 24))); empty(id("minecraft:operation", mapSet(MINECRAFT_1_21_6, 26), mapSet(MINECRAFT_1_20_3, 25), mapSet(MINECRAFT_1_19, 24)));
empty(id("minecraft:particle", mapSet(MINECRAFT_1_20_3, 26), mapSet(MINECRAFT_1_19, 25))); empty(id("minecraft:particle", mapSet(MINECRAFT_1_21_6, 27), mapSet(MINECRAFT_1_20_3, 26), mapSet(MINECRAFT_1_19, 25)));
empty(id("minecraft:angle", mapSet(MINECRAFT_1_20_3, 27), mapSet(MINECRAFT_1_19, 26))); // added in 1.16.2 empty(id("minecraft:angle", mapSet(MINECRAFT_1_21_6, 28), mapSet(MINECRAFT_1_20_3, 27), mapSet(MINECRAFT_1_19, 26))); // added in 1.16.2
empty(id("minecraft:rotation", mapSet(MINECRAFT_1_20_3, 28), mapSet(MINECRAFT_1_19, 27))); empty(id("minecraft:rotation", mapSet(MINECRAFT_1_21_6, 29), mapSet(MINECRAFT_1_20_3, 28), mapSet(MINECRAFT_1_19, 27)));
empty(id("minecraft:scoreboard_slot", mapSet(MINECRAFT_1_20_3, 29), mapSet(MINECRAFT_1_19, 28))); empty(id("minecraft:scoreboard_slot", mapSet(MINECRAFT_1_21_6, 30), mapSet(MINECRAFT_1_20_3, 29), mapSet(MINECRAFT_1_19, 28)));
empty(id("minecraft:score_holder", mapSet(MINECRAFT_1_20_3, 30), mapSet(MINECRAFT_1_19, 29)), ByteArgumentPropertySerializer.BYTE); empty(id("minecraft:score_holder", mapSet(MINECRAFT_1_21_6, 31), mapSet(MINECRAFT_1_20_3, 30), mapSet(MINECRAFT_1_19, 29)),
empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_20_3, 31), mapSet(MINECRAFT_1_19, 30))); ByteArgumentPropertySerializer.BYTE);
empty(id("minecraft:team", mapSet(MINECRAFT_1_20_3, 32), mapSet(MINECRAFT_1_19, 31))); empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_21_6, 32), mapSet(MINECRAFT_1_20_3, 31), mapSet(MINECRAFT_1_19, 30)));
empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_20_3, 33), mapSet(MINECRAFT_1_19, 32))); empty(id("minecraft:team", mapSet(MINECRAFT_1_21_6, 33), mapSet(MINECRAFT_1_20_3, 32), mapSet(MINECRAFT_1_19, 31)));
empty(id("minecraft:item_slots", mapSet(MINECRAFT_1_20_5, 34))); // added 1.20.5 empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_21_6, 34), mapSet(MINECRAFT_1_20_3, 33), mapSet(MINECRAFT_1_19, 32)));
empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_20_5, 35), mapSet(MINECRAFT_1_20_3, 34), mapSet(MINECRAFT_1_19, 33))); empty(id("minecraft:item_slots", mapSet(MINECRAFT_1_21_6, 35), mapSet(MINECRAFT_1_20_5, 34))); // added 1.20.5
empty(id("minecraft:resource_location", mapSet(MINECRAFT_1_21_6, 36), mapSet(MINECRAFT_1_20_5, 35), mapSet(MINECRAFT_1_20_3, 34),
mapSet(MINECRAFT_1_19, 33)));
empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 34))); empty(id("minecraft:mob_effect", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 34)));
empty(id("minecraft:function", mapSet(MINECRAFT_1_20_5, 36), mapSet(MINECRAFT_1_20_3, 35), mapSet(MINECRAFT_1_19_3, 34), empty(id("minecraft:function", mapSet(MINECRAFT_1_21_6, 37), mapSet(MINECRAFT_1_20_5, 36), mapSet(MINECRAFT_1_20_3, 35),
mapSet(MINECRAFT_1_19, 35))); mapSet(MINECRAFT_1_19_3, 34), mapSet(MINECRAFT_1_19, 35)));
empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_20_5, 37), mapSet(MINECRAFT_1_20_3, 36), mapSet(MINECRAFT_1_19_3, 35), empty(id("minecraft:entity_anchor", mapSet(MINECRAFT_1_21_6, 38), mapSet(MINECRAFT_1_20_5, 37), mapSet(MINECRAFT_1_20_3, 36),
mapSet(MINECRAFT_1_19, 36))); mapSet(MINECRAFT_1_19_3, 35), mapSet(MINECRAFT_1_19, 36)));
empty(id("minecraft:int_range", mapSet(MINECRAFT_1_20_5, 38), mapSet(MINECRAFT_1_20_3, 37), mapSet(MINECRAFT_1_19_3, 36), empty(id("minecraft:int_range", mapSet(MINECRAFT_1_21_6, 39), mapSet(MINECRAFT_1_20_5, 38), mapSet(MINECRAFT_1_20_3, 37),
mapSet(MINECRAFT_1_19, 37))); mapSet(MINECRAFT_1_19_3, 36), mapSet(MINECRAFT_1_19, 37)));
empty(id("minecraft:float_range", mapSet(MINECRAFT_1_20_5, 39), mapSet(MINECRAFT_1_20_3, 38), mapSet(MINECRAFT_1_19_3, 37), empty(id("minecraft:float_range", mapSet(MINECRAFT_1_21_6, 40), mapSet(MINECRAFT_1_20_5, 39), mapSet(MINECRAFT_1_20_3, 38),
mapSet(MINECRAFT_1_19, 38))); mapSet(MINECRAFT_1_19_3, 37), mapSet(MINECRAFT_1_19, 38)));
empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 39))); empty(id("minecraft:item_enchantment", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 39)));
empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 40))); empty(id("minecraft:entity_summon", mapSet(MINECRAFT_1_19_3, -1), mapSet(MINECRAFT_1_19, 40)));
empty(id("minecraft:dimension", mapSet(MINECRAFT_1_20_5, 40), mapSet(MINECRAFT_1_20_3, 39), mapSet(MINECRAFT_1_19_3, 38), empty(id("minecraft:dimension", mapSet(MINECRAFT_1_21_6, 41), mapSet(MINECRAFT_1_20_5, 40), mapSet(MINECRAFT_1_20_3, 39),
mapSet(MINECRAFT_1_19, 41))); mapSet(MINECRAFT_1_19_3, 38), mapSet(MINECRAFT_1_19, 41)));
empty(id("minecraft:gamemode", mapSet(MINECRAFT_1_20_5, 41), mapSet(MINECRAFT_1_20_3, 40), mapSet(MINECRAFT_1_19_3, 39))); // 1.19.3 empty(id("minecraft:gamemode", mapSet(MINECRAFT_1_21_6, 42), mapSet(MINECRAFT_1_20_5, 41), mapSet(MINECRAFT_1_20_3, 40),
mapSet(MINECRAFT_1_19_3, 39))); // 1.19.3
empty(id("minecraft:time", mapSet(MINECRAFT_1_20_5, 42), mapSet(MINECRAFT_1_20_3, 41), mapSet(MINECRAFT_1_19_3, 40), empty(id("minecraft:time", mapSet(MINECRAFT_1_21_6, 43), mapSet(MINECRAFT_1_20_5, 42), mapSet(MINECRAFT_1_20_3, 41),
mapSet(MINECRAFT_1_19, 42)), TimeArgumentSerializer.TIME); // added in 1.14 mapSet(MINECRAFT_1_19_3, 40), mapSet(MINECRAFT_1_19, 42)), TimeArgumentSerializer.TIME); // added in 1.14
register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_20_5, 43), mapSet(MINECRAFT_1_20_3, 42), mapSet(MINECRAFT_1_19_3, 41), register(id("minecraft:resource_or_tag", mapSet(MINECRAFT_1_21_6, 44), mapSet(MINECRAFT_1_20_5, 43), mapSet(MINECRAFT_1_20_3, 42),
mapSet(MINECRAFT_1_19, 43)), RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); mapSet(MINECRAFT_1_19_3, 41), mapSet(MINECRAFT_1_19, 43)), RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY);
register(id("minecraft:resource_or_tag_key", mapSet(MINECRAFT_1_20_5, 44), mapSet(MINECRAFT_1_20_3, 43), mapSet(MINECRAFT_1_19_3, 42)), register(id("minecraft:resource_or_tag_key", mapSet(MINECRAFT_1_21_6, 45), mapSet(MINECRAFT_1_20_5, 44), mapSet(MINECRAFT_1_20_3, 43),
mapSet(MINECRAFT_1_19_3, 42)),
RegistryKeyArgumentList.ResourceOrTagKey.class, RegistryKeyArgumentList.ResourceOrTagKey.class,
RegistryKeyArgumentList.ResourceOrTagKey.Serializer.REGISTRY); RegistryKeyArgumentList.ResourceOrTagKey.Serializer.REGISTRY);
register(id("minecraft:resource", mapSet(MINECRAFT_1_20_5, 45), mapSet(MINECRAFT_1_20_3, 44), mapSet(MINECRAFT_1_19_3, 43), register(id("minecraft:resource", mapSet(MINECRAFT_1_21_6, 46), mapSet(MINECRAFT_1_20_5, 45), mapSet(MINECRAFT_1_20_3, 44),
mapSet(MINECRAFT_1_19, 44)), mapSet(MINECRAFT_1_19_3, 43), mapSet(MINECRAFT_1_19, 44)),
RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY); RegistryKeyArgument.class, RegistryKeyArgumentSerializer.REGISTRY);
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_21_6, 47), 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_6, 48), 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_6, 49), mapSet(MINECRAFT_1_21_5, 48), mapSet(MINECRAFT_1_20_5, 47),
empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_20_5, 48), mapSet(MINECRAFT_1_20_3, 47), mapSet(MINECRAFT_1_19, 46))); // 1.19 mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19
empty(id("minecraft:heightmap", mapSet(MINECRAFT_1_20_3, 49), mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4 empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_21_6, 50), 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_21_6, 51), 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_6, 56), mapSet(MINECRAFT_1_21_5, 54),mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48),
mapSet(MINECRAFT_1_19, 47))); // added in 1.16 mapSet(MINECRAFT_1_19_4, 48), 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_6, 52), 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_6, 53), 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_6, 54), mapSet(MINECRAFT_1_21_5, 53), mapSet(MINECRAFT_1_20_5, 52)));
empty(id("minecraft:hex_color", mapSet(MINECRAFT_1_21_6, 17))); // added in 1.21.6
empty(id("minecraft:dialog", mapSet(MINECRAFT_1_21_6, 55))); // added in 1.21.6
// 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,16 +105,14 @@ 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();
if (number instanceof Byte) { if (number instanceof Byte) {

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,14 +70,15 @@ 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
public String toString() { public String toString() {
return "LastSeenMessages{" + return "LastSeenMessages{" +
"offset=" + offset + "offset=" + offset +
", acknowledged=" + acknowledged + ", acknowledged=" + acknowledged +
'}'; ", checksum=" + checksum +
'}';
} }
} }

View File

@@ -0,0 +1,58 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet.chat;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import net.kyori.adventure.text.Component;
public abstract class RateLimitedCommandHandler<T extends MinecraftPacket> implements CommandHandler<T> {
private final Player player;
private final VelocityServer velocityServer;
private int failedAttempts;
protected RateLimitedCommandHandler(Player player, VelocityServer velocityServer) {
this.player = player;
this.velocityServer = velocityServer;
}
@Override
public boolean handlePlayerCommand(MinecraftPacket packet) {
if (packetClass().isInstance(packet)) {
if (!velocityServer.getCommandRateLimiter().attempt(player.getUniqueId())) {
if (velocityServer.getConfiguration().isKickOnCommandRateLimit() && failedAttempts++ >= velocityServer.getConfiguration().getKickAfterRateLimitedCommands()) {
player.disconnect(Component.translatable("velocity.kick.command-rate-limit"));
}
if (velocityServer.getConfiguration().isForwardCommandsIfRateLimited()) {
return false; // Send the packet to the server
}
} else {
failedAttempts = 0;
}
handlePlayerCommandInternal(packetClass().cast(packet));
return true;
}
return false;
}
}

View File

@@ -21,17 +21,18 @@ import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.crypto.IdentifiedKey;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPacket> { public class KeyedCommandHandler extends RateLimitedCommandHandler<KeyedPlayerCommandPacket> {
private final ConnectedPlayer player; private final ConnectedPlayer player;
private final VelocityServer server; private final VelocityServer server;
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) { public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
super(player, server);
this.player = player; this.player = player;
this.server = server; this.server = server;
} }

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, 256); 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

@@ -20,16 +20,18 @@ package com.velocitypowered.proxy.protocol.packet.chat.legacy;
import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
import java.time.Instant; import java.time.Instant;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> { public class LegacyCommandHandler extends RateLimitedCommandHandler<LegacyChatPacket> {
private final ConnectedPlayer player; private final ConnectedPlayer player;
private final VelocityServer server; private final VelocityServer server;
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) { public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
super(player, server);
this.player = player; this.player = player;
this.server = server; this.server = server;
} }

View File

@@ -22,17 +22,19 @@ import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket; import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> { public class SessionCommandHandler extends RateLimitedCommandHandler<SessionPlayerCommandPacket> {
private final ConnectedPlayer player; private final ConnectedPlayer player;
private final VelocityServer server; private final VelocityServer server;
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) { public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
super(player, server);
this.player = player; this.player = player;
this.server = server; this.server = server;
} }

View File

@@ -73,7 +73,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
@@ -86,7 +86,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);
} }
@Override @Override
@@ -54,7 +54,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

@@ -36,7 +36,7 @@ public class KnownPacksPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) { ProtocolVersion protocolVersion) {
final int packCount = ProtocolUtils.readVarInt(buf); final int packCount = ProtocolUtils.readVarInt(buf);
if (packCount > MAX_LENGTH_PACKS) { if (direction == ProtocolUtils.Direction.SERVERBOUND && packCount > MAX_LENGTH_PACKS) {
throw TOO_MANY_PACKS; throw TOO_MANY_PACKS;
} }

View File

@@ -22,13 +22,20 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.api.util.ProxyVersion;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collection; import java.util.Collection;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -85,13 +92,18 @@ public final class PluginMessageUtil {
.equals(UNREGISTER_CHANNEL); .equals(UNREGISTER_CHANNEL);
} }
private static final QuietDecoderException ILLEGAL_CHANNEL = new QuietDecoderException("Illegal channel");
/** /**
* Fetches all the channels in a register or unregister plugin message. * Fetches all the channels in a register or unregister plugin message.
* *
* @param existingChannels the number of channels already registered
* @param message the message to get the channels from * @param message the message to get the channels from
* @return the channels, as an immutable list * @return the channels, as an immutable list
*/ */
public static List<String> getChannels(PluginMessagePacket message) { public static List<ChannelIdentifier> getChannels(int existingChannels,
PluginMessagePacket message,
ProtocolVersion protocolVersion) {
checkNotNull(message, "message"); checkNotNull(message, "message");
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s", checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
message.getChannel()); message.getChannel());
@@ -100,8 +112,28 @@ public final class PluginMessageUtil {
// has caused issues with 1.13+ compatibility. Just return an empty list. // has caused issues with 1.13+ compatibility. Just return an empty list.
return ImmutableList.of(); return ImmutableList.of();
} }
String channels = message.content().toString(StandardCharsets.UTF_8); String payload = message.content().toString(StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0")); checkArgument(payload.length() <= Short.MAX_VALUE, "payload too long: %s", payload.length());
String[] channels = payload.split("\0");
checkArgument(existingChannels + channels.length <= ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS,
"too many channels: %s + %s > %s", existingChannels, channels.length, ConnectedPlayer.MAX_CLIENTSIDE_PLUGIN_CHANNELS);
ImmutableList.Builder<ChannelIdentifier> channelIdentifiers = ImmutableList.builderWithExpectedSize(channels.length);
try {
for (String channel : channels) {
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) {
channelIdentifiers.add(MinecraftChannelIdentifier.from(channel));
} else {
channelIdentifiers.add(new LegacyChannelIdentifier(channel));
}
}
} catch (IllegalArgumentException e) {
if (MinecraftDecoder.DEBUG) {
throw e;
} else {
throw ILLEGAL_CHANNEL;
}
}
return channelIdentifiers.build();
} }
/** /**
@@ -112,16 +144,31 @@ public final class PluginMessageUtil {
* @return the plugin message to send * @return the plugin message to send
*/ */
public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion, public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion,
Collection<String> channels) { Collection<ChannelIdentifier> channels) {
checkNotNull(channels, "channels"); checkNotNull(channels, "channels");
checkArgument(!channels.isEmpty(), "no channels specified"); checkArgument(!channels.isEmpty(), "no channels specified");
String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13) String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY; ? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
ByteBuf contents = Unpooled.buffer(); ByteBuf contents = Unpooled.buffer();
contents.writeCharSequence(String.join("\0", channels), StandardCharsets.UTF_8); contents.writeCharSequence(joinChannels(channels), StandardCharsets.UTF_8);
return new PluginMessagePacket(channelName, contents); return new PluginMessagePacket(channelName, contents);
} }
private static String joinChannels(Collection<ChannelIdentifier> channels) {
checkNotNull(channels, "channels");
checkArgument(!channels.isEmpty(), "no channels specified");
StringBuilder sb = new StringBuilder();
Iterator<ChannelIdentifier> iterator = channels.iterator();
while (iterator.hasNext()) {
ChannelIdentifier channel = iterator.next();
sb.append(channel.getId());
if (iterator.hasNext()) {
sb.append('\0');
}
}
return sb.toString();
}
/** /**
* Rewrites the brand message to indicate the presence of Velocity. * Rewrites the brand message to indicate the presence of Velocity.
* *

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

@@ -114,7 +114,7 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud
server.createBootstrap(loop).handler(new ChannelInitializer<>() { server.createBootstrap(loop).handler(new ChannelInitializer<>() {
@Override @Override
protected void initChannel(Channel ch) { protected void initChannel(Channel ch) {
ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder(ProtocolUtils.Direction.CLIENTBOUND))
.addLast(READ_TIMEOUT, new ReadTimeoutHandler( .addLast(READ_TIMEOUT, new ReadTimeoutHandler(
pingOptions.getTimeout() == 0 pingOptions.getTimeout() == 0
? server.getConfiguration().getReadTimeout() ? server.getConfiguration().getReadTimeout()
@@ -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

@@ -24,9 +24,6 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationRegistry;
import net.kyori.adventure.translation.Translator;
import org.jetbrains.annotations.Nullable;
/** /**
* Velocity Translation Mapper. * Velocity Translation Mapper.
@@ -43,25 +40,9 @@ public enum TranslatableMapper implements BiConsumer<TranslatableComponent, Cons
final TranslatableComponent translatableComponent, final TranslatableComponent translatableComponent,
final Consumer<Component> componentConsumer final Consumer<Component> componentConsumer
) { ) {
for (final Translator source : GlobalTranslator.translator().sources()) { final Locale locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault());
if (source instanceof TranslationRegistry registry if (GlobalTranslator.translator().canTranslate(translatableComponent.key(), locale)) {
&& registry.contains(translatableComponent.key())) { componentConsumer.accept(GlobalTranslator.render(translatableComponent, locale));
componentConsumer.accept(GlobalTranslator.render(translatableComponent,
ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault())));
return;
}
}
final @Nullable String fallback = translatableComponent.fallback();
if (fallback == null) {
return;
}
for (final Translator source : GlobalTranslator.translator().sources()) {
if (source instanceof TranslationRegistry registry && registry.contains(fallback)) {
componentConsumer.accept(
GlobalTranslator.render(Component.translatable(fallback),
ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault())));
return;
}
} }
} }
} }

View File

@@ -79,10 +79,10 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
* *
* @return all legacy channel IDs * @return all legacy channel IDs
*/ */
public Collection<String> getLegacyChannelIds() { public Collection<ChannelIdentifier> getLegacyChannelIds() {
Collection<String> ids = new HashSet<>(); Collection<ChannelIdentifier> ids = new HashSet<>();
for (ChannelIdentifier value : identifierMap.values()) { for (ChannelIdentifier value : identifierMap.values()) {
ids.add(value.getId()); ids.add(new LegacyChannelIdentifier(value.getId()));
} }
return ids; return ids;
} }
@@ -92,13 +92,13 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
* *
* @return the channel IDs for Minecraft 1.13 and above * @return the channel IDs for Minecraft 1.13 and above
*/ */
public Collection<String> getModernChannelIds() { public Collection<ChannelIdentifier> getModernChannelIds() {
Collection<String> ids = new HashSet<>(); Collection<ChannelIdentifier> ids = new HashSet<>();
for (ChannelIdentifier value : identifierMap.values()) { for (ChannelIdentifier value : identifierMap.values()) {
if (value instanceof MinecraftChannelIdentifier) { if (value instanceof MinecraftChannelIdentifier) {
ids.add(value.getId()); ids.add(value);
} else { } else {
ids.add(PluginMessageUtil.transformLegacyToModernChannel(value.getId())); ids.add(MinecraftChannelIdentifier.from(PluginMessageUtil.transformLegacyToModernChannel(value.getId())));
} }
} }
return ids; return ids;
@@ -114,7 +114,7 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
* @param protocolVersion the protocol version in use * @param protocolVersion the protocol version in use
* @return the list of channels to register * @return the list of channels to register
*/ */
public Collection<String> getChannelsForProtocol(ProtocolVersion protocolVersion) { public Collection<ChannelIdentifier> getChannelsForProtocol(ProtocolVersion protocolVersion) {
if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) { if (protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)) {
return getModernChannelIds(); return getModernChannelIds();
} }

View File

@@ -0,0 +1,70 @@
/*
* Copyright (C) 2019-2023 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.util.collect;
import com.google.common.base.Preconditions;
import com.google.common.collect.ForwardingSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
/**
* An unsynchronized collection that puts an upper bound on the size of the collection.
*/
public final class CappedSet<T> extends ForwardingSet<T> {
private final Set<T> delegate;
private final int upperSize;
private CappedSet(Set<T> delegate, int upperSize) {
this.delegate = delegate;
this.upperSize = upperSize;
}
/**
* Creates a capped collection backed by a {@link HashSet}.
*
* @param maxSize the maximum size of the collection
* @param <T> the type of elements in the collection
* @return the new collection
*/
public static <T> Set<T> create(int maxSize) {
return new CappedSet<>(new HashSet<>(), maxSize);
}
@Override
protected Set<T> delegate() {
return delegate;
}
@Override
public boolean add(T element) {
if (this.delegate.size() >= upperSize) {
Preconditions.checkState(this.delegate.contains(element),
"collection is too large (%s >= %s)",
this.delegate.size(), this.upperSize);
return false;
}
return this.delegate.add(element);
}
@Override
public boolean addAll(Collection<? extends T> collection) {
return this.standardAddAll(collection);
}
}

View File

@@ -22,15 +22,15 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.cache.Ticker;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
/** /**
* A simple rate-limiter based on a Caffeine {@link Cache}. * A simple rate-limiter based on a Caffeine {@link Cache}.
*/ */
public class CaffeineCacheRatelimiter implements Ratelimiter { public class CaffeineCacheRatelimiter<T> implements Ratelimiter<T> {
private final Cache<InetAddress, Long> expiringCache; private final Cache<T, Long> expiringCache;
private final long timeoutNanos; private final long timeoutNanos;
CaffeineCacheRatelimiter(long time, TimeUnit unit) { CaffeineCacheRatelimiter(long time, TimeUnit unit) {
@@ -49,16 +49,15 @@ public class CaffeineCacheRatelimiter implements Ratelimiter {
} }
/** /**
* Attempts to rate-limit the client. * Attempts to rate-limit the object.
* *
* @param address the address to rate limit * @param key the object to rate limit
* @return true if we should allow the client, false if we should rate-limit * @return true if we should allow the object, false if we should rate-limit
*/ */
@Override @Override
public boolean attempt(InetAddress address) { public boolean attempt(@NotNull T key) {
Preconditions.checkNotNull(address, "address");
long expectedNewValue = System.nanoTime() + timeoutNanos; long expectedNewValue = System.nanoTime() + timeoutNanos;
long last = expiringCache.get(address, (address1) -> expectedNewValue); long last = expiringCache.get(key, (key1) -> expectedNewValue);
return expectedNewValue == last; return expectedNewValue == last;
} }
} }

View File

@@ -17,16 +17,16 @@
package com.velocitypowered.proxy.util.ratelimit; package com.velocitypowered.proxy.util.ratelimit;
import java.net.InetAddress; import org.jetbrains.annotations.NotNull;
/** /**
* A {@link Ratelimiter} that does no rate-limiting. * A {@link Ratelimiter} that does no rate-limiting.
*/ */
enum NoopCacheRatelimiter implements Ratelimiter { enum NoopCacheRatelimiter implements Ratelimiter<Object> {
INSTANCE; INSTANCE;
@Override @Override
public boolean attempt(InetAddress address) { public boolean attempt(@NotNull Object key) {
return true; return true;
} }
} }

View File

@@ -17,18 +17,18 @@
package com.velocitypowered.proxy.util.ratelimit; package com.velocitypowered.proxy.util.ratelimit;
import java.net.InetAddress; import org.jetbrains.annotations.NotNull;
/** /**
* Allows rate limiting of clients. * Allows rate limiting of objects.
*/ */
public interface Ratelimiter { public interface Ratelimiter<T> {
/** /**
* Determines whether or not to allow the connection. * Attempts to rate-limit the object.
* *
* @param address the address to rate limit * @param key the object to rate limit
* @return true if allowed, false if not * @return true if we should allow the object, false if we should rate-limit
*/ */
boolean attempt(InetAddress address); boolean attempt(@NotNull T key);
} }

View File

@@ -28,8 +28,9 @@ public final class Ratelimiters {
throw new AssertionError(); throw new AssertionError();
} }
public static Ratelimiter createWithMilliseconds(long ms) { @SuppressWarnings("unchecked")
return ms <= 0 ? NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms, public static <T> Ratelimiter<T> createWithMilliseconds(long ms) {
return ms <= 0 ? (Ratelimiter<T>) NoopCacheRatelimiter.INSTANCE : new CaffeineCacheRatelimiter(ms,
TimeUnit.MILLISECONDS); TimeUnit.MILLISECONDS);
} }
} }

View File

@@ -63,3 +63,5 @@ velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no in
velocity.command.send-usage=/send <player> <server> velocity.command.send-usage=/send <player> <server>
# Kick # Kick
velocity.kick.shutdown=Proxy shutting down. velocity.kick.shutdown=Proxy shutting down.
velocity.kick.command-rate-limit=You are sending too many commands too quickly.
velocity.kick.tab-complete-rate-limit=You are sending too many tab complete requests too quickly.

View File

@@ -35,7 +35,7 @@ velocity.command.generic-error=Възникна грешка при изпълн
velocity.command.command-does-not-exist=Тази команда не съществува. velocity.command.command-does-not-exist=Тази команда не съществува.
velocity.command.players-only=Само играчи могат да изпълняват тази команда. velocity.command.players-only=Само играчи могат да изпълняват тази команда.
velocity.command.server-does-not-exist=Сървър с името {0} не съществува. velocity.command.server-does-not-exist=Сървър с името {0} не съществува.
velocity.command.player-not-found=Този играч {0} не съществува. velocity.command.player-not-found=Играч с името {0} не съществува.
velocity.command.server-current-server=В момента сте свързан към {0}. velocity.command.server-current-server=В момента сте свързан към {0}.
velocity.command.server-too-many=Има прекалено много регистрирани сървъри. Използвайте TAB, за да видите всички налични сървъри. velocity.command.server-too-many=Има прекалено много регистрирани сървъри. Използвайте TAB, за да видите всички налични сървъри.
velocity.command.server-available=Налични сървъри\: velocity.command.server-available=Налични сървъри\:

View File

@@ -29,7 +29,7 @@ velocity.error.modern-forwarding-needs-new-client=Este servidor solo es compatib
velocity.error.modern-forwarding-failed=El servidor no ha enviado una solicitud de reenvío al proxy. Asegúrate de que tu servidor está configurado para usar el método de reenvío de Velocity. velocity.error.modern-forwarding-failed=El servidor no ha enviado una solicitud de reenvío al proxy. Asegúrate de que tu servidor está configurado para usar el método de reenvío de Velocity.
velocity.error.moved-to-new-server=Has sido echado de {0}\: {1} velocity.error.moved-to-new-server=Has sido echado de {0}\: {1}
velocity.error.no-available-servers=No hay servidores disponibles a los que conectarte. Inténtalo de nuevo más tarde o contacta con un administrador. velocity.error.no-available-servers=No hay servidores disponibles a los que conectarte. Inténtalo de nuevo más tarde o contacta con un administrador.
velocity.error.illegal-chat-characters=Illegal characters in chat velocity.error.illegal-chat-characters=Caracteres no válidos en el chat
# Commands # Commands
velocity.command.generic-error=Se ha producido un error al ejecutar este comando. velocity.command.generic-error=Se ha producido un error al ejecutar este comando.
velocity.command.command-does-not-exist=Este comando no existe. velocity.command.command-does-not-exist=Este comando no existe.
@@ -60,6 +60,6 @@ velocity.command.dump-success=Se ha creado un informe anónimo que contiene info
velocity.command.dump-will-expire=Este enlace caducará en unos días. velocity.command.dump-will-expire=Este enlace caducará en unos días.
velocity.command.dump-server-error=Se ha producido un error en los servidores de Velocity y la subida no se ha podido completar. Notifica al equipo de Velocity sobre este problema y proporciona los detalles sobre este error disponibles en el archivo de registro o la consola de tu servidor Velocity. velocity.command.dump-server-error=Se ha producido un error en los servidores de Velocity y la subida no se ha podido completar. Notifica al equipo de Velocity sobre este problema y proporciona los detalles sobre este error disponibles en el archivo de registro o la consola de tu servidor Velocity.
velocity.command.dump-offline=Causa probable\: la configuración DNS del sistema no es válida o no hay conexión a internet velocity.command.dump-offline=Causa probable\: la configuración DNS del sistema no es válida o no hay conexión a internet
velocity.command.send-usage=/send <player> <server> velocity.command.send-usage=/send <jugador> <servidor>
# Kick # Kick
velocity.kick.shutdown=Proxy shutting down. velocity.kick.shutdown=El proxy se ha apagado.

View File

@@ -66,6 +66,11 @@ kick-existing-players = false
# configuration is used if no servers could be contacted. # configuration is used if no servers could be contacted.
ping-passthrough = "DISABLED" ping-passthrough = "DISABLED"
# If enabled (default is false), then a sample of the online players on the proxy will be visible
# when hovering over the player count in the server list.
# This doesn't have any effect when ping passthrough is set to either "description" or "all".
sample-players-in-ping = false
# If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs # If not enabled (default is true) player IP addresses will be replaced by <ip address withheld> in logs
enable-player-address-logging = true enable-player-address-logging = true
@@ -145,6 +150,33 @@ 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
# How fast (in milliseconds) are clients allowed to send commands after the last command
# By default this is 50ms (20 commands per second)
command-rate-limit = 50
# Should we forward commands to the backend upon being rate limited?
# This will forward the command to the server instead of processing it on the proxy.
# Since most server implementations have a rate limit, this will prevent the player
# from being able to send excessive commands to the server.
forward-commands-if-rate-limited = true
# How many commands are allowed to be sent after the rate limit is hit before the player is kicked?
# Setting this to 0 or lower will disable this feature.
kick-after-rate-limited-commands = 0
# How fast (in milliseconds) are clients allowed to send tab completions after the last tab completion
tab-complete-rate-limit = 10
# How many tab completions are allowed to be sent after the rate limit is hit before the player is kicked?
# Setting this to 0 or lower will disable this feature.
kick-after-rate-limited-tab-completes = 0
[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

@@ -20,8 +20,10 @@ package com.velocitypowered.proxy.util;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
class VelocityChannelRegistrarTest { class VelocityChannelRegistrarTest {
@@ -46,9 +48,9 @@ class VelocityChannelRegistrarTest {
// Two channels cover the modern channel (velocity:test) and the legacy-mapped channel // Two channels cover the modern channel (velocity:test) and the legacy-mapped channel
// (legacy:velocitytest). Make sure they're what we expect. // (legacy:velocitytest). Make sure they're what we expect.
assertEquals(ImmutableSet.of(MODERN.getId(), SIMPLE_LEGACY_REMAPPED), registrar assertEquals(ImmutableSet.of(MODERN.getId(), SIMPLE_LEGACY_REMAPPED), registrar
.getModernChannelIds()); .getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
assertEquals(ImmutableSet.of(SIMPLE_LEGACY.getId(), MODERN.getId()), registrar assertEquals(ImmutableSet.of(SIMPLE_LEGACY.getId(), MODERN.getId()), registrar
.getLegacyChannelIds()); .getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
} }
@Test @Test
@@ -57,9 +59,10 @@ class VelocityChannelRegistrarTest {
registrar.register(SPECIAL_REMAP_LEGACY, MODERN_SPECIAL_REMAP); registrar.register(SPECIAL_REMAP_LEGACY, MODERN_SPECIAL_REMAP);
// This one, just one channel for the modern case. // This one, just one channel for the modern case.
assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()), registrar.getModernChannelIds()); assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId()),
registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId(), SPECIAL_REMAP_LEGACY.getId()), assertEquals(ImmutableSet.of(MODERN_SPECIAL_REMAP.getId(), SPECIAL_REMAP_LEGACY.getId()),
registrar.getLegacyChannelIds()); registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
} }
@Test @Test
@@ -68,7 +71,9 @@ class VelocityChannelRegistrarTest {
registrar.register(MODERN, SIMPLE_LEGACY); registrar.register(MODERN, SIMPLE_LEGACY);
registrar.unregister(SIMPLE_LEGACY); registrar.unregister(SIMPLE_LEGACY);
assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getModernChannelIds()); assertEquals(ImmutableSet.of(MODERN.getId()),
assertEquals(ImmutableSet.of(MODERN.getId()), registrar.getLegacyChannelIds()); registrar.getModernChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));;
assertEquals(ImmutableSet.of(MODERN.getId()),
registrar.getLegacyChannelIds().stream().map(ChannelIdentifier::getId).collect(Collectors.toSet()));
} }
} }

View File

@@ -0,0 +1,71 @@
/*
* Copyright (C) 2019-2021 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.util.collect;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.google.common.collect.ImmutableSet;
import java.util.Collection;
import java.util.Set;
import org.junit.jupiter.api.Test;
class CappedSetTest {
@Test
void basicVerification() {
Collection<String> coll = CappedSet.create(1);
assertTrue(coll.add("coffee"), "did not add single item");
assertThrows(IllegalStateException.class, () -> coll.add("tea"),
"item was added to collection although it is too full");
assertEquals(1, coll.size(), "collection grew in size unexpectedly");
}
@Test
void testAddAll() {
Set<String> doesFill1 = ImmutableSet.of("coffee", "tea");
Set<String> doesFill2 = ImmutableSet.of("chocolate");
Set<String> overfill = ImmutableSet.of("Coke", "Pepsi");
Collection<String> coll = CappedSet.create(3);
assertTrue(coll.addAll(doesFill1), "did not add items");
assertTrue(coll.addAll(doesFill2), "did not add items");
assertThrows(IllegalStateException.class, () -> coll.addAll(overfill),
"items added to collection although it is too full");
assertEquals(3, coll.size(), "collection grew in size unexpectedly");
}
@Test
void handlesSetBehaviorCorrectly() {
Set<String> doesFill1 = ImmutableSet.of("coffee", "tea");
Set<String> doesFill2 = ImmutableSet.of("coffee", "chocolate");
Set<String> overfill = ImmutableSet.of("coffee", "Coke", "Pepsi");
Collection<String> coll = CappedSet.create(3);
assertTrue(coll.addAll(doesFill1), "did not add items");
assertTrue(coll.addAll(doesFill2), "did not add items");
assertThrows(IllegalStateException.class, () -> coll.addAll(overfill),
"items added to collection although it is too full");
assertFalse(coll.addAll(doesFill1), "added items?!?");
assertEquals(3, coll.size(), "collection grew in size unexpectedly");
}
}