Compare commits
56 Commits
b06af3718c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 873fca763d | |||
| e99407132f | |||
| 67d63faeca | |||
| 871b053561 | |||
| 81deb1fff8 | |||
| 59560ebad1 | |||
| 67a6600c05 | |||
| f3e30558e4 | |||
| e46ab6ad7d | |||
| b6fd48f282 | |||
| c2edc26d8e | |||
| 76417b13d4 | |||
| 47f36e3ff9 | |||
| 10e75b6d55 | |||
| fe69214e77 | |||
| 020c7fe6f5 | |||
| 44bc15db40 | |||
| 549b8d624e | |||
| 669fda298c | |||
| 75bb48d00e | |||
| 21ecd344ba | |||
| 8c8162dbf6 | |||
| 5e20ec19ff | |||
| 5eb83760cd | |||
| 8fea43d6ba | |||
| 678c7aa3a4 | |||
| e13c8c340f | |||
| dc659538d3 | |||
| eb099d1220 | |||
| 1ad1f3b215 | |||
| 063065b21a | |||
| 00016ba4e1 | |||
| 11834de220 | |||
| b411a0fa09 | |||
| 1561ba2e38 | |||
| 91a61643bd | |||
| b6e05cb0b9 | |||
| 1507b91463 | |||
| bd2bb6325e | |||
| 3f0a85d794 | |||
| 74d05211d6 | |||
| 7ad06614fe | |||
| 163a85a468 | |||
| a51711e4bb | |||
| ae312339a3 | |||
| a429bb53ce | |||
| a549880df1 | |||
| 9c1be72db0 | |||
| 747f70d80a | |||
| b482443e79 | |||
| 676ec9cb21 | |||
| aae97dce3d | |||
| c72a3eefde | |||
| 86b88cf4b7 | |||
| 7ffa43f0e2 | |||
| b3e218bd7d |
6
.github/workflows/gradle.yml
vendored
6
.github/workflows/gradle.yml
vendored
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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::"))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/"
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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")) {
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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{"
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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(() -> {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,4 +62,6 @@ velocity.command.dump-server-error=An error occurred on the Velocity servers and
|
||||
velocity.command.dump-offline=Likely cause: Invalid system DNS settings or no internet connection
|
||||
velocity.command.send-usage=/send <player> <server>
|
||||
# Kick
|
||||
velocity.kick.shutdown=Proxy shutting down.
|
||||
velocity.kick.shutdown=Proxy shutting down.
|
||||
velocity.kick.command-rate-limit=You are sending too many commands too quickly.
|
||||
velocity.kick.tab-complete-rate-limit=You are sending too many tab complete requests too quickly.
|
||||
@ -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=Налични сървъри\:
|
||||
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user