Compare commits

...

56 Commits

Author SHA1 Message Date
873fca763d Merge remote-tracking branch 'upstream/dev/3.0.0'
All checks were successful
SteamWarCI Build successful
2025-07-28 18:34:52 +02:00
e99407132f Add version information for 1.21.8 (#1612) 2025-07-18 03:26:24 +01:00
67d63faeca Reapply "Disable io_uring transport by default"
All checks were successful
SteamWarCI Build successful
This reverts commit 11834de220.
2025-07-10 10:42:11 +02:00
871b053561 Merge remote-tracking branch 'upstream/dev/3.0.0' 2025-07-10 10:41:50 +02:00
81deb1fff8 Update maven publishing repo name 2025-06-30 15:22:39 +01:00
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
67a6600c05 New Crowdin updates (#1283)
* New translations messages.properties (Bulgarian)

* New translations messages.properties (Spanish)
2025-06-29 23:31:24 -07:00
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
e46ab6ad7d build: publish using fill (#1599) 2025-06-28 16:12:00 -07:00
b6fd48f282 Update to adventure 4.22.0 (#1595) 2025-06-27 16:56:05 +01:00
c2edc26d8e Merge pull request 'Update Velocity' (#2) from update into master
All checks were successful
SteamWarCI Build successful
Reviewed-on: #2
2025-06-26 23:11:02 +02:00
76417b13d4 Merge branch 'updatev2' into update
All checks were successful
SteamWarCI Build successful
2025-06-26 22:53:32 +02:00
47f36e3ff9 Merge remote-tracking branch 'upstream/dev/3.0.0' into updatev2
All checks were successful
SteamWarCI Build successful
2025-06-26 22:52:31 +02:00
10e75b6d55 feat: property support for max clientside channels (#1557) 2025-06-21 10:05:04 +02:00
fe69214e77 Downgrade netty (Fixes #1591) 2025-06-17 18:46:22 +01:00
020c7fe6f5 1.21.6 (#1580) 2025-06-17 14:52:50 +01:00
44bc15db40 Add system property to skip packet compression threshold validation 2025-06-14 20:29:12 +01:00
549b8d624e Update some dependencies (#1576) 2025-06-14 19:53:22 +01:00
669fda298c Only apply max known pack restrictions Serverbound 2025-06-11 10:12:48 +01:00
75bb48d00e Merge remote-tracking branch 'upstream/dev/3.0.0' into updatev2
All checks were successful
SteamWarCI Build successful
2025-06-03 23:12:58 +02:00
AR
21ecd344ba Fix HoverEvent.showEntity() in protocol versions prior to 1.21.5 (#1578) 2025-05-25 21:24:21 +01:00
8c8162dbf6 Discard known packs if we don't have a target 2025-05-24 18:28:54 +01:00
5e20ec19ff Stabilize and expose suggestions API (#1406)
* Expose suggestions API

* Improve javadoc of suggestions api
2025-05-23 14:23:38 +01:00
5eb83760cd Attempt to improve partial read situations during early connections 2025-05-22 14:26:48 +01:00
8fea43d6ba Add some means to quickly overlook deframing issues 2025-05-22 13:13:06 +01:00
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
e13c8c340f Increase limit to account for strings being strings 2025-05-09 22:17:04 +01:00
dc659538d3 Set netty allocator earlier and more globally 2025-05-09 20:19:29 +01:00
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
1ad1f3b215 Use pooled netty allocator instead of default adaptive allocator (#1570) 2025-05-08 11:30:55 +01:00
063065b21a fix: adventure 4.21.0 adaptation (#1569) 2025-05-08 09:55:16 +01:00
00016ba4e1 Validate handshake packet length early 2025-05-08 00:15:50 +01:00
11834de220 Revert "Disable io_uring transport by default"
All checks were successful
SteamWarCI Build successful
This reverts commit ae312339a3.
2025-05-02 20:49:11 +02:00
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
1561ba2e38 Bump adventure to 4.20.0 (#1544) 2025-04-28 16:13:48 -05:00
91a61643bd Revert "Disable io_uring transport by default"
All checks were successful
SteamWarCI Build successful
This reverts commit ae312339a3.
2025-04-27 20:24:41 +02:00
b6e05cb0b9 Refactor TCP Fast Open checks and update message identifiers.
All checks were successful
SteamWarCI Build successful
Removed transport type conditions for TCP Fast Open to streamline configuration usage. Added imports for new message identifiers in `ClientPlaySessionHandler`. Cleaned up Netty library definitions in `libs.versions.toml`.
2025-04-27 20:09:05 +02:00
1507b91463 Merge remote-tracking branch 'upstream/dev/3.0.0' into update
# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/network/TransportType.java
2025-04-27 19:53:15 +02:00
bd2bb6325e Validate state transition 2025-04-17 18:59:49 +01:00
3f0a85d794 Fix MinecraftChannelIdentifier parsing to align with vanilla (#1552) 2025-04-14 13:52:02 -04:00
74d05211d6 Also validate length before caring to invest time into processing 2025-04-12 16:52:31 +01:00
7ad06614fe Appease checkstyle gods 2025-04-12 16:22:06 +01:00
163a85a468 Merge branch 'cleanup/plugin-message-channel-handling' into dev/3.0.0 2025-04-12 16:20:21 +01:00
a51711e4bb Use an ImmutableList Builder 2025-04-12 16:20:07 +01:00
ae312339a3 Disable io_uring transport by default 2025-04-11 00:35:49 -04:00
a429bb53ce Merge remote-tracking branch 'origin/dev/3.0.0' into cleanup/plugin-message-channel-handling 2025-04-09 10:06:40 +01:00
a549880df1 Bump to Netty 4.2.0 (#1380) 2025-04-09 01:21:08 -04:00
9c1be72db0 Fix tests 2025-04-06 21:25:08 +01:00
747f70d80a Appease checkstyle 2025-04-06 21:25:03 +01:00
b482443e79 Fix spot 2025-04-06 21:22:22 +01:00
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
c72a3eefde Check if kicking on command rate limit is enabled 2025-04-03 22:51:26 +01:00
86b88cf4b7 fix: typo 2025-04-03 18:59:16 +05:30
7ffa43f0e2 feat: implement command rate limiter (#1524) 2025-04-03 13:26:00 +01:00
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
57 changed files with 999 additions and 299 deletions

View File

@ -14,10 +14,10 @@ jobs:
persist-credentials: false
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Set up JDK 17
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: 17
distribution: 'temurin'
java-version: 21
distribution: 'zulu'
- name: Build with Gradle
run: ./gradlew build

View File

@ -7,8 +7,10 @@
package com.velocitypowered.api.command;
import com.mojang.brigadier.suggestion.Suggestions;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.Nullable;
@ -116,6 +118,27 @@ public interface CommandManager {
*/
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
* on this manager.

View File

@ -90,7 +90,9 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21(767, "1.21", "1.21.1"),
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
MINECRAFT_1_21_4(769, "1.21.4"),
MINECRAFT_1_21_5(770, /*1073742067,*/ "1.21.5");
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;

View File

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

View File

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

View File

@ -517,6 +517,10 @@ public final class ServerPing {
*/
public static final class SamplePlayer {
public static final SamplePlayer ANONYMOUS = new SamplePlayer(
"Anonymous Player",
new UUID(0L, 0L)
);
private final String name;
private final UUID id;

View File

@ -47,17 +47,25 @@ class MinecraftChannelIdentifierTest {
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
void fromIdentifierThrowsOnBadValues() {
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("he/llo:wor/ld")),
() -> assertThrows(IllegalArgumentException.class, () -> from("hello::"))
);
}
}

View File

@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.withType
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 {
exec {
val execOps = objects.newInstance<Injected>().execOps
execOps.exec {
executable = "git"
args = listOf("rev-parse", "HEAD")
standardOutput = it

View File

@ -8,7 +8,7 @@ extensions.configure<PublishingExtension> {
maven {
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 releasesRepoUrl = "$base-releases/"
val snapshotsRepoUrl = "$base-snapshots/"

View File

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

View File

@ -2,19 +2,20 @@
configurate3 = "3.7.3"
configurate4 = "4.1.2"
flare = "2.0.1"
log4j = "2.24.1"
netty = "4.1.119.Final"
log4j = "2.24.3"
netty = "4.2.1.Final"
[plugins]
fill = "io.papermc.fill.gradle:1.0.3"
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"
[libraries]
adventure-bom = "net.kyori:adventure-bom:4.19.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.19.0"
adventure-bom = "net.kyori:adventure-bom:4.23.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"
asm = "org.ow2.asm:asm:9.7.1"
asm = "org.ow2.asm:asm:9.8"
auto-service = "com.google.auto.service:auto-service:1.0.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
@ -33,7 +34,7 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.15"
flare-core = { module = "space.vectrix.flare:flare", 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"
junit = "org.junit.jupiter:junit-jupiter:5.10.2"
jspecify = "org.jspecify:jspecify:0.3.0"
@ -53,10 +54,10 @@ netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "
netty-codec-http = { module = "io.netty:netty-codec-http", 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-iouring = { module = "io.netty.incubator:netty-incubator-transport-native-io_uring", version = "0.0.25.Final" }
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"
slf4j = "org.slf4j:slf4j-api:2.0.12"
slf4j = "org.slf4j:slf4j-api:2.0.17"
snakeyaml = "org.yaml:snakeyaml:1.33"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

View File

@ -1,9 +1,11 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import io.papermc.fill.model.BuildChannel
plugins {
application
id("velocity-init-manifest")
alias(libs.plugins.shadow)
alias(libs.plugins.fill)
}
application {
@ -100,6 +102,7 @@ tasks {
runShadow {
workingDir = file("run").also(File::mkdirs)
standardInput = System.`in`
jvmArgs("-Dvelocity.packet-decode-logging=true")
}
named<JavaExec>("run") {
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 {
implementation(project(":velocity-api"))
implementation(project(":velocity-native"))

View File

@ -47,6 +47,11 @@ public class Velocity {
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
// override this if desired.
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {

View File

@ -75,11 +75,13 @@ import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.http.HttpClient;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -103,7 +105,7 @@ import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component;
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.Logger;
import org.bstats.MetricsBase;
@ -162,7 +164,9 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
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 VelocityScheduler scheduler;
private final VelocityChannelRegistrar channelRegistrar = new VelocityChannelRegistrar();
@ -295,6 +299,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
}
ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit());
commandRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getCommandRatelimit());
tabCompleteRateLimiter = Ratelimiters.createWithMilliseconds(configuration.getTabCompleteRatelimit());
loadPlugins();
// Go ahead and fire the proxy initialization event. We block since plugins should have a chance
@ -332,8 +338,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
}
private void registerTranslations() {
final TranslationRegistry translationRegistry = TranslationRegistry
.create(Key.key("velocity", "translations"));
final TranslationStore.StringBased<MessageFormat> translationRegistry =
TranslationStore.messageFormat(Key.key("velocity", "translations"));
translationRegistry.defaultLocale(Locale.US);
try {
ResourceUtils.visitResources(VelocityServer.class, path -> {
@ -654,10 +660,18 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
return cm.createHttpClient();
}
public Ratelimiter getIpAttemptLimiter() {
public @MonotonicNonNull Ratelimiter<InetAddress> getIpAttemptLimiter() {
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.
*

View File

@ -300,27 +300,14 @@ public class VelocityCommandManager implements CommandManager {
);
}
/**
* 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
*/
@Override
public CompletableFuture<List<String>> offerSuggestions(final CommandSource source,
final String cmdLine) {
return offerBrigadierSuggestions(source, cmdLine)
.thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText));
}
/**
* 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
*/
@Override
public CompletableFuture<Suggestions> offerBrigadierSuggestions(
final CommandSource source, final String cmdLine) {
Preconditions.checkNotNull(source, "source");

View File

@ -78,6 +78,8 @@ public class VelocityConfiguration implements ProxyConfig {
private boolean onlineModeKickExistingPlayers = false;
@Expose
private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED;
@Expose
private boolean samplePlayersInPing = false;
private final Servers servers;
private final ForcedHosts forcedHosts;
@Expose
@ -105,8 +107,9 @@ public class VelocityConfiguration implements ProxyConfig {
boolean preventClientProxyConnections, boolean announceForge,
PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret,
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
boolean enablePlayerAddressLogging, Servers servers, ForcedHosts forcedHosts,
Advanced advanced, Query query, Metrics metrics, boolean forceKeyAuthentication) {
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
boolean forceKeyAuthentication) {
this.bind = bind;
this.motd = motd;
this.showMaxPlayers = showMaxPlayers;
@ -117,6 +120,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.forwardingSecret = forwardingSecret;
this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers;
this.pingPassthrough = pingPassthrough;
this.samplePlayersInPing = samplePlayersInPing;
this.enablePlayerAddressLogging = enablePlayerAddressLogging;
this.servers = servers;
this.forcedHosts = forcedHosts;
@ -230,6 +234,11 @@ public class VelocityConfiguration implements ProxyConfig {
valid = false;
}
if (advanced.commandRateLimit < 0) {
logger.error("Invalid command rate limit {}", advanced.commandRateLimit);
valid = false;
}
loadFavicon();
return valid;
@ -351,6 +360,31 @@ public class VelocityConfiguration implements ProxyConfig {
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() {
return advanced.isProxyProtocol();
}
@ -371,6 +405,10 @@ public class VelocityConfiguration implements ProxyConfig {
return pingPassthrough;
}
public boolean getSamplePlayersInPing() {
return samplePlayersInPing;
}
public boolean isPlayerAddressLoggingEnabled() {
return enablePlayerAddressLogging;
}
@ -507,6 +545,8 @@ public class VelocityConfiguration implements ProxyConfig {
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
PingPassthroughMode.DISABLED);
final boolean samplePlayersInPing = config.getOrElse("sample-players-in-ping", false);
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
final boolean onlineMode = config.getOrElse("online-mode", true);
@ -537,6 +577,7 @@ public class VelocityConfiguration implements ProxyConfig {
forwardingSecret,
kickExisting,
pingPassthroughMode,
samplePlayersInPing,
enablePlayerAddressLogging,
new Servers(serversConfig),
new ForcedHosts(forcedHostsConfig),
@ -722,6 +763,16 @@ public class VelocityConfiguration implements ProxyConfig {
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() {
}
@ -748,6 +799,11 @@ public class VelocityConfiguration implements ProxyConfig {
this.logPlayerConnections = config.getOrElse("log-player-connections", true);
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);
}
}
@ -815,6 +871,26 @@ public class VelocityConfiguration implements ProxyConfig {
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
public String toString() {
return "Advanced{"

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.MinecraftDecoder;
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.PlayPacketQueueInboundHandler;
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 final Channel channel;
public boolean pendingConfigurationSwitch = false;
private SocketAddress remoteAddress;
private StateRegistry state;
private Map<StateRegistry, MinecraftSessionHandler> sessionHandlers;
@ -367,6 +369,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
ensureInEventLoop();
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.
final MinecraftEncoder minecraftEncoder = this.channel.pipeline()
.get(MinecraftEncoder.class);

View File

@ -49,6 +49,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionMessages;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry;
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.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
@ -151,6 +152,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
MinecraftConnection smc = serverConn.ensureConnected();
smc.setAutoReading(false);
// 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);
serverConn.getPlayer().switchToConfigState();
return true;

View File

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

View File

@ -40,6 +40,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry;
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.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
@ -232,6 +233,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
final ConnectedPlayer player = serverConn.getPlayer();
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);
//noinspection DataFlowIssue
configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> {

View File

@ -170,7 +170,11 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(KnownPacksPacket packet) {
callConfigurationEvent().thenRun(() -> {
player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet);
VelocityServerConnection targetServer =
player.getConnectionInFlightOrConnectedServer();
if (targetServer != null) {
targetServer.ensureConnected().write(packet);
}
}).exceptionally(ex -> {
logger.error("Error forwarding known packs response to backend:", ex);
return null;

View File

@ -74,6 +74,7 @@ import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import com.velocitypowered.proxy.util.CharacterUtil;
import com.velocitypowered.proxy.util.except.QuietRuntimeException;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
@ -113,6 +114,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private CompletableFuture<Void> configSwitchFuture;
private int failedTabCompleteAttempts;
/**
* Constructs a client play session handler.
*
@ -160,7 +163,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public void activated() {
configSwitchFuture = new CompletableFuture<>();
Collection<String> channels =
Collection<ChannelIdentifier> channels =
server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion());
if (!channels.isEmpty()) {
PluginMessagePacket register = constructChannelsPacket(player.getProtocolVersion(), channels);
@ -308,20 +311,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
logger.warn("A plugin message was received while the backend server was not "
+ "ready. Channel: {}. Packet discarded.", packet.getChannel());
} else if (PluginMessageUtil.isRegister(packet)) {
List<String> channels = PluginMessageUtil.getChannels(packet);
List<ChannelIdentifier> channelIdentifiers = new ArrayList<>();
for (String channel : channels) {
try {
channelIdentifiers.add(MinecraftChannelIdentifier.from(channel));
} catch (IllegalArgumentException e) {
channelIdentifiers.add(new LegacyChannelIdentifier(channel));
}
}
List<ChannelIdentifier> channels =
PluginMessageUtil.getChannels(this.player.getClientsideChannels().size(), packet,
this.player.getProtocolVersion());
player.getClientsideChannels().addAll(channels);
server.getEventManager()
.fireAndForget(
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channelIdentifiers)));
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain());
} else if (PluginMessageUtil.isUnregister(packet)) {
player.getClientsideChannels()
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()));
backendConn.write(packet.retain());
} else if (PluginMessageUtil.isMcBrand(packet)) {
String brand = PluginMessageUtil.readBrandMessage(packet.content());
@ -377,10 +377,14 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override
public boolean handle(FinishedUpdatePacket packet) {
if (!player.getConnection().pendingConfigurationSwitch) {
throw new QuietRuntimeException("Not expecting reconfiguration");
}
// Complete client switch
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
VelocityServerConnection serverConnection = player.getConnectedServer();
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
server.getEventManager()
.fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
if (serverConnection != null) {
MinecraftConnection smc = serverConnection.ensureConnected();
CompletableFuture.runAsync(() -> {
@ -567,11 +571,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Tell the server about the proxy's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
final Collection<String> channels = server.getChannelRegistrar()
final Collection<ChannelIdentifier> channels = server.getChannelRegistrar()
.getChannelsForProtocol(serverMc.getProtocolVersion());
if (!channels.isEmpty()) {
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.
PluginMessagePacket pm;
@ -653,6 +661,17 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
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)
.thenAcceptAsync(suggestions -> {
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.DurationUtils;
import com.velocitypowered.proxy.util.TranslatableMapper;
import com.velocitypowered.proxy.util.collect.CappedSet;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
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.Type;
import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.resource.ResourcePackInfoLike;
import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike;
@ -144,13 +146,23 @@ import org.jetbrains.annotations.NotNull;
public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, KeyIdentifiable,
VelocityInboundConnection {
public static final int MAX_CLIENTSIDE_PLUGIN_CHANNELS = Integer.getInteger("velocity.max-clientside-plugin-channels", 1024);
private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE =
PlainTextComponentSerializer.builder().flattener(TranslatableMapper.FLATTENER).build();
static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED;
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.
*/
@ -173,19 +185,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private final InternalTabList tabList;
private final VelocityServer server;
private ClientConnectionPhase connectionPhase;
private final Collection<ChannelIdentifier> clientsideChannels;
private final CompletableFuture<Void> teardownFuture = new CompletableFuture<>();
private @MonotonicNonNull List<String> serversToTry = null;
private final ResourcePackHandler resourcePackHandler;
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 Locale effectiveLocale;
private final @Nullable IdentifiedKey playerKey;
@ -205,6 +210,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED;
this.connectionPhase = connection.getType().getInitialClientPhase();
this.onlineMode = onlineMode;
this.clientsideChannels = CappedSet.create(MAX_CLIENTSIDE_PLUGIN_CHANNELS);
if (connection.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3)) {
this.tabList = new VelocityTabList(this);
@ -253,7 +259,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
@Override
public @NonNull Identity identity() {
return this.identity;
return Identity.identity(this.getUniqueId());
}
@Override
@ -359,7 +365,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
@Override
public @NotNull Pointers pointers() {
return this.pointers;
return POINTERS_SUPPLIER.view(this);
}
@Override
@ -392,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
* @return the translated message
*/
public Component translateMessage(Component message) {
Locale locale = ClosestLocaleMatcher.INSTANCE
.lookupClosest(getEffectiveLocale() == null ? Locale.getDefault() : getEffectiveLocale());
Locale locale = this.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);
}
@ -1314,6 +1326,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
connection.write(BundleDelimiterPacket.INSTANCE);
}
connection.write(StartUpdatePacket.INSTANCE);
connection.pendingConfigurationSwitch = true;
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
// Make sure we don't send any play packets to the player after update start
connection.addPlayPacketQueueHandler();
@ -1342,19 +1355,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.connectionPhase = connectionPhase;
}
/**
* Return all the plugin message channels that registered by client.
*
* @return the channels
*/
public Collection<ChannelIdentifier> getClientsideChannels() {
return clientsideChannels;
}
@Override
public @Nullable IdentifiedKey getIdentifiedKey() {
return playerKey;
}
private class IdentityImpl implements Identity {
@Override
public @NonNull UUID uuid() {
return ConnectedPlayer.this.getUniqueId();
}
}
@Override
public ProtocolState getProtocolState() {
return connection.getState().toProtocolState();

View File

@ -30,10 +30,12 @@ import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.server.VelocityRegisteredServer;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* Common utilities for handling server list ping results.
@ -51,11 +53,27 @@ public class ServerListPingHandler {
version = ProtocolVersion.MAXIMUM_VERSION;
}
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(
new ServerPing.Version(version.getProtocol(),
"Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING),
new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(),
ImmutableList.of()),
samplePlayers),
configuration.getMotd(),
configuration.getFavicon().orElse(null),
configuration.isAnnounceForge() ? ModInfo.DEFAULT : null

View File

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

View File

@ -104,7 +104,7 @@ public final class ConnectionManager {
.childOption(ChannelOption.IP_TOS, 0x18)
.localAddress(address);
if (transportType.supportsTcpFastOpenServer() && server.getConfiguration().useTcpFastOpen()) {
if (server.getConfiguration().useTcpFastOpen()) {
bootstrap.option(ChannelOption.TCP_FASTOPEN, 3);
}
@ -197,7 +197,7 @@ public final class ConnectionManager {
this.server.getConfiguration().getConnectTimeout())
.group(group == null ? this.workerGroup : group)
.resolver(this.resolver.asGroup());
if (transportType.supportsTcpFastOpenClient() && server.getConfiguration().useTcpFastOpen()) {
if (server.getConfiguration().useTcpFastOpen()) {
bootstrap.option(ChannelOption.TCP_FASTOPEN_CONNECT, true);
}
return bootstrap;

View File

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

View File

@ -20,27 +20,32 @@ package com.velocitypowered.proxy.network;
import com.velocitypowered.proxy.util.concurrent.VelocityNettyThreadFactory;
import io.netty.channel.ChannelFactory;
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.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollIoHandler;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.kqueue.KQueue;
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.KQueueSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.nio.NioIoHandler;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.incubator.channel.uring.*;
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.function.BiFunction;
import java.util.function.Supplier;
/**
* Enumerates the supported transports for Velocity.
@ -49,50 +54,36 @@ public enum TransportType {
NIO("NIO", NioServerSocketChannel::new,
NioSocketChannel::new,
NioDatagramChannel::new,
(name, type) -> new NioEventLoopGroup(0, createThreadFactory(name, type)),
false,
false),
NioIoHandler::newFactory),
EPOLL("epoll", EpollServerSocketChannel::new,
EpollSocketChannel::new,
EpollDatagramChannel::new,
(name, type) -> new EpollEventLoopGroup(0, createThreadFactory(name, type)),
Epoll.isTcpFastOpenServerSideAvailable(),
Epoll.isTcpFastOpenClientSideAvailable()),
IO_URING("io_uring", IOUringServerSocketChannel::new,
IOUringSocketChannel::new,
IOUringDatagramChannel::new,
(name, type) -> new IOUringEventLoopGroup(0, createThreadFactory(name, type)),
IOUring.isTcpFastOpenServerSideAvailable(),
IOUring.isTcpFastOpenClientSideAvailable()),
EpollIoHandler::newFactory),
KQUEUE("kqueue", KQueueServerSocketChannel::new,
KQueueSocketChannel::new,
KQueueDatagramChannel::new,
(name, type) -> new KQueueEventLoopGroup(0, createThreadFactory(name, type)),
KQueue.isTcpFastOpenServerSideAvailable(),
KQueue.isTcpFastOpenClientSideAvailable());
KQueueIoHandler::newFactory),
IO_URING("io_uring", IoUringServerSocketChannel::new,
IoUringSocketChannel::new,
IoUringDatagramChannel::new,
IoUringIoHandler::newFactory);
final String name;
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory;
final ChannelFactory<? extends SocketChannel> socketChannelFactory;
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory;
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory;
final boolean supportsTcpFastOpenServer;
final boolean supportsTcpFastOpenClient;
final Supplier<IoHandlerFactory> ioHandlerFactorySupplier;
TransportType(final String name,
final ChannelFactory<? extends ServerSocketChannel> serverSocketChannelFactory,
final ChannelFactory<? extends SocketChannel> socketChannelFactory,
final ChannelFactory<? extends DatagramChannel> datagramChannelFactory,
final BiFunction<String, Type, EventLoopGroup> eventLoopGroupFactory,
final boolean supportsTcpFastOpenServer,
final boolean supportsTcpFastOpenClient) {
final Supplier<IoHandlerFactory> ioHandlerFactorySupplier) {
this.name = name;
this.serverSocketChannelFactory = serverSocketChannelFactory;
this.socketChannelFactory = socketChannelFactory;
this.datagramChannelFactory = datagramChannelFactory;
this.eventLoopGroupFactory = eventLoopGroupFactory;
this.supportsTcpFastOpenServer = supportsTcpFastOpenServer;
this.supportsTcpFastOpenClient = supportsTcpFastOpenClient;
this.ioHandlerFactorySupplier = ioHandlerFactorySupplier;
}
@Override
@ -100,16 +91,15 @@ public enum TransportType {
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) {
return this.eventLoopGroupFactory.apply(this.name, type);
}
public boolean supportsTcpFastOpenServer() {
return supportsTcpFastOpenServer;
}
public boolean supportsTcpFastOpenClient() {
return supportsTcpFastOpenClient;
return new MultiThreadIoEventLoopGroup(
0, createThreadFactory(this.name, type), this.ioHandlerFactorySupplier.get());
}
private static ThreadFactory createThreadFactory(final String name, final Type type) {
@ -126,7 +116,7 @@ public enum TransportType {
return NIO;
}
if(IOUring.isAvailable()) {
if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) {
return IO_URING;
}

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.json.JSONOptions;
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.
@ -60,14 +60,17 @@ public enum ProtocolUtils {
.downsampleColors()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionState.optionState()
OptionSchema.globalSchema().stateBuilder()
// before 1.16
.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
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, 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();
@ -75,14 +78,37 @@ public enum ProtocolUtils {
GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionState.optionState()
OptionSchema.globalSchema().stateBuilder()
// after 1.16
.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
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, 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();
@ -90,14 +116,18 @@ public enum ProtocolUtils {
GsonComponentSerializer.builder()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options(
OptionState.optionState()
OptionSchema.globalSchema().stateBuilder()
// after 1.16
.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
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, 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.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.FALSE)
.build()
)
.build();
@ -713,9 +743,12 @@ public enum ProtocolUtils {
* @return the appropriate {@link GsonComponentSerializer}
*/
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;
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
return PRE_1_21_5_SERIALIZER;
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_16)) {
return PRE_1_20_3_SERIALIZER;
}

View File

@ -40,6 +40,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
@ -257,7 +258,8 @@ public enum StateRegistry {
map(0x09, MINECRAFT_1_19_4, false),
map(0x0A, MINECRAFT_1_20_2, 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(
LegacyChatPacket.class,
LegacyChatPacket::new,
@ -270,7 +272,8 @@ public enum StateRegistry {
ChatAcknowledgementPacket.class,
ChatAcknowledgementPacket::new,
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,
map(0x03, MINECRAFT_1_19, false),
map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false));
@ -280,16 +283,19 @@ public enum StateRegistry {
serverbound.register(SessionPlayerCommandPacket.class, SessionPlayerCommandPacket::new,
map(0x04, MINECRAFT_1_19_3, 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,
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(
SessionPlayerChatPacket.class,
SessionPlayerChatPacket::new,
map(0x05, MINECRAFT_1_19_3, 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(
ClientSettingsPacket.class,
ClientSettingsPacket::new,
@ -304,11 +310,13 @@ public enum StateRegistry {
map(0x08, MINECRAFT_1_19_4, false),
map(0x09, MINECRAFT_1_20_2, 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(
ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new,
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(
PluginMessagePacket.class,
PluginMessagePacket::new,
@ -326,7 +334,8 @@ public enum StateRegistry {
map(0x0F, MINECRAFT_1_20_2, false),
map(0x10, MINECRAFT_1_20_3, 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(
KeepAlivePacket.class,
KeepAlivePacket::new,
@ -345,7 +354,8 @@ public enum StateRegistry {
map(0x14, MINECRAFT_1_20_2, false),
map(0x15, MINECRAFT_1_20_3, 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(
ResourcePackResponsePacket.class,
ResourcePackResponsePacket::new,
@ -362,12 +372,14 @@ public enum StateRegistry {
map(0x28, MINECRAFT_1_20_3, false),
map(0x2B, MINECRAFT_1_20_5, 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(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, 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(
BossBarPacket.class,

View File

@ -39,6 +39,7 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap")
? 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 final VelocityCompressor compressor;
@ -52,9 +53,11 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int claimedUncompressedSize = ProtocolUtils.readVarInt(in);
if (claimedUncompressedSize == 0) {
int actualUncompressedSize = in.readableBytes();
checkFrame(actualUncompressedSize < threshold, "Actual uncompressed size %s is greater than"
+ " threshold %s", actualUncompressedSize, threshold);
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.
out.add(in.retain());
return;

View File

@ -135,7 +135,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
private String getExtraConnectionDetail(int packetId) {
return "Direction " + direction + " Protocol " + registry.version + " State " + state
+ " ID " + Integer.toHexString(packetId);
+ " ID 0x" + Integer.toHexString(packetId);
}
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 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.QuietRuntimeException;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.CorruptedFrameException;
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.
*/
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 =
new QuietDecoderException("Bad packet length");
private static final 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
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)
@ -62,6 +92,43 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
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
if (length > 0) {
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.
*
@ -141,4 +218,26 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
}
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.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
public class AvailableCommandsPacket implements MinecraftPacket {
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_LITERAL = 0x01;
@ -65,6 +67,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
private static final byte FLAG_EXECUTABLE = 0x04;
private static final byte FLAG_IS_REDIRECT = 0x08;
private static final byte FLAG_HAS_SUGGESTIONS = 0x10;
private static final byte FLAG_IS_RESTRICTED = 0x20;
private @MonotonicNonNull RootCommandNode<CommandSource> rootNode;
@ -146,6 +149,9 @@ public class AvailableCommandsPacket implements MinecraftPacket {
if (node.getCommand() != null) {
flags |= FLAG_EXECUTABLE;
}
if (node.getRequirement() == PLACEHOLDER_REQUIREMENT) {
flags |= FLAG_IS_RESTRICTED;
}
if (node instanceof LiteralCommandNode<?>) {
flags |= NODE_TYPE_LITERAL;
@ -289,6 +295,11 @@ public class AvailableCommandsPacket implements MinecraftPacket {
args.executes(PLACEHOLDER_COMMAND);
}
// If restricted, add empty requirement
if ((flags & FLAG_IS_RESTRICTED) != 0) {
args.requires(PLACEHOLDER_REQUIREMENT);
}
this.built = args.build();
}
}

View File

@ -106,4 +106,16 @@ public class HandshakePacket implements MinecraftPacket {
public boolean handle(MinecraftSessionHandler handler) {
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

@ -23,6 +23,7 @@ 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_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.mapSet;
import static com.velocitypowered.proxy.protocol.packet.brigadier.DoubleArgumentPropertySerializer.DOUBLE;
@ -164,6 +165,7 @@ public class ArgumentPropertyRegistry {
return i;
}
}
throw new IllegalArgumentException("Argument type identifier " + id + " unknown.");
} else {
String identifier = ProtocolUtils.readString(buf);
for (ArgumentIdentifier i : byIdentifier.keySet()) {
@ -207,68 +209,79 @@ public class ArgumentPropertyRegistry {
empty(id("minecraft:item_stack", mapSet(MINECRAFT_1_19, 14)));
empty(id("minecraft:item_predicate", mapSet(MINECRAFT_1_19, 15)));
empty(id("minecraft:color", mapSet(MINECRAFT_1_19, 16)));
empty(id("minecraft:component", mapSet(MINECRAFT_1_19, 17)));
empty(id("minecraft:style", 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:nbt_compound_tag", 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_path", 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_criteria", 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:particle", 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:rotation", 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:score_holder", mapSet(MINECRAFT_1_20_3, 30), mapSet(MINECRAFT_1_19, 29)), ByteArgumentPropertySerializer.BYTE);
empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_20_3, 31), mapSet(MINECRAFT_1_19, 30)));
empty(id("minecraft:team", mapSet(MINECRAFT_1_20_3, 32), mapSet(MINECRAFT_1_19, 31)));
empty(id("minecraft:item_slot", mapSet(MINECRAFT_1_20_3, 33), mapSet(MINECRAFT_1_19, 32)));
empty(id("minecraft:item_slots", mapSet(MINECRAFT_1_20_5, 34))); // added 1.20.5
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:component", mapSet(MINECRAFT_1_21_6, 18), mapSet(MINECRAFT_1_19, 17)));
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_21_6, 20), mapSet(MINECRAFT_1_20_3, 19), mapSet(MINECRAFT_1_19, 18)));
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_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_21_6, 23), mapSet(MINECRAFT_1_20_3, 22), mapSet(MINECRAFT_1_19, 21)));
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_21_6, 25), mapSet(MINECRAFT_1_20_3, 24), mapSet(MINECRAFT_1_19, 23)));
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_21_6, 27), mapSet(MINECRAFT_1_20_3, 26), mapSet(MINECRAFT_1_19, 25)));
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_21_6, 29), mapSet(MINECRAFT_1_20_3, 28), mapSet(MINECRAFT_1_19, 27)));
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_21_6, 31), mapSet(MINECRAFT_1_20_3, 30), mapSet(MINECRAFT_1_19, 29)),
ByteArgumentPropertySerializer.BYTE);
empty(id("minecraft:swizzle", mapSet(MINECRAFT_1_21_6, 32), mapSet(MINECRAFT_1_20_3, 31), mapSet(MINECRAFT_1_19, 30)));
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_slot", mapSet(MINECRAFT_1_21_6, 34), mapSet(MINECRAFT_1_20_3, 33), mapSet(MINECRAFT_1_19, 32)));
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:function", mapSet(MINECRAFT_1_20_5, 36), mapSet(MINECRAFT_1_20_3, 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),
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),
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),
mapSet(MINECRAFT_1_19, 38)));
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_3, 34), mapSet(MINECRAFT_1_19, 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_3, 35), mapSet(MINECRAFT_1_19, 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_3, 36), mapSet(MINECRAFT_1_19, 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_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: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),
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:dimension", mapSet(MINECRAFT_1_21_6, 41), mapSet(MINECRAFT_1_20_5, 40), mapSet(MINECRAFT_1_20_3, 39),
mapSet(MINECRAFT_1_19_3, 38), mapSet(MINECRAFT_1_19, 41)));
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),
mapSet(MINECRAFT_1_19, 42)), TimeArgumentSerializer.TIME); // added in 1.14
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_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),
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", mapSet(MINECRAFT_1_21_6, 44), mapSet(MINECRAFT_1_20_5, 43), mapSet(MINECRAFT_1_20_3, 42),
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_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.Serializer.REGISTRY);
register(id("minecraft:resource", mapSet(MINECRAFT_1_20_5, 45), mapSet(MINECRAFT_1_20_3, 44), mapSet(MINECRAFT_1_19_3, 43),
mapSet(MINECRAFT_1_19, 44)),
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_3, 43), mapSet(MINECRAFT_1_19, 44)),
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.Serializer.REGISTRY);
register(id("minecraft:resource_selector", mapSet(MINECRAFT_1_21_5, 47)),
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_21_5, 48), mapSet(MINECRAFT_1_20_5, 47), mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19
empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_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_5, 50), mapSet(MINECRAFT_1_20_3, 49), mapSet(MINECRAFT_1_19_4, 47))); // 1.19.4
empty(id("minecraft:template_mirror", mapSet(MINECRAFT_1_21_6, 49), mapSet(MINECRAFT_1_21_5, 48), mapSet(MINECRAFT_1_20_5, 47),
mapSet(MINECRAFT_1_20_3, 46), mapSet(MINECRAFT_1_19, 45))); // 1.19
empty(id("minecraft:template_rotation", mapSet(MINECRAFT_1_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_21_5, 54),mapSet(MINECRAFT_1_20_5, 53), mapSet(MINECRAFT_1_20_3, 48), mapSet(MINECRAFT_1_19_4, 48),
mapSet(MINECRAFT_1_19, 47))); // added in 1.16
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_4, 48), mapSet(MINECRAFT_1_19, 47))); // added in 1.16
empty(id("minecraft:loot_table", mapSet(MINECRAFT_1_21_5, 51), mapSet(MINECRAFT_1_20_5, 50)));
empty(id("minecraft:loot_predicate", mapSet(MINECRAFT_1_21_5, 52), mapSet(MINECRAFT_1_20_5, 51)));
empty(id("minecraft:loot_modifier", mapSet(MINECRAFT_1_21_5, 53), mapSet(MINECRAFT_1_20_5, 52)));
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_21_6, 53), mapSet(MINECRAFT_1_21_5, 52), mapSet(MINECRAFT_1_20_5, 51)));
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
register(id("crossstitch:mod_argument", mapSet(MINECRAFT_1_19, -256)), ModArgumentProperty.class, MOD);

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.proxy.VelocityServer;
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 java.util.concurrent.CompletableFuture;
import net.kyori.adventure.text.Component;
public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPacket> {
public class KeyedCommandHandler extends RateLimitedCommandHandler<KeyedPlayerCommandPacket> {
private final ConnectedPlayer player;
private final VelocityServer server;
public KeyedCommandHandler(ConnectedPlayer player, VelocityServer server) {
super(player, server);
this.player = player;
this.server = server;
}

View File

@ -20,16 +20,18 @@ package com.velocitypowered.proxy.protocol.packet.chat.legacy;
import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.proxy.VelocityServer;
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.util.concurrent.CompletableFuture;
public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
public class LegacyCommandHandler extends RateLimitedCommandHandler<LegacyChatPacket> {
private final ConnectedPlayer player;
private final VelocityServer server;
public LegacyCommandHandler(ConnectedPlayer player, VelocityServer server) {
super(player, server);
this.player = player;
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.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
import java.util.concurrent.CompletableFuture;
import com.velocitypowered.proxy.protocol.packet.chat.RateLimitedCommandHandler;
import net.kyori.adventure.text.Component;
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 VelocityServer server;
public SessionCommandHandler(ConnectedPlayer player, VelocityServer server) {
super(player, server);
this.player = player;
this.server = server;
}

View File

@ -36,7 +36,7 @@ public class KnownPacksPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) {
final int packCount = ProtocolUtils.readVarInt(buf);
if (packCount > MAX_LENGTH_PACKS) {
if (direction == ProtocolUtils.Direction.SERVERBOUND && packCount > MAX_LENGTH_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.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.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
@ -85,13 +92,18 @@ public final class PluginMessageUtil {
.equals(UNREGISTER_CHANNEL);
}
private static final QuietDecoderException ILLEGAL_CHANNEL = new QuietDecoderException("Illegal channel");
/**
* 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
* @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");
checkArgument(isRegister(message) || isUnregister(message), "Unknown channel type %s",
message.getChannel());
@ -100,8 +112,28 @@ public final class PluginMessageUtil {
// has caused issues with 1.13+ compatibility. Just return an empty list.
return ImmutableList.of();
}
String channels = message.content().toString(StandardCharsets.UTF_8);
return ImmutableList.copyOf(channels.split("\0"));
String payload = message.content().toString(StandardCharsets.UTF_8);
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
*/
public static PluginMessagePacket constructChannelsPacket(ProtocolVersion protocolVersion,
Collection<String> channels) {
Collection<ChannelIdentifier> channels) {
checkNotNull(channels, "channels");
checkArgument(!channels.isEmpty(), "no channels specified");
String channelName = protocolVersion.noLessThan(ProtocolVersion.MINECRAFT_1_13)
? REGISTER_CHANNEL : REGISTER_CHANNEL_LEGACY;
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);
}
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.
*

View File

@ -114,7 +114,7 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud
server.createBootstrap(loop).handler(new ChannelInitializer<>() {
@Override
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(
pingOptions.getTimeout() == 0
? server.getConfiguration().getReadTimeout()

View File

@ -24,9 +24,6 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.flattener.ComponentFlattener;
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.
@ -43,25 +40,9 @@ public enum TranslatableMapper implements BiConsumer<TranslatableComponent, Cons
final TranslatableComponent translatableComponent,
final Consumer<Component> componentConsumer
) {
for (final Translator source : GlobalTranslator.translator().sources()) {
if (source instanceof TranslationRegistry registry
&& registry.contains(translatableComponent.key())) {
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;
}
final Locale locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault());
if (GlobalTranslator.translator().canTranslate(translatableComponent.key(), locale)) {
componentConsumer.accept(GlobalTranslator.render(translatableComponent, locale));
}
}
}

View File

@ -79,10 +79,10 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
*
* @return all legacy channel IDs
*/
public Collection<String> getLegacyChannelIds() {
Collection<String> ids = new HashSet<>();
public Collection<ChannelIdentifier> getLegacyChannelIds() {
Collection<ChannelIdentifier> ids = new HashSet<>();
for (ChannelIdentifier value : identifierMap.values()) {
ids.add(value.getId());
ids.add(new LegacyChannelIdentifier(value.getId()));
}
return ids;
}
@ -92,13 +92,13 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
*
* @return the channel IDs for Minecraft 1.13 and above
*/
public Collection<String> getModernChannelIds() {
Collection<String> ids = new HashSet<>();
public Collection<ChannelIdentifier> getModernChannelIds() {
Collection<ChannelIdentifier> ids = new HashSet<>();
for (ChannelIdentifier value : identifierMap.values()) {
if (value instanceof MinecraftChannelIdentifier) {
ids.add(value.getId());
ids.add(value);
} else {
ids.add(PluginMessageUtil.transformLegacyToModernChannel(value.getId()));
ids.add(MinecraftChannelIdentifier.from(PluginMessageUtil.transformLegacyToModernChannel(value.getId())));
}
}
return ids;
@ -114,7 +114,7 @@ public class VelocityChannelRegistrar implements ChannelRegistrar {
* @param protocolVersion the protocol version in use
* @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)) {
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.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import java.net.InetAddress;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
/**
* 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;
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
* @return true if we should allow the client, false if we should rate-limit
* @param key the object to rate limit
* @return true if we should allow the object, false if we should rate-limit
*/
@Override
public boolean attempt(InetAddress address) {
Preconditions.checkNotNull(address, "address");
public boolean attempt(@NotNull T key) {
long expectedNewValue = System.nanoTime() + timeoutNanos;
long last = expiringCache.get(address, (address1) -> expectedNewValue);
long last = expiringCache.get(key, (key1) -> expectedNewValue);
return expectedNewValue == last;
}
}

View File

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

View File

@ -17,18 +17,18 @@
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.
*
* @param address the address to rate limit
* @return true if allowed, false if not
*/
boolean attempt(InetAddress address);
* Attempts to rate-limit the object.
*
* @param key the object to rate limit
* @return true if we should allow the object, false if we should rate-limit
*/
boolean attempt(@NotNull T key);
}

View File

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

View File

@ -63,3 +63,5 @@ velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no in
velocity.command.send-usage=/send <player> <server>
# Kick
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.players-only=Само играчи могат да изпълняват тази команда.
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-too-many=Има прекалено много регистрирани сървъри. Използвайте TAB, за да видите всички налични сървъри.
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.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.illegal-chat-characters=Illegal characters in chat
velocity.error.illegal-chat-characters=Caracteres no válidos en el chat
# Commands
velocity.command.generic-error=Se ha producido un error al ejecutar este comando.
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-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.send-usage=/send <player> <server>
velocity.command.send-usage=/send <jugador> <servidor>
# Kick
velocity.kick.shutdown=Proxy shutting down.
velocity.kick.shutdown=El proxy se ha apagado.

View File

@ -66,6 +66,11 @@ kick-existing-players = false
# configuration is used if no servers could be contacted.
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
enable-player-address-logging = true
@ -151,6 +156,27 @@ accepts-transfers = false
# 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]
# Whether to enable responding to GameSpy 4 query responses or not.
enabled = false

View File

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