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 1845 additions and 577 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

@@ -5,7 +5,7 @@ plugins {
} }
java { java {
// withJavadocJar() withJavadocJar()
withSourcesJar() withSourcesJar()
sourceSets["main"].java { sourceSets["main"].java {
@@ -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

@@ -149,7 +149,58 @@ public interface ProxyConfig {
*/ */
int getReadTimeout(); int getReadTimeout();
String getNeteaseAuthUrl(); /**
* Get the rate limit for how fast a player can execute commands.
*
* @return the command rate limit (in milliseconds)
*/
int getCommandRatelimit();
String getNeteaseGameId(); /**
* 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

@@ -225,116 +225,4 @@ public final class GameProfile {
+ '}'; + '}';
} }
} }
/**
* netease auth response.
*/
public static class Response {
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 Response(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<Property> properties;
/**
* default constructor.
*
* @param id -
* @param name -
* @param properties -
*/
public ResponseEntity(String id, String name, List<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<Property> getProperties() {
return properties;
}
public void setProperties(List<Property> properties) {
this.properties = properties;
}
@Override
public String toString() {
return "ResponseEntity{"
+ "id='" + id + '\''
+ ", name='" + name + '\''
+ ", properties=" + properties
+ '}';
}
}
} }

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
@@ -92,11 +94,6 @@ public class VelocityConfiguration implements ProxyConfig {
@Expose @Expose
private boolean forceKeyAuthentication = true; // Added in 1.19 private boolean forceKeyAuthentication = true; // Added in 1.19
@Expose
private String authUrl = "http://192.168.46.50:9999/check";
@Expose
private String gameId = "77140593557373952";
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced, private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
Query query, Metrics metrics) { Query query, Metrics metrics) {
this.servers = servers; this.servers = servers;
@@ -110,9 +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,
String authUrl, String gameId) { boolean forceKeyAuthentication) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
@@ -123,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;
@@ -130,8 +128,6 @@ public class VelocityConfiguration implements ProxyConfig {
this.query = query; this.query = query;
this.metrics = metrics; this.metrics = metrics;
this.forceKeyAuthentication = forceKeyAuthentication; this.forceKeyAuthentication = forceKeyAuthentication;
this.authUrl = authUrl;
this.gameId = gameId;
} }
/** /**
@@ -238,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;
@@ -359,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();
} }
@@ -379,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;
} }
@@ -415,14 +445,8 @@ public class VelocityConfiguration implements ProxyConfig {
return forceKeyAuthentication; return forceKeyAuthentication;
} }
@Override public boolean isEnableReusePort() {
public String getNeteaseAuthUrl() { return advanced.isEnableReusePort();
return authUrl;
}
@Override
public String getNeteaseGameId() {
return gameId;
} }
@Override @Override
@@ -442,8 +466,6 @@ public class VelocityConfiguration implements ProxyConfig {
.add("favicon", favicon) .add("favicon", favicon)
.add("enablePlayerAddressLogging", enablePlayerAddressLogging) .add("enablePlayerAddressLogging", enablePlayerAddressLogging)
.add("forceKeyAuthentication", forceKeyAuthentication) .add("forceKeyAuthentication", forceKeyAuthentication)
.add("authUrl", authUrl)
.add("gameId", gameId)
.toString(); .toString();
} }
@@ -523,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);
@@ -541,8 +565,6 @@ public class VelocityConfiguration implements ProxyConfig {
|| forwardingMode == PlayerInfoForwarding.BUNGEEGUARD)) { || forwardingMode == PlayerInfoForwarding.BUNGEEGUARD)) {
throw new RuntimeException("The forwarding-secret file must not be empty."); throw new RuntimeException("The forwarding-secret file must not be empty.");
} }
final String authUrl = config.getOrElse("auth-url", "http://192.168.46.50:9999/check");
final String gameId = config.getOrElse("game-id", "77140593557373952");
return new VelocityConfiguration( return new VelocityConfiguration(
bind, bind,
@@ -555,15 +577,14 @@ 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),
new Advanced(advancedConfig), new Advanced(advancedConfig),
new Query(queryConfig), new Query(queryConfig),
new Metrics(metricsConfig), new Metrics(metricsConfig),
forceKeyAuthentication, forceKeyAuthentication
authUrl,
gameId
); );
} }
} }
@@ -740,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() {
} }
@@ -765,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);
} }
} }
@@ -828,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{"
@@ -845,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

@@ -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;
@@ -206,14 +208,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
JsonObject data = new JsonObject(); JsonObject data = new JsonObject();
data.addProperty("username", login.getUsername()); data.addProperty("username", login.getUsername());
data.addProperty("serverId", serverId); data.addProperty("serverId", serverId);
data.addProperty("gameID", server.getConfiguration().getNeteaseGameId()); data.addProperty("gameID", NeteaseDataManager.getConfig().getGameId());
HttpRequest httpRequest = HttpRequest.newBuilder() HttpRequest httpRequest = HttpRequest.newBuilder()
.headers( .headers("Content-Type", "application/json")
"User-Agent", server.getVersion().getName() + "/" + server.getVersion().getVersion(), .uri(URI.create(NeteaseDataManager.getConfig().getAuthUrl()))
"Content-Type", "application/json" .POST(HttpRequest.BodyPublishers.ofString(data.toString()))
).POST(HttpRequest.BodyPublishers.ofString(data.toString()))
.uri(URI.create(server.getConfiguration().getNeteaseAuthUrl()))
.build(); .build();
final HttpClient httpClient = server.createHttpClient(); final HttpClient httpClient = server.createHttpClient();
@@ -244,13 +244,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
try { try {
if (response.statusCode() == 200) { if (response.statusCode() == 200) {
final GameProfile.Response authResponse = GENERAL_GSON.fromJson(response.body(), final AuthResponse authResponse = GENERAL_GSON.fromJson(response.body(), AuthResponse.class);
GameProfile.Response.class);
if (authResponse.getCode() != 0) { if (authResponse.getCode() != 0) {
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down")); inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
logger.error("Error authenticating {} with netease", login.getUsername()); logger.error("Error authenticating {} with netease", login.getUsername());
} else { } else {
GameProfile.ResponseEntity entity = authResponse.getEntity(); AuthResponse.ResponseEntity entity = authResponse.getEntity();
if (entity.getName() == null || entity.getName().isEmpty()) { if (entity.getName() == null || entity.getName().isEmpty()) {
entity.setName(login.getUsername()); entity.setName(login.getUsername());
} }
@@ -259,7 +258,8 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
if (entity.getId() == null) { if (entity.getId() == null) {
inbound.disconnect( inbound.disconnect(
Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)); Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)
);
} else { } else {
GameProfile profile = new GameProfile(entity.getId(), entity.getName(), entity.getProperties()); GameProfile profile = new GameProfile(entity.getId(), entity.getName(), entity.getProperties());
// Not so fast, now we verify the public key for 1.19.1+ // Not so fast, now we verify the public key for 1.19.1+

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 }
if (this.server.getConfiguration().isProxyProtocol()) {
LOGGER.warn("Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", channel.localAddress()); final int binds = server.getConfiguration().isEnableReusePort()
? ((MultithreadEventExecutorGroup) this.workerGroup).executorCount() : 1;
for (int bind = 0; bind < binds; bind++) {
// Wait for each bind to open. If we encounter any errors, don't try to bind again.
int finalBind = bind;
ChannelFuture f = bootstrap.bind()
.addListener((ChannelFutureListener) future -> {
final Channel channel = future.channel();
if (future.isSuccess()) {
this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT));
LOGGER.info("Listening on {}", channel.localAddress());
if (finalBind == 0) {
// Warn people with console access that HAProxy is in use, see PR: #1436
if (this.server.getConfiguration().isProxyProtocol()) {
LOGGER.warn(
"Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.",
channel.localAddress());
}
// Fire the proxy bound event after the socket is bound
server.getEventManager().fireAndForget(
new ListenerBoundEvent(address, ListenerType.MINECRAFT));
}
} else {
LOGGER.error("Can't bind to {}", address, future.cause());
} }
});
f.syncUninterruptibly();
LOGGER.info("Listening on {}", channel.localAddress()); 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

@@ -62,4 +62,6 @@ velocity.command.dump-server-error=An error occurred on the Velocity servers and
velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection
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,23 +66,14 @@ 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
# 认证链接
# 正式环境http://x19authserver.nie.netease.com/check
# 测试环境http://x19authexpr.nie.netease.com/check
# 1.20版本请使用以下接口:
# 正式环境https://x19apigatewayobt.nie.netease.com/pcauth/check
# 测试环境https://x19apigatewayexpr.nie.netease.com/pcauth/check
# 另有外网测试认证接口对应接入test版bc认证通常情况下不使用需要启用时会另行沟通。
# http://x19authtest.nie.netease.com/check
auth-url = "http://192.168.46.50:9999/check"
# 网络服游戏 id
# 在开发者平台中可以查看
game-id = "77140593557373952"
[servers] [servers]
# Configure your servers here. Each key represents the server's name, and the value # Configure your servers here. Each key represents the server's name, and the value
# represents the IP address of the server to connect to. # represents the IP address of the server to connect to.
@@ -159,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");
}
}