Merge remote-tracking branch 'upstream/dev/3.0.0'
SteamWarCI Build failed

# Conflicts:
#	proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java
This commit is contained in:
2026-05-21 08:23:49 +02:00
131 changed files with 2623 additions and 1356 deletions
+4 -4
View File
@@ -6,16 +6,16 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v5
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v5
with: with:
java-version: 21 java-version: 21
distribution: 'zulu' distribution: 'zulu'
+6
View File
@@ -34,3 +34,9 @@ and you can configure it from there.
Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity) Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity)
page. page.
# Localisation
Translations are handled using [Crowdin](https://papermc-io.crowdin.com/velocity).
If you want to translate a language not available on Crowdin,
you might want to ask in the [Discord](https://discord.gg/papermc) about it.
+2 -2
View File
@@ -59,11 +59,11 @@ tasks {
val o = options as StandardJavadocDocletOptions val o = options as StandardJavadocDocletOptions
o.encoding = "UTF-8" o.encoding = "UTF-8"
o.source = "17" o.source = "21"
o.use() o.use()
o.links( o.links(
"https://www.slf4j.org/apidocs/", "https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/", "https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/", "https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
"https://docs.oracle.com/en/java/javase/17/docs/api/", "https://docs.oracle.com/en/java/javase/17/docs/api/",
@@ -24,7 +24,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/ */
public final class SerializedPluginDescription { public final class SerializedPluginDescription {
public static final Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}"); public static final String ID_PATTERN_STRING = "[a-z][a-z0-9-_]{0,63}";
public static final Pattern ID_PATTERN = Pattern.compile(ID_PATTERN_STRING);
// @Nullable is used here to make GSON skip these in the serialized file // @Nullable is used here to make GSON skip these in the serialized file
private final String id; private final String id;
@@ -23,7 +23,7 @@ public interface CommandSource extends Audience, PermissionSubject {
* Sends a message with the MiniMessage format to this source. * Sends a message with the MiniMessage format to this source.
* *
* @param message MiniMessage content * @param message MiniMessage content
* @see <a href="https://docs.advntr.dev/minimessage/format.html">MiniMessage docs</a> * @see <a href="https://docs.papermc.io/adventure/minimessage/format/">MiniMessage docs</a>
* for more information on the format. * for more information on the format.
**/ **/
default void sendRichMessage(final @NotNull String message) { default void sendRichMessage(final @NotNull String message) {
@@ -31,14 +31,14 @@ public interface CommandSource extends Audience, PermissionSubject {
} }
/** /**
* Sends a message with the MiniMessage format to this source. * Sends a message with the MiniMessage format to this source.
* *
* @param message MiniMessage content * @param message MiniMessage content
* @param resolvers resolvers to use * @param resolvers resolvers to use
* @see <a href="https://docs.advntr.dev/minimessage/">MiniMessage docs</a> * @see <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a>
* and <a href="https://docs.advntr.dev/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a> * and <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
* for more information on the format. * for more information on the format.
**/ */
default void sendRichMessage( default void sendRichMessage(
final @NotNull String message, final @NotNull String message,
final @NotNull TagResolver @NotNull... resolvers final @NotNull TagResolver @NotNull... resolvers
@@ -11,6 +11,7 @@ import com.google.common.base.Preconditions;
import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.event.ResultedEvent;
import com.velocitypowered.api.event.annotation.AwaitingEvent; import com.velocitypowered.api.event.annotation.AwaitingEvent;
import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.Player;
import org.checkerframework.checker.nullness.qual.Nullable;
/** /**
* This event is fired once the player has been authenticated, but before they connect to a server. * This event is fired once the player has been authenticated, but before they connect to a server.
@@ -22,10 +23,24 @@ import com.velocitypowered.api.proxy.Player;
public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> { public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
private final Player player; private final Player player;
private final String serverIdHash;
private ComponentResult result; private ComponentResult result;
@Deprecated(forRemoval = true)
public LoginEvent(Player player) { public LoginEvent(Player player) {
this(player, null);
}
/**
* Constructs a new {@link LoginEvent}.
*
* @param player the player who has completed authentication
* @param serverIdHash the server ID hash sent to Mojang for authentication,
* or {@code null} if the connection is in offline-mode
*/
public LoginEvent(Player player, @Nullable String serverIdHash) {
this.player = Preconditions.checkNotNull(player, "player"); this.player = Preconditions.checkNotNull(player, "player");
this.serverIdHash = serverIdHash;
this.result = ComponentResult.allowed(); this.result = ComponentResult.allowed();
} }
@@ -33,6 +48,16 @@ public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentRe
return player; return player;
} }
/**
* Returns the server ID hash that was sent to Mojang to authenticate the player.
* If the connection was in offline-mode, this returns {@code null}.
*
* @return the server ID hash that was sent to Mojang to authenticate the player
*/
public @Nullable String getServerIdHash() {
return serverIdHash;
}
@Override @Override
public ComponentResult getResult() { public ComponentResult getResult() {
return result; return result;
@@ -0,0 +1,44 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* The Velocity API is licensed under the terms of the MIT License. For more details,
* reference the LICENSE file in the api top-level directory.
*/
package com.velocitypowered.api.event.player;
import com.google.common.base.Preconditions;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
import java.util.List;
/**
* This event is fired when a client ({@link Player}) sends a plugin message through the
* unregister channel. Velocity will not wait on this event to finish firing.
*/
public final class PlayerChannelUnregisterEvent {
private final Player player;
private final List<ChannelIdentifier> channels;
public PlayerChannelUnregisterEvent(Player player, List<ChannelIdentifier> channels) {
this.player = Preconditions.checkNotNull(player, "player");
this.channels = Preconditions.checkNotNull(channels, "channels");
}
public Player getPlayer() {
return player;
}
public List<ChannelIdentifier> getChannels() {
return channels;
}
@Override
public String toString() {
return "PlayerChannelUnregisterEvent{"
+ "player=" + player
+ ", channels=" + channels
+ '}';
}
}
@@ -143,7 +143,7 @@ public final class ServerPreConnectEvent implements
* is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status * is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status
* {@link Status#CONNECTION_CANCELLED}. * {@link Status#CONNECTION_CANCELLED}.
* *
* @return a result to deny conneections * @return a result to deny connections
*/ */
public static ServerResult denied() { public static ServerResult denied() {
return DENIED; return DENIED;
@@ -93,7 +93,9 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_5(770, "1.21.5"), MINECRAFT_1_21_5(770, "1.21.5"),
MINECRAFT_1_21_6(771, "1.21.6"), MINECRAFT_1_21_6(771, "1.21.6"),
MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"), MINECRAFT_1_21_7(772, "1.21.7", "1.21.8"),
MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"); MINECRAFT_1_21_9(773, "1.21.9", "1.21.10"),
MINECRAFT_1_21_11(774, "1.21.11"),
MINECRAFT_26_1(775, "26.1", "26.1.1", "26.1.2");
private static final int SNAPSHOT_BIT = 30; private static final int SNAPSHOT_BIT = 30;
@@ -7,9 +7,11 @@
package com.velocitypowered.api.plugin; package com.velocitypowered.api.plugin;
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.intellij.lang.annotations.Pattern;
/** /**
* Indicates that the {@link Plugin} depends on another plugin in order to enable. * Indicates that the {@link Plugin} depends on another plugin in order to enable.
@@ -24,6 +26,7 @@ public @interface Dependency {
* @return The dependency plugin ID * @return The dependency plugin ID
* @see Plugin#id() * @see Plugin#id()
*/ */
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id(); String id();
/** /**
@@ -7,10 +7,12 @@
package com.velocitypowered.api.plugin; package com.velocitypowered.api.plugin;
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import org.intellij.lang.annotations.Pattern;
/** /**
* Annotation used to describe a Velocity plugin. * Annotation used to describe a Velocity plugin.
@@ -26,6 +28,7 @@ public @interface Plugin {
* *
* @return the ID for this plugin * @return the ID for this plugin
*/ */
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id(); String id();
/** /**
@@ -39,6 +39,7 @@ import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.text.event.HoverEventSource;
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -49,7 +50,8 @@ public interface Player extends
/* Fundamental Velocity interfaces */ /* Fundamental Velocity interfaces */
CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink, CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink,
/* Adventure-specific interfaces */ /* Adventure-specific interfaces */
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable, Sound.Emitter { Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable, Sound.Emitter,
PlayerHeadObjectContents.SkinSource {
/** /**
* Returns the player's current username. * Returns the player's current username.
@@ -336,6 +338,15 @@ public interface Player extends
Component.text(getUsername())))); Component.text(getUsername()))));
} }
@SuppressWarnings("UnstableApiUsage") // permitted implementation
@Override
default void applySkinToPlayerHeadContents(
final PlayerHeadObjectContents.@NotNull Builder builder) {
builder.skin(this.getGameProfile());
if (this.hasSentPlayerSettings()) {
builder.hat(this.getPlayerSettings().getSkinParts().hasHat());
}
}
/** /**
* Gets the player's client brand. * Gets the player's client brand.
@@ -11,11 +11,14 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.stream.Collectors;
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
import org.jetbrains.annotations.NotNull;
/** /**
* Represents a Mojang game profile. This class is immutable. * Represents a Mojang game profile. This class is immutable.
*/ */
public final class GameProfile { public final class GameProfile implements PlayerHeadObjectContents.SkinSource {
private final UUID id; private final UUID id;
private final String undashedId; private final String undashedId;
@@ -169,6 +172,23 @@ public final class GameProfile {
ImmutableList.of()); ImmutableList.of());
} }
@SuppressWarnings("UnstableApiUsage") // permitted implementation
@Override
public void applySkinToPlayerHeadContents(
final PlayerHeadObjectContents.@NotNull Builder builder) {
if (this.properties.isEmpty()) {
builder.id(this.id);
return;
}
builder.id(this.id)
.name(this.name)
.profileProperties(this.properties.stream()
.map(property -> PlayerHeadObjectContents.property(property.getName(),
property.getValue(), property.getSignature()))
.collect(Collectors.toList()));
}
@Override @Override
public String toString() { public String toString() {
return "GameProfile{" return "GameProfile{"
+1 -1
View File
@@ -12,7 +12,7 @@ subprojects {
java { java {
toolchain { toolchain {
languageVersion.set(JavaLanguageVersion.of(17)) languageVersion.set(JavaLanguageVersion.of(21))
} }
} }
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=3.4.0-SNAPSHOT version=3.5.0-SNAPSHOT
+25 -26
View File
@@ -1,27 +1,26 @@
[versions] [versions]
configurate3 = "3.7.3" configurate3 = "3.7.3"
configurate4 = "4.1.2" configurate4 = "4.2.0"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.24.3" log4j = "2.25.3"
netty = "4.2.7.Final" netty = "4.2.10.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.3" fill = "io.papermc.fill.gradle:1.0.10"
indra-publishing = "net.kyori.indra.publishing:2.0.6" shadow = "com.gradleup.shadow:9.3.1"
shadow = "com.gradleup.shadow:8.3.6" spotless = "com.diffplug.spotless:8.2.0"
spotless = "com.diffplug.spotless:6.25.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:4.25.0" adventure-bom = "net.kyori:adventure-bom:4.26.1"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.25.0" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.26.1"
adventure-facet = "net.kyori:adventure-platform-facet:4.3.4" adventure-facet = "net.kyori:adventure-platform-facet:4.4.1"
asm = "org.ow2.asm:asm:9.8" asm = "org.ow2.asm:asm:9.9.1"
auto-service = "com.google.auto.service:auto-service:1.0.1" auto-service = "com.google.auto.service:auto-service:1.1.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.0.3" bstats = "org.bstats:bstats-base:3.1.0"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8" caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3"
checker-qual = "org.checkerframework:checker-qual:3.42.0" checker-qual = "org.checkerframework:checker-qual:3.53.0"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
completablefutures = "com.spotify:completable-futures:0.3.6" completablefutures = "com.spotify:completable-futures:0.3.6"
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" } configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
@@ -34,21 +33,21 @@ disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.15" fastutil = "it.unimi.dsi:fastutil:8.5.15"
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" } flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" } flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
jline = "org.jline:jline-terminal-jansi:3.30.2" jline = "org.jline:jline-terminal-jansi:3.30.6"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:5.10.2" junit = "org.junit.jupiter:junit-jupiter:5.14.2"
jspecify = "org.jspecify:jspecify:0.3.0" jspecify = "org.jspecify:jspecify:1.0.0"
kyori-ansi = "net.kyori:ansi:1.1.1" kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:25.1-jre" guava = "com.google.guava:guava:33.5.0-jre"
gson = "com.google.code.gson:gson:2.10.1" gson = "com.google.code.gson:gson:2.13.2"
guice = "com.google.inject:guice:6.0.0" guice = "com.google.inject:guice:7.0.0"
lmbda = "org.lanternpowered:lmbda:2.0.0" lmbda = "org.lanternpowered:lmbda:2.0.0"
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" } log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" }
log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-slf4j-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" }
log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" } log4j-iostreams = { module = "org.apache.logging.log4j:log4j-iostreams", version.ref = "log4j" }
log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" } log4j-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
mockito = "org.mockito:mockito-core:5.10.0" mockito = "org.mockito:mockito-core:5.21.0"
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" } netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" } netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" } netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
@@ -56,10 +55,10 @@ netty-handler = { module = "io.netty:netty-handler", version.ref = "netty" }
netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" } netty-transport-native-epoll = { module = "io.netty:netty-transport-native-epoll", version.ref = "netty" }
netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" } netty-transport-native-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" } netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
nightconfig = "com.electronwill.night-config:toml:3.6.7" nightconfig = "com.electronwill.night-config:toml:3.8.3"
slf4j = "org.slf4j:slf4j-api:2.0.17" slf4j = "org.slf4j:slf4j-api:2.0.17"
snakeyaml = "org.yaml:snakeyaml:1.33" snakeyaml = "org.yaml:snakeyaml:2.5"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8"
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0" terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
[bundles] [bundles]
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+6 -10
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015-2021 the original authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -115,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@@ -173,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -203,18 +200,17 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped. # and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line. # treated as '${Hostname}' itself on the command line.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
@@ -249,4 +245,4 @@ eval "set -- $(
tr '\n' ' ' tr '\n' ' '
)" '"$@"' )" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"
Vendored
+3 -4
View File
@@ -36,7 +36,7 @@ set APP_HOME=%DIRNAME%
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
@@ -70,11 +70,10 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
@@ -91,4 +90,4 @@ exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
:omega :omega
+6 -2
View File
@@ -10,7 +10,7 @@ plugins {
application { application {
mainClass.set("com.velocitypowered.proxy.Velocity") mainClass.set("com.velocitypowered.proxy.Velocity")
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true"); applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true")
} }
tasks { tasks {
@@ -27,6 +27,10 @@ tasks {
} }
shadowJar { shadowJar {
filesMatching("META-INF/org/apache/logging/log4j/core/config/plugins/**") {
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}
transform(Log4j2PluginsCacheFileTransformer::class.java) transform(Log4j2PluginsCacheFileTransformer::class.java)
// Exclude all the collection types we don"t intend to use // Exclude all the collection types we don"t intend to use
@@ -131,7 +135,6 @@ fill {
dependencies { dependencies {
implementation(project(":velocity-api")) implementation(project(":velocity-api"))
implementation(project(":velocity-native")) implementation(project(":velocity-native"))
implementation(project(":velocity-proxy-log4j2-plugin"))
implementation(libs.bundles.log4j) implementation(libs.bundles.log4j)
implementation(libs.kyori.ansi) implementation(libs.kyori.ansi)
@@ -168,4 +171,5 @@ dependencies {
testImplementation(libs.mockito) testImplementation(libs.mockito)
annotationProcessor(libs.auto.service) annotationProcessor(libs.auto.service)
annotationProcessor(libs.log4j.core)
} }
-4
View File
@@ -1,4 +0,0 @@
dependencies {
implementation(libs.bundles.log4j)
annotationProcessor(libs.log4j.core)
}
@@ -82,7 +82,6 @@ import java.net.http.HttpClient;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.KeyPair; import java.security.KeyPair;
import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@@ -105,8 +104,8 @@ import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.ForwardingAudience; import net.kyori.adventure.audience.ForwardingAudience;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.translation.MiniMessageTranslationStore;
import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.GlobalTranslator;
import net.kyori.adventure.translation.TranslationStore;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.bstats.MetricsBase; import org.bstats.MetricsBase;
@@ -242,8 +241,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
console.setupStreams(); console.setupStreams();
pluginManager.registerPlugin(this.createVirtualPlugin()); pluginManager.registerPlugin(this.createVirtualPlugin());
registerTranslations();
// Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds // Yes, you're reading that correctly. We're generating a 1024-bit RSA keypair. Sounds
// dangerous, right? We're well within the realm of factoring such a key... // dangerous, right? We're well within the realm of factoring such a key...
// //
@@ -292,6 +289,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
this.doStartupConfigLoad(); this.doStartupConfigLoad();
registerTranslations();
for (ServerInfo cliServer : options.getServers()) { for (ServerInfo cliServer : options.getServers()) {
servers.register(cliServer); servers.register(cliServer);
} }
@@ -342,8 +341,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
} }
private void registerTranslations() { private void registerTranslations() {
final TranslationStore.StringBased<MessageFormat> translationRegistry = final MiniMessageTranslationStore translationRegistry =
TranslationStore.messageFormat(Key.key("velocity", "translations")); MiniMessageTranslationStore.create(Key.key("velocity", "translations"));
translationRegistry.defaultLocale(Locale.US); translationRegistry.defaultLocale(Locale.US);
try { try {
ResourceUtils.visitResources(VelocityServer.class, path -> { ResourceUtils.visitResources(VelocityServer.class, path -> {
@@ -835,7 +834,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
public VelocityChannelRegistrar getChannelRegistrar() { public VelocityChannelRegistrar getChannelRegistrar() {
return channelRegistrar; return channelRegistrar;
} }
@Override @Override
public boolean isShuttingDown() { public boolean isShuttingDown() {
return shutdownInProgress.get(); return shutdownInProgress.get();
@@ -344,7 +344,7 @@ final class SuggestionsProvider<S> {
return 0; return 0;
}); });
} }
return potentials.get(0); return potentials.getFirst();
} }
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap()); return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
} }
@@ -140,7 +140,7 @@ public class VelocityCommandManager implements CommandManager {
command + " implements multiple registrable Command subinterfaces: " command + " implements multiple registrable Command subinterfaces: "
+ implementedInterfaces); + implementedInterfaces);
} else { } else {
this.internalRegister(commandRegistrars.get(0), command, meta); this.internalRegister(commandRegistrars.getFirst(), command, meta);
} }
} }
@@ -256,7 +256,7 @@ public class VelocityCommandManager implements CommandManager {
} }
} catch (final Throwable e) { } catch (final Throwable e) {
// Ugly, ugly swallowing of everything Throwable, because plugins are naughty. // Ugly, ugly swallowing of everything Throwable, because plugins are naughty.
throw new RuntimeException("Unable to invoke command " + parsed.getReader().getString() + "for " + source, e); throw new RuntimeException("Unable to invoke command " + parsed.getReader().getString() + " for " + source, e);
} finally { } finally {
eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result)); eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result));
} }
@@ -400,4 +400,4 @@ public class VelocityCommandManager implements CommandManager {
return MoreExecutors.directExecutor(); return MoreExecutors.directExecutor();
} }
} }
} }
@@ -70,33 +70,34 @@ public final class VelocityCommands {
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant); maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
} }
if (delegate instanceof LiteralCommandNode<CommandSource> lcn) { return switch (delegate) {
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true); case LiteralCommandNode<CommandSource> lcn -> {
literalBuilder.executes(maybeCommand); var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
// we also need to wrap any children literalBuilder.executes(maybeCommand);
for (final CommandNode<CommandSource> child : delegate.getChildren()) { // we also need to wrap any children
literalBuilder.then(wrap(child, registrant)); for (final CommandNode<CommandSource> child : delegate.getChildren()) {
literalBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
yield literalBuilder.build();
} }
if (delegate.getRedirect() != null) { case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand)
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant)); .withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
case ArgumentCommandNode<CommandSource, ?> node -> {
var argBuilder = node.createBuilder().executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
argBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
yield argBuilder.build();
} }
return literalBuilder.build(); default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
} else if (delegate instanceof VelocityArgumentCommandNode<CommandSource, ?> vacn) { };
return vacn.withCommand(maybeCommand)
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
} else if (delegate instanceof ArgumentCommandNode) {
var argBuilder = delegate.createBuilder().executes(maybeCommand);
// we also need to wrap any children
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
argBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
return argBuilder.build();
} else {
throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
}
} }
// Normalization // Normalization
@@ -133,7 +134,7 @@ public final class VelocityCommands {
if (nodes.isEmpty()) { if (nodes.isEmpty()) {
throw new IllegalArgumentException("Cannot read alias from empty node list"); throw new IllegalArgumentException("Cannot read alias from empty node list");
} }
return nodes.get(0).getNode().getName(); return nodes.getFirst().getNode().getName();
} }
public static final String ARGS_NODE_NAME = "arguments"; public static final String ARGS_NODE_NAME = "arguments";
@@ -118,14 +118,12 @@ public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, St
if (this == o) { if (this == o) {
return true; return true;
} }
if (!(o instanceof VelocityArgumentCommandNode)) { if (!(o instanceof VelocityArgumentCommandNode that)) {
return false; return false;
} }
if (!super.equals(o)) { if (!super.equals(that)) {
return false; return false;
} }
final VelocityArgumentCommandNode<?, ?> that = (VelocityArgumentCommandNode<?, ?>) o;
return this.type.equals(that.type); return this.type.equals(that.type);
} }
@@ -38,6 +38,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/** /**
* Implements the Velocity default {@code /glist} command. * Implements the Velocity default {@code /glist} command.
@@ -111,7 +112,7 @@ public class GlistCommand {
if (registeredServer.isEmpty()) { if (registeredServer.isEmpty()) {
source.sendMessage( source.sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST CommandMessages.SERVER_DOES_NOT_EXIST
.arguments(Component.text(serverName))); .arguments(Argument.string("server", serverName)));
return -1; return -1;
} }
sendServerPlayers(source, registeredServer.get(), false); sendServerPlayers(source, registeredServer.get(), false);
@@ -126,7 +127,8 @@ public class GlistCommand {
? "velocity.command.glist-player-singular" ? "velocity.command.glist-player-singular"
: "velocity.command.glist-player-plural" : "velocity.command.glist-player-plural"
).color(NamedTextColor.YELLOW) ).color(NamedTextColor.YELLOW)
.arguments(Component.text(Integer.toString(online), NamedTextColor.GREEN)); .arguments(Argument.component(
"players", Component.text(Integer.toString(online), NamedTextColor.GREEN)));
target.sendMessage(msg.build()); target.sendMessage(msg.build());
} }
@@ -35,6 +35,7 @@ import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/** /**
* Implements the Velocity default {@code /send} command. * Implements the Velocity default {@code /send} command.
@@ -121,7 +122,7 @@ public class SendCommand {
if (maybeServer.isEmpty()) { if (maybeServer.isEmpty()) {
context.getSource().sendMessage( context.getSource().sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName)) CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Argument.string("server", serverName))
); );
return 0; return 0;
} }
@@ -133,7 +134,7 @@ public class SendCommand {
&& !Objects.equals(player, "all") && !Objects.equals(player, "all")
&& !Objects.equals(player, "current")) { && !Objects.equals(player, "current")) {
context.getSource().sendMessage( context.getSource().sendMessage(
CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player)) CommandMessages.PLAYER_NOT_FOUND.arguments(Argument.string("player", player))
); );
return 0; return 0;
} }
@@ -37,6 +37,7 @@ import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.translation.Argument;
/** /**
* Implements Velocity's {@code /server} command. * Implements Velocity's {@code /server} command.
@@ -76,7 +77,7 @@ public final class ServerCommand {
final Optional<RegisteredServer> toConnect = server.getServer(serverName); final Optional<RegisteredServer> toConnect = server.getServer(serverName);
if (toConnect.isEmpty()) { if (toConnect.isEmpty()) {
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST
.arguments(Component.text(serverName))); .arguments(Argument.string("server", serverName)));
return -1; return -1;
} }
@@ -135,7 +136,7 @@ public final class ServerCommand {
} else { } else {
playersTextComponent.key("velocity.command.server-tooltip-players-online"); playersTextComponent.key("velocity.command.server-tooltip-players-online");
} }
playersTextComponent.arguments(Component.text(connectedPlayers)); playersTextComponent.arguments(Argument.component("players", Component.text(connectedPlayers)));
if (serverInfo.getName().equals(currentPlayerServer)) { if (serverInfo.getName().equals(currentPlayerServer)) {
serverTextComponent.color(NamedTextColor.GREEN) serverTextComponent.color(NamedTextColor.GREEN)
.hoverEvent( .hoverEvent(
@@ -62,6 +62,7 @@ import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.text.minimessage.translation.Argument;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -165,9 +166,9 @@ public final class VelocityCommand {
.build(); .build();
final Component copyright = Component final Component copyright = Component
.translatable("velocity.command.version-copyright", .translatable("velocity.command.version-copyright",
Component.text(version.getVendor()), Argument.string("vendor", version.getVendor()),
Component.text(version.getName()), Argument.string("name", version.getName()),
Component.text(LocalDate.now().getYear())); Argument.component("year", Component.text(LocalDate.now().getYear())));
source.sendMessage(velocity); source.sendMessage(velocity);
source.sendMessage(copyright); source.sendMessage(copyright);
@@ -220,7 +221,7 @@ public final class VelocityCommand {
final TranslatableComponent output = Component.translatable() final TranslatableComponent output = Component.translatable()
.key("velocity.command.plugins-list") .key("velocity.command.plugins-list")
.color(NamedTextColor.YELLOW) .color(NamedTextColor.YELLOW)
.arguments(listBuilder.build()) .arguments(Argument.component("plugins", listBuilder.build()))
.build(); .build();
source.sendMessage(output); source.sendMessage(output);
return Command.SINGLE_SUCCESS; return Command.SINGLE_SUCCESS;
@@ -236,17 +237,17 @@ public final class VelocityCommand {
hoverText.append(Component.newline()); hoverText.append(Component.newline());
hoverText.append(Component.translatable( hoverText.append(Component.translatable(
"velocity.command.plugin-tooltip-website", "velocity.command.plugin-tooltip-website",
Component.text(url))); Argument.component("url", Component.text(url))));
}); });
if (!description.getAuthors().isEmpty()) { if (!description.getAuthors().isEmpty()) {
hoverText.append(Component.newline()); hoverText.append(Component.newline());
if (description.getAuthors().size() == 1) { if (description.getAuthors().size() == 1) {
hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author", hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author",
Component.text(description.getAuthors().get(0)))); Component.text(description.getAuthors().getFirst())));
} else { } else {
hoverText.append( hoverText.append(
Component.translatable("velocity.command.plugin-tooltip-author", Component.translatable("velocity.command.plugin-tooltip-author",
Component.text(String.join(", ", description.getAuthors())) Argument.string("authors", String.join(", ", description.getAuthors()))
) )
); );
} }
@@ -29,6 +29,7 @@ import com.velocitypowered.api.util.Favicon;
import com.velocitypowered.proxy.config.migration.ConfigurationMigration; import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
import com.velocitypowered.proxy.config.migration.ForwardingMigration; import com.velocitypowered.proxy.config.migration.ForwardingMigration;
import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration; import com.velocitypowered.proxy.config.migration.KeyAuthenticationMigration;
import com.velocitypowered.proxy.config.migration.MiniMessageTranslationsMigration;
import com.velocitypowered.proxy.config.migration.MotdMigration; import com.velocitypowered.proxy.config.migration.MotdMigration;
import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration; import com.velocitypowered.proxy.config.migration.TransferIntegrationMigration;
import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.AddressUtil;
@@ -93,6 +94,8 @@ public class VelocityConfiguration implements ProxyConfig {
private @Nullable Favicon favicon; private @Nullable Favicon favicon;
@Expose @Expose
private boolean forceKeyAuthentication = true; // Added in 1.19 private boolean forceKeyAuthentication = true; // Added in 1.19
@Expose
private PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.DEFAULT;
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced, private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
Query query, Metrics metrics) { Query query, Metrics metrics) {
@@ -109,7 +112,7 @@ public class VelocityConfiguration implements ProxyConfig {
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers, boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics, ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
boolean forceKeyAuthentication) { boolean forceKeyAuthentication, PacketLimiterConfig packetLimiterConfig) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
@@ -128,6 +131,7 @@ public class VelocityConfiguration implements ProxyConfig {
this.query = query; this.query = query;
this.metrics = metrics; this.metrics = metrics;
this.forceKeyAuthentication = forceKeyAuthentication; this.forceKeyAuthentication = forceKeyAuthentication;
this.packetLimiterConfig = packetLimiterConfig;
} }
/** /**
@@ -156,19 +160,16 @@ public class VelocityConfiguration implements ProxyConfig {
} }
switch (playerInfoForwardingMode) { switch (playerInfoForwardingMode) {
case NONE: case NONE -> logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
logger.warn("Player info forwarding is disabled! All players will appear to be connecting "
+ "from the proxy and will have offline-mode UUIDs."); + "from the proxy and will have offline-mode UUIDs.");
break; case MODERN, BUNGEEGUARD -> {
case MODERN:
case BUNGEEGUARD:
if (forwardingSecret == null || forwardingSecret.length == 0) { if (forwardingSecret == null || forwardingSecret.length == 0) {
logger.error("You don't have a forwarding secret set. This is required for security."); logger.error("You don't have a forwarding secret set. This is required for security.");
valid = false; valid = false;
} }
break; }
default: default -> {
break; }
} }
if (servers.getServers().isEmpty()) { if (servers.getServers().isEmpty()) {
@@ -449,6 +450,10 @@ public class VelocityConfiguration implements ProxyConfig {
return advanced.isEnableReusePort(); return advanced.isEnableReusePort();
} }
public PacketLimiterConfig getPacketLimiterConfig() {
return packetLimiterConfig;
}
@Override @Override
public String toString() { public String toString() {
return MoreObjects.toStringHelper(this) return MoreObjects.toStringHelper(this)
@@ -466,6 +471,7 @@ public class VelocityConfiguration implements ProxyConfig {
.add("favicon", favicon) .add("favicon", favicon)
.add("enablePlayerAddressLogging", enablePlayerAddressLogging) .add("enablePlayerAddressLogging", enablePlayerAddressLogging)
.add("forceKeyAuthentication", forceKeyAuthentication) .add("forceKeyAuthentication", forceKeyAuthentication)
.add("packetLimiterConfig", packetLimiterConfig)
.toString(); .toString();
} }
@@ -504,6 +510,7 @@ public class VelocityConfiguration implements ProxyConfig {
new ForwardingMigration(), new ForwardingMigration(),
new KeyAuthenticationMigration(), new KeyAuthenticationMigration(),
new MotdMigration(), new MotdMigration(),
new MiniMessageTranslationsMigration(),
new TransferIntegrationMigration() new TransferIntegrationMigration()
}; };
@@ -561,6 +568,7 @@ public class VelocityConfiguration implements ProxyConfig {
final boolean kickExisting = config.getOrElse("kick-existing-players", false); final boolean kickExisting = config.getOrElse("kick-existing-players", false);
final boolean enablePlayerAddressLogging = config.getOrElse( final boolean enablePlayerAddressLogging = config.getOrElse(
"enable-player-address-logging", true); "enable-player-address-logging", true);
final PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.fromConfig(config.get("packet-limiter"));
// Throw an exception if the forwarding-secret file is empty and the proxy is using a // Throw an exception if the forwarding-secret file is empty and the proxy is using a
// forwarding mode that requires it. // forwarding mode that requires it.
@@ -588,7 +596,8 @@ public class VelocityConfiguration implements ProxyConfig {
new Advanced(advancedConfig), new Advanced(advancedConfig),
new Query(queryConfig), new Query(queryConfig),
new Metrics(metricsConfig), new Metrics(metricsConfig),
forceKeyAuthentication forceKeyAuthentication,
packetLimiterConfig
); );
} }
} }
@@ -991,4 +1000,33 @@ public class VelocityConfiguration implements ProxyConfig {
return enabled; return enabled;
} }
} }
/**
* Configuration for packet limiting.
*
* @param interval the interval in seconds to measure packets over
* @param pps the maximum number of packets per second allowed
* @param bytes the maximum number of bytes per second allowed
*/
public record PacketLimiterConfig(int interval, int pps, int bytes) {
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
/**
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
*
* @param config the configuration object to parse
* @return the packet limiter config, or the default if {@code config} is null
*/
public static PacketLimiterConfig fromConfig(CommentedConfig config) {
if (config != null) {
return new PacketLimiterConfig(
config.getIntOrElse("interval", DEFAULT.interval()),
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
config.getIntOrElse("bytes-per-second", DEFAULT.bytes())
);
} else {
return DEFAULT;
}
}
}
} }
@@ -28,6 +28,7 @@ public sealed interface ConfigurationMigration
permits ForwardingMigration, permits ForwardingMigration,
KeyAuthenticationMigration, KeyAuthenticationMigration,
MotdMigration, MotdMigration,
MiniMessageTranslationsMigration,
TransferIntegrationMigration { TransferIntegrationMigration {
boolean shouldMigrate(CommentedFileConfig config); boolean shouldMigrate(CommentedFileConfig config);
@@ -0,0 +1,65 @@
/*
* Copyright (C) 2024 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.config.migration;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.regex.Pattern;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.apache.logging.log4j.Logger;
/**
* Migration from old to modern language argument format with MiniMessage.
* Also migrates possible use of legacy colors to MiniMessage format.
*/
public final class MiniMessageTranslationsMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(final CommentedFileConfig config) {
// Checking whether translations should be migrated would be just as costly as attempting to migrate them directly.
return true;
}
@Override
public void migrate(final CommentedFileConfig config, final Logger logger) throws IOException {
final Path langFolder = Path.of("lang");
if (Files.notExists(langFolder)) {
return;
}
final Pattern oldPlaceholderPattern = Pattern.compile("\\{(\\d+)}");
try (final DirectoryStream<Path> stream
= Files.newDirectoryStream(langFolder, Files::isRegularFile)) {
for (final Path path : stream) {
String content = Files.readString(path, StandardCharsets.UTF_8);
if (content.indexOf('{') == -1) {
continue;
}
// Migrate old arguments
content = oldPlaceholderPattern.matcher(content).replaceAll("<arg:$1>");
// Some setups use legacy color codes, this format is migrated to MiniMessage
content = MiniMessage.miniMessage().serialize(
LegacyComponentSerializer.legacySection().deserialize(content));
Files.writeString(path, content, StandardCharsets.UTF_8);
}
}
}
}
@@ -33,6 +33,7 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
import com.velocitypowered.natives.encryption.VelocityCipherFactory; import com.velocitypowered.natives.encryption.VelocityCipherFactory;
import com.velocitypowered.natives.util.Natives; import com.velocitypowered.natives.util.Natives;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler;
import com.velocitypowered.proxy.connection.client.StatusSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
@@ -66,7 +67,7 @@ import io.netty.util.ReferenceCountUtil;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketAddress; import java.net.SocketAddress;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.HashMap; import java.util.EnumMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -108,7 +109,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
this.server = server; this.server = server;
this.state = StateRegistry.HANDSHAKE; this.state = StateRegistry.HANDSHAKE;
this.sessionHandlers = new HashMap<>(); this.sessionHandlers = new EnumMap<>(StateRegistry.class);
} }
@Override @Override
@@ -153,13 +154,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (msg instanceof MinecraftPacket pkt) { if (msg instanceof MinecraftPacket pkt) {
if (!pkt.handle(activeSessionHandler)) { if (!pkt.handle(activeSessionHandler)) {
activeSessionHandler.handleGeneric((MinecraftPacket) msg); activeSessionHandler.handleGeneric(pkt);
} }
} else if (msg instanceof HAProxyMessage proxyMessage) { } else if (msg instanceof HAProxyMessage proxyMessage) {
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
proxyMessage.sourcePort()); proxyMessage.sourcePort());
} else if (msg instanceof ByteBuf) { } else if (msg instanceof ByteBuf buf) {
activeSessionHandler.handleUnknown((ByteBuf) msg); activeSessionHandler.handleUnknown(buf);
} }
} finally { } finally {
ReferenceCountUtil.release(msg); ReferenceCountUtil.release(msg);
@@ -368,6 +369,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
public void setState(StateRegistry state) { public void setState(StateRegistry state) {
ensureInEventLoop(); ensureInEventLoop();
final StateRegistry previousState = this.state;
this.state = state; this.state = state;
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline() final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
.get(MinecraftVarintFrameDecoder.class); .get(MinecraftVarintFrameDecoder.class);
@@ -388,7 +390,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (state == StateRegistry.CONFIG) { if (state == StateRegistry.CONFIG) {
// Activate the play packet queue // Activate the play packet queue
addPlayPacketQueueHandler(); if (previousState == StateRegistry.PLAY
&& this.pendingConfigurationSwitch
&& this.association instanceof ConnectedPlayer) {
addPlayPacketQueueOutboundHandler();
} else {
addPlayPacketQueueHandler();
}
} else { } else {
// Remove the queue // Remove the queue
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
@@ -404,13 +412,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* Adds the play packet queue handler. * Adds the play packet queue handler.
*/ */
public void addPlayPacketQueueHandler() { public void addPlayPacketQueueHandler() {
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) { addPlayPacketQueueOutboundHandler();
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
}
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND, this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection())); new PlayPacketQueueInboundHandler(this.protocolVersion,
channel.pipeline().get(MinecraftDecoder.class).getDirection()));
}
}
/**
* Adds only the outbound play packet queue handler.
*/
public void addPlayPacketQueueOutboundHandler() {
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueOutboundHandler(this.protocolVersion,
channel.pipeline().get(MinecraftEncoder.class).getDirection()));
} }
} }
@@ -544,9 +562,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
} else { } else {
int level = server.getConfiguration().getCompressionLevel(); int level = server.getConfiguration().getCompressionLevel();
VelocityCompressor compressor = Natives.compress.get().create(level); VelocityCompressor compressor = Natives.compress.get().create(level);
final MinecraftDecoder minecraftDecoder = (MinecraftDecoder) channel.pipeline().get(MINECRAFT_DECODER);
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor); encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
decoder = new MinecraftCompressDecoder(threshold, compressor); decoder = new MinecraftCompressDecoder(threshold, compressor, minecraftDecoder.getDirection());
channel.pipeline().remove(FRAME_ENCODER); channel.pipeline().remove(FRAME_ENCODER);
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder); channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
@@ -70,6 +70,7 @@ import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
@@ -93,6 +94,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
Boolean.getBoolean("velocity.log-server-backpressure"); Boolean.getBoolean("velocity.log-server-backpressure");
private static final int MAXIMUM_PACKETS_TO_FLUSH = private static final int MAXIMUM_PACKETS_TO_FLUSH =
Integer.getInteger("velocity.max-packets-per-flush", 8192); Integer.getInteger("velocity.max-packets-per-flush", 8192);
private static final int LARGE_PACKET_THRESHOLD = 1024 * 128;
private final VelocityServer server; private final VelocityServer server;
private final VelocityServerConnection serverConn; private final VelocityServerConnection serverConn;
@@ -437,11 +439,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void handleGeneric(MinecraftPacket packet) { public void handleGeneric(MinecraftPacket packet) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof PluginMessagePacket pluginMessage) {
((PluginMessagePacket) packet).retain(); pluginMessage.retain();
} }
boolean huge = packet instanceof DeferredByteBufHolder def && def.content().readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(packet); playerConnection.delayedWrite(packet);
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush(); playerConnection.flush();
packetsFlushed = 0; packetsFlushed = 0;
} }
@@ -449,8 +452,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void handleUnknown(ByteBuf buf) { public void handleUnknown(ByteBuf buf) {
boolean huge = buf.readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(buf.retain()); playerConnection.delayedWrite(buf.retain());
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush(); playerConnection.flush();
packetsFlushed = 0; packetsFlushed = 0;
} }
@@ -344,66 +344,30 @@ public class BungeeCordMessageResponder {
return false; return false;
} }
ByteBufDataInput in = new ByteBufDataInput(message.content()); final ByteBufDataInput in = new ByteBufDataInput(message.content());
String subChannel = in.readUTF(); final String subChannel = in.readUTF();
switch (subChannel) { switch (subChannel) {
case "GetPlayerServer": case "GetPlayerServer" -> this.processGetPlayerServer(in);
this.processGetPlayerServer(in); case "ForwardToPlayer" -> this.processForwardToPlayer(in);
break; case "Forward" -> this.processForwardToServer(in);
case "ForwardToPlayer": case "Connect" -> this.processConnect(in);
this.processForwardToPlayer(in); case "ConnectOther" -> this.processConnectOther(in);
break; case "IP" -> this.processIp(in);
case "Forward": case "PlayerCount" -> this.processPlayerCount(in);
this.processForwardToServer(in); case "PlayerList" -> this.processPlayerList(in);
break; case "GetServers" -> this.processGetServers();
case "Connect": case "Message" -> this.processMessage(in);
this.processConnect(in); case "MessageRaw" -> this.processMessageRaw(in);
break; case "GetServer" -> this.processGetServer();
case "ConnectOther": case "UUID" -> this.processUuid();
this.processConnectOther(in); case "UUIDOther" -> this.processUuidOther(in);
break; case "IPOther" -> this.processIpOther(in);
case "IP": case "ServerIP" -> this.processServerIp(in);
this.processIp(in); case "KickPlayer" -> this.processKick(in);
break; case "KickPlayerRaw" -> this.processKickRaw(in);
case "PlayerCount": default -> {
this.processPlayerCount(in); // Do nothing, unknown command
break; }
case "PlayerList":
this.processPlayerList(in);
break;
case "GetServers":
this.processGetServers();
break;
case "Message":
this.processMessage(in);
break;
case "MessageRaw":
this.processMessageRaw(in);
break;
case "GetServer":
this.processGetServer();
break;
case "UUID":
this.processUuid();
break;
case "UUIDOther":
this.processUuidOther(in);
break;
case "IPOther":
this.processIpOther(in);
break;
case "ServerIP":
this.processServerIp(in);
break;
case "KickPlayer":
this.processKick(in);
break;
case "KickPlayerRaw":
this.processKickRaw(in);
break;
default:
// Do nothing, unknown command
break;
} }
return true; return true;
@@ -60,7 +60,7 @@ import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.io.IOException; import io.netty.channel.Channel;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
@@ -72,6 +72,9 @@ import org.apache.logging.log4j.Logger;
* 1.20.2+ switching. Yes, some of this is exceptionally stupid. * 1.20.2+ switching. Yes, some of this is exceptionally stupid.
*/ */
public class ConfigSessionHandler implements MinecraftSessionHandler { public class ConfigSessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class); private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class);
private final VelocityServer server; private final VelocityServer server;
private final VelocityServerConnection serverConn; private final VelocityServerConnection serverConn;
@@ -373,8 +376,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void disconnected() { public void disconnected() {
resultFuture.completeExceptionally( resultFuture.complete(ConnectionRequestResults.forDisconnect(
new IOException("Unexpectedly disconnected from remote server")); ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
} }
@Override @Override
@@ -382,6 +385,22 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
serverConn.getPlayer().getConnection().write(packet); serverConn.getPlayer().getConnection().write(packet);
} }
@Override
public void writabilityChanged() {
Channel serverChan = serverConn.ensureConnected().getChannel();
boolean writable = serverChan.isWritable();
if (BACKPRESSURE_LOG) {
if (writable) {
logger.info("{} is writable, will auto-read player connection data", this.serverConn);
} else {
logger.info("{} is not writable, not auto-reading player connection data", this.serverConn);
}
}
serverConn.getPlayer().getConnection().setAutoReading(writable);
}
private void switchFailure(Throwable cause) { private void switchFailure(Throwable cause) {
logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(),
serverConn.getPlayer().getUsername(), cause); serverConn.getPlayer().getUsername(), cause);
@@ -395,4 +414,4 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
public enum State { public enum State {
START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE
} }
} }
@@ -38,7 +38,6 @@ import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket; import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import java.io.IOException;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -122,9 +121,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// Change the client to use the ClientPlaySessionHandler if required. // Change the client to use the ClientPlaySessionHandler if required.
ClientPlaySessionHandler playHandler; ClientPlaySessionHandler playHandler;
if (player.getConnection() if (player.getConnection()
.getActiveSessionHandler() instanceof ClientPlaySessionHandler) { .getActiveSessionHandler() instanceof ClientPlaySessionHandler sessionHandler) {
playHandler = playHandler = sessionHandler;
(ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler();
} else { } else {
playHandler = new ClientPlaySessionHandler(server, player); playHandler = new ClientPlaySessionHandler(server, player);
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler); player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler);
@@ -214,7 +212,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void disconnected() { public void disconnected() {
resultFuture resultFuture.complete(ConnectionRequestResults.forDisconnect(
.completeExceptionally(new IOException("Unexpectedly disconnected from remote server")); ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
} }
} }
@@ -180,9 +180,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
handshake.setServerAddress(createBungeeGuardForwardingAddress(secret)); handshake.setServerAddress(createBungeeGuardForwardingAddress(secret));
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) { } else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN); handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN);
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType) { } else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType forgeConnection) {
handshake.setServerAddress(playerVhost + ((ModernForgeConnectionType) proxyPlayer handshake.setServerAddress(playerVhost + forgeConnection.getModernToken());
.getConnection().getType()).getModernToken());
} else { } else {
handshake.setServerAddress(playerVhost); handshake.setServerAddress(playerVhost);
} }
@@ -69,14 +69,16 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
private @MonotonicNonNull ConnectedPlayer connectedPlayer; private @MonotonicNonNull ConnectedPlayer connectedPlayer;
private final boolean onlineMode; private final boolean onlineMode;
private State loginState = State.START; // 1.20.2+ private State loginState = State.START; // 1.20.2+
private final String serverIdHash;
AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound,
GameProfile profile, boolean onlineMode) { GameProfile profile, boolean onlineMode, String serverIdHash) {
this.server = Preconditions.checkNotNull(server, "server"); this.server = Preconditions.checkNotNull(server, "server");
this.inbound = Preconditions.checkNotNull(inbound, "inbound"); this.inbound = Preconditions.checkNotNull(inbound, "inbound");
this.profile = Preconditions.checkNotNull(profile, "profile"); this.profile = Preconditions.checkNotNull(profile, "profile");
this.onlineMode = onlineMode; this.onlineMode = onlineMode;
this.mcConnection = inbound.delegatedConnection(); this.mcConnection = inbound.delegatedConnection();
this.serverIdHash = serverIdHash;
} }
@Override @Override
@@ -213,7 +215,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) {
mcConnection.setAssociation(player); mcConnection.setAssociation(player);
server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> { server.getEventManager().fire(new LoginEvent(player, serverIdHash)).thenAcceptAsync(event -> {
if (mcConnection.isClosed()) { if (mcConnection.isClosed()) {
// The player was disconnected // The player was disconnected
server.getEventManager().fireAndForget(new DisconnectEvent(player, server.getEventManager().fireAndForget(new DisconnectEvent(player,
@@ -33,6 +33,7 @@ import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResp
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
@@ -60,6 +61,8 @@ import org.apache.logging.log4j.Logger;
* Handles the client config stage. * Handles the client config stage.
*/ */
public class ClientConfigSessionHandler implements MinecraftSessionHandler { public class ClientConfigSessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class); private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class);
private final VelocityServer server; private final VelocityServer server;
@@ -266,6 +269,36 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void exception(Throwable throwable) { public void exception(Throwable throwable) {
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
if (MinecraftDecoder.DEBUG) {
logger.info("Exception while handling packet for {}", player, throwable);
}
}
@Override
public void writabilityChanged() {
final boolean writable = player.getConnection().getChannel().isWritable();
if (BACKPRESSURE_LOG) {
if (writable) {
logger.info("{} is writable, will auto-read backend connection data", player);
} else {
logger.info("{} is not writable, not auto-reading backend connection data", player);
}
}
if (!writable) {
// Flush pending packets to free up memory. Schedule on a future event loop invocation
// to avoid disabling auto-read while the flush resolves backpressure.
player.getConnection().eventLoop().execute(() -> player.getConnection().flush());
}
final VelocityServerConnection serverConn = player.getConnectionInFlightOrConnectedServer();
if (serverConn != null) {
final MinecraftConnection smc = serverConn.getConnection();
if (smc != null) {
smc.setAutoReading(writable);
}
}
} }
/** /**
@@ -24,6 +24,7 @@ import com.mojang.brigadier.suggestion.Suggestion;
import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.CookieReceiveEvent;
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
import com.velocitypowered.api.event.player.PlayerChannelUnregisterEvent;
import com.velocitypowered.api.event.player.PlayerClientBrandEvent; import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
import com.velocitypowered.api.event.player.TabCompleteEvent; import com.velocitypowered.api.event.player.TabCompleteEvent;
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent; import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
@@ -42,6 +43,7 @@ import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle; import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
import com.velocitypowered.proxy.protocol.packet.BossBarPacket; import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket; import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
@@ -99,6 +101,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
* center that joins backend servers with players. * center that joins backend servers with players.
*/ */
public class ClientPlaySessionHandler implements MinecraftSessionHandler { public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private static final boolean BACKPRESSURE_LOG =
Boolean.getBoolean("velocity.log-server-backpressure");
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class); private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
@@ -320,8 +324,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels))); new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain()); backendConn.write(packet.retain());
} else if (PluginMessageUtil.isUnregister(packet)) { } else if (PluginMessageUtil.isUnregister(packet)) {
player.getClientsideChannels() List<ChannelIdentifier> channels =
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion())); PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion());
player.getClientsideChannels().removeAll(channels);
server.getEventManager()
.fireAndForget(
new PlayerChannelUnregisterEvent(player, ImmutableList.copyOf(channels)));
backendConn.write(packet.retain()); backendConn.write(packet.retain());
} else if (PluginMessageUtil.isMcBrand(packet)) { } else if (PluginMessageUtil.isMcBrand(packet)) {
String brand = PluginMessageUtil.readBrandMessage(packet.content()); String brand = PluginMessageUtil.readBrandMessage(packet.content());
@@ -447,7 +455,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && serverConnection.getPhase().consideredComplete()) { final boolean stateAllowsForward = smc != null
&& !smc.isClosed()
&& serverConnection.getPhase().consideredComplete()
&& smc.getState() == StateRegistry.PLAY;
if (stateAllowsForward) {
if (packet instanceof PluginMessagePacket) { if (packet instanceof PluginMessagePacket) {
((PluginMessagePacket) packet).retain(); ((PluginMessagePacket) packet).retain();
} }
@@ -464,7 +476,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { final boolean stateAllowsForward = smc != null
&& !smc.isClosed()
&& serverConnection.getPhase().consideredComplete()
&& smc.getState() == StateRegistry.PLAY;
if (stateAllowsForward) {
smc.write(buf.retain()); smc.write(buf.retain());
} }
} }
@@ -476,14 +492,24 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void exception(Throwable throwable) { public void exception(Throwable throwable) {
player.disconnect( player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); if (MinecraftDecoder.DEBUG) {
logger.info("Exception while handling packet for {}", player, throwable);
}
} }
@Override @Override
public void writabilityChanged() { public void writabilityChanged() {
boolean writable = player.getConnection().getChannel().isWritable(); boolean writable = player.getConnection().getChannel().isWritable();
if (BACKPRESSURE_LOG) {
if (writable) {
logger.info("{} is writable, will auto-read backend connection data", player);
} else {
logger.info("{} is not writable, not auto-reading backend connection data", player);
}
}
if (!writable) { if (!writable) {
// We might have packets queued from the server, so flush them now to free up memory. Make // We might have packets queued from the server, so flush them now to free up memory. Make
// sure to do it on a future invocation of the event loop, otherwise while the issue will // sure to do it on a future invocation of the event loop, otherwise while the issue will
@@ -135,6 +135,7 @@ import net.kyori.adventure.sound.SoundStop;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.minimessage.translation.Argument;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import net.kyori.adventure.title.Title.Times; import net.kyori.adventure.title.Title.Times;
@@ -713,12 +714,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component friendlyError; Component friendlyError;
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
friendlyError = Component.translatable("velocity.error.connected-server-error", friendlyError = Component.translatable("velocity.error.connected-server-error",
Component.text(server.getServerInfo().getName())); Argument.string("server", server.getServerInfo().getName()));
} else { } else {
logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(), logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(),
wrapped); wrapped);
friendlyError = Component.translatable("velocity.error.connecting-server-error", friendlyError = Component.translatable("velocity.error.connecting-server-error",
Component.text(server.getServerInfo().getName())); Argument.string("server", server.getServerInfo().getName()));
} }
handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe); handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
} }
@@ -746,7 +747,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
handleConnectionException(server, disconnectReason, handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED, Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED,
Component.text(server.getServerInfo().getName()), Argument.string("server", server.getServerInfo().getName()),
disconnectReason), safe); disconnectReason), safe);
} else { } else {
if (this.server.getConfiguration().isLogPlayerConnections()) { if (this.server.getConfiguration().isLogPlayerConnections()) {
@@ -755,7 +756,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
handleConnectionException(server, disconnectReason, handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED, Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
Component.text(server.getServerInfo().getName()), Argument.string("server", server.getServerInfo().getName()),
disconnectReason), safe); disconnectReason), safe);
} }
} }
@@ -811,61 +812,56 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return; return;
} }
if (event.getResult() instanceof final DisconnectPlayer res) { switch (event.getResult()) {
disconnect(res.getReasonComponent()); case DisconnectPlayer res -> disconnect(res.getReasonComponent());
} else if (event.getResult() instanceof final RedirectPlayer res) { case RedirectPlayer res -> createConnectionRequest(res.getServer(), previousConnection).connect()
createConnectionRequest(res.getServer(), previousConnection).connect() .whenCompleteAsync((status, throwable) -> {
.whenCompleteAsync((status, throwable) -> { if (throwable != null) {
if (throwable != null) { handleConnectionException(res.getServer(), throwable, true);
handleConnectionException(res.getServer(), throwable, true); return;
return; }
}
switch (status.getStatus()) { switch (status.getStatus()) {
// Impossible/nonsensical cases // Impossible/nonsensical cases
case ALREADY_CONNECTED: case ALREADY_CONNECTED -> logger.error("{}: already connected to {}", this,
logger.error("{}: already connected to {}", this, status.getAttemptedConnection().getServerInfo().getName());
status.getAttemptedConnection().getServerInfo().getName()); case CONNECTION_IN_PROGRESS, CONNECTION_CANCELLED -> {
break; Component fallbackMsg = res.getMessageComponent();
case CONNECTION_IN_PROGRESS: if (fallbackMsg == null) {
// Fatal case fallbackMsg = friendlyReason;
case CONNECTION_CANCELLED: }
Component fallbackMsg = res.getMessageComponent(); disconnect(status.getReasonComponent().orElse(fallbackMsg));
if (fallbackMsg == null) { }
fallbackMsg = friendlyReason; case SERVER_DISCONNECTED -> {
Component reason = status.getReasonComponent()
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
handleConnectionException(res.getServer(),
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
((Impl) status).isSafe());
}
case SUCCESS -> {
Component requestedMessage = res.getMessageComponent();
if (requestedMessage == null) {
requestedMessage = friendlyReason;
}
if (requestedMessage != Component.empty()) {
sendMessage(requestedMessage);
}
}
default -> {
// The only remaining value is successful (no need to do anything!)
}
} }
disconnect(status.getReasonComponent().orElse(fallbackMsg)); }, connection.eventLoop());
break; case Notify res -> {
case SERVER_DISCONNECTED: if (event.kickedDuringServerConnect() && previousConnection != null) {
Component reason = status.getReasonComponent() sendMessage(res.getMessageComponent());
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); } else {
handleConnectionException(res.getServer(), disconnect(res.getMessageComponent());
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), }
((Impl) status).isSafe());
break;
case SUCCESS:
Component requestedMessage = res.getMessageComponent();
if (requestedMessage == null) {
requestedMessage = friendlyReason;
}
if (requestedMessage != Component.empty()) {
sendMessage(requestedMessage);
}
break;
default:
// The only remaining value is successful (no need to do anything!)
break;
}
}, connection.eventLoop());
} else if (event.getResult() instanceof final Notify res) {
if (event.kickedDuringServerConnect() && previousConnection != null) {
sendMessage(res.getMessageComponent());
} else {
disconnect(res.getMessageComponent());
} }
} else {
// In case someone gets creative, assume we want to disconnect the player. // In case someone gets creative, assume we want to disconnect the player.
disconnect(friendlyReason); default -> disconnect(friendlyReason);
} }
}, connection.eventLoop()); }, connection.eventLoop());
} }
@@ -1353,11 +1349,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
if (sentTime != null) { if (sentTime != null) {
final MinecraftConnection smc = serverConnection.getConnection(); final MinecraftConnection smc = serverConnection.getConnection();
if (smc != null) { final StateRegistry clientState = connection.getState();
final boolean stateAllowsForward = smc != null
&& !smc.isClosed()
&& clientState == smc.getState()
&& (clientState == StateRegistry.CONFIG || clientState == StateRegistry.PLAY);
if (stateAllowsForward) {
setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime)); setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
smc.write(packet); smc.write(packet);
return true;
} }
// We removed this, and so this is ours
return true;
} }
} }
return false; return false;
@@ -1367,7 +1369,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
* Switches the connection to the client into config state. * Switches the connection to the client into config state.
*/ */
public void switchToConfigState() { public void switchToConfigState() {
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer())) final VelocityServerConnection targetServer = getConnectionInFlightOrConnectedServer();
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, targetServer))
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> { .completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
// if the connection was closed earlier, there is a risk that the player is no longer connected // if the connection was closed earlier, there is a risk that the player is no longer connected
if (!connection.getChannel().isActive()) { if (!connection.getChannel().isActive()) {
@@ -1382,7 +1385,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
connection.pendingConfigurationSwitch = true; connection.pendingConfigurationSwitch = true;
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
// Make sure we don't send any play packets to the player after update start // Make sure we don't send any play packets to the player after update start
connection.addPlayPacketQueueHandler(); connection.addPlayPacketQueueOutboundHandler();
}, connection.eventLoop()).exceptionally((ex) -> { }, connection.eventLoop()).exceptionally((ex) -> {
logger.error("Error switching player connection to config state", ex); logger.error("Error switching player connection to config state", ex);
return null; return null;
@@ -127,10 +127,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
if (!handshake.getProtocolVersion().isSupported()) { if (!handshake.getProtocolVersion().isSupported()) {
// Bump connection into correct protocol state so that we can send the disconnect packet. // Bump connection into correct protocol state so that we can send the disconnect packet.
connection.setState(StateRegistry.LOGIN); connection.setState(StateRegistry.LOGIN);
ic.disconnectQuietly(Component.translatable() ic.disconnectQuietly(Component.translatable(
.key("multiplayer.disconnect.outdated_client") "multiplayer.disconnect.outdated_client",
.arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)) Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)
.build()); ));
return; return;
} }
@@ -152,7 +152,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} else { } else {
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, new AuthSessionHandler(server, inbound,
GameProfile.forOfflinePlayer(login.getUsername()), false)); GameProfile.forOfflinePlayer(login.getUsername()), false, null));
} }
}); });
}); });
@@ -214,6 +214,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
server.getVersion().getName() + "/" + server.getVersion().getVersion()) server.getVersion().getName() + "/" + server.getVersion().getVersion())
.uri(URI.create(url)) .uri(URI.create(url))
.build(); .build();
//noinspection resource
final HttpClient httpClient = server.createHttpClient(); final HttpClient httpClient = server.createHttpClient();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
.whenCompleteAsync((response, throwable) -> { .whenCompleteAsync((response, throwable) -> {
@@ -254,7 +255,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
// All went well, initialize the session. // All went well, initialize the session.
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
new AuthSessionHandler(server, inbound, profile, true)); new AuthSessionHandler(server, inbound, profile, true, serverId));
} else if (response.statusCode() == 204) { } else if (response.statusCode() == 204) {
// Apparently an offline-mode user logged onto this online-mode proxy. // Apparently an offline-mode user logged onto this online-mode proxy.
inbound.disconnect( inbound.disconnect(
@@ -268,14 +269,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
} }
}, mcConnection.eventLoop()) }, mcConnection.eventLoop())
.thenRun(() -> { .thenRun(() -> {
if (httpClient instanceof final AutoCloseable closeable) { try {
try { httpClient.close();
closeable.close(); } catch (Exception e) {
} catch (Exception e) { // In Java 21, the HttpClient does not throw any Exception
// In Java 21, the HttpClient does not throw any Exception // when trying to clean its resources, so this should not happen
// when trying to clean its resources, so this should not happen logger.error("An unknown error occurred while trying to close an HttpClient", e);
logger.error("An unknown error occurred while trying to close an HttpClient", e);
}
} }
}); });
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
@@ -94,7 +94,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
this.outstandingResourcePacks.get(info.getId()); this.outstandingResourcePacks.get(info.getId());
outstandingResourcePacks.add(info); outstandingResourcePacks.add(info);
if (outstandingResourcePacks.size() == 1) { if (outstandingResourcePacks.size() == 1) {
tickResourcePackQueue(outstandingResourcePacks.get(0).getId()); tickResourcePackQueue(outstandingResourcePacks.getFirst().getId());
} }
} }
@@ -111,7 +111,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
final List<ResourcePackInfo> outstandingResourcePacks = final List<ResourcePackInfo> outstandingResourcePacks =
this.outstandingResourcePacks.get(uuid); this.outstandingResourcePacks.get(uuid);
if (!outstandingResourcePacks.isEmpty()) { if (!outstandingResourcePacks.isEmpty()) {
sendResourcePackRequestPacket(outstandingResourcePacks.get(0)); sendResourcePackRequestPacket(outstandingResourcePacks.getFirst());
} }
} }
@@ -124,7 +124,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
this.outstandingResourcePacks.get(uuid); this.outstandingResourcePacks.get(uuid);
final boolean peek = bundle.status().isIntermediate(); final boolean peek = bundle.status().isIntermediate();
final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null : final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null :
peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0); peek ? outstandingResourcePacks.getFirst() : outstandingResourcePacks.removeFirst();
server.getEventManager() server.getEventManager()
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued)) .fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
@@ -100,65 +100,60 @@ public class ServerListPingHandler {
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings, CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback); (ex) -> fallback);
switch (mode) { return switch (mode) {
case ALL: case ALL -> pingResponses.thenApply(responses -> {
return pingResponses.thenApply(responses -> { // Find the first non-fallback
// Find the first non-fallback for (ServerPing response : responses) {
for (ServerPing response : responses) { if (response == fallback) {
if (response == fallback) { continue;
continue;
}
if (response.getDescriptionComponent() == null) {
return response.asBuilder()
.description(Component.empty())
.build();
}
return response;
} }
return fallback;
});
case MODS:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback that contains a mod list
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
Optional<ModInfo> modInfo = response.getModinfo();
if (modInfo.isPresent()) {
return fallback.asBuilder().mods(modInfo.get()).build();
}
}
return fallback;
});
case DESCRIPTION:
return pingResponses.thenApply(responses -> {
// Find the first non-fallback. If it includes a modlist, add it too.
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
if (response.getDescriptionComponent() == null) { if (response.getDescriptionComponent() == null) {
continue; return response.asBuilder()
} .description(Component.empty())
.build();
return new ServerPing(
fallback.getVersion(),
fallback.getPlayers().orElse(null),
response.getDescriptionComponent(),
fallback.getFavicon().orElse(null),
response.getModinfo().orElse(null)
);
} }
return fallback;
}); return response;
}
return fallback;
});
case MODS -> pingResponses.thenApply(responses -> {
// Find the first non-fallback that contains a mod list
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
Optional<ModInfo> modInfo = response.getModinfo();
if (modInfo.isPresent()) {
return fallback.asBuilder().mods(modInfo.get()).build();
}
}
return fallback;
});
case DESCRIPTION -> pingResponses.thenApply(responses -> {
// Find the first non-fallback. If it includes a modlist, add it too.
for (ServerPing response : responses) {
if (response == fallback) {
continue;
}
if (response.getDescriptionComponent() == null) {
continue;
}
return new ServerPing(
fallback.getVersion(),
fallback.getPlayers().orElse(null),
response.getDescriptionComponent(),
fallback.getFavicon().orElse(null),
response.getModinfo().orElse(null)
);
}
return fallback;
});
// Not possible, but covered for completeness. // Not possible, but covered for completeness.
default: default -> CompletableFuture.completedFuture(fallback);
return CompletableFuture.completedFuture(fallback); };
}
} }
/** /**
@@ -33,6 +33,7 @@ import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers; import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type; import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger; import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
@@ -59,11 +60,11 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
private final VelocityServer server; private final VelocityServer server;
private PermissionFunction permissionFunction = ALWAYS_TRUE; private PermissionFunction permissionFunction = ALWAYS_TRUE;
private final @NotNull Pointers pointers = ConsoleCommandSource.super.pointers().toBuilder() private static final @NotNull PointersSupplier<VelocityConsole> POINTERS = PointersSupplier.<VelocityConsole>builder()
.withDynamic(PermissionChecker.POINTER, this::getPermissionChecker) .resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
.withDynamic(Identity.LOCALE, () -> ClosestLocaleMatcher.INSTANCE .resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
.lookupClosest(Locale.getDefault())) .lookupClosest(Locale.getDefault()))
.withStatic(FacetPointers.TYPE, Type.CONSOLE) .resolving(FacetPointers.TYPE, (console) -> Type.CONSOLE)
.build(); .build();
public VelocityConsole(VelocityServer server) { public VelocityConsole(VelocityServer server) {
@@ -153,6 +154,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
@Override @Override
public @NotNull Pointers pointers() { public @NotNull Pointers pointers() {
return pointers; return POINTERS.view(this);
} }
} }
@@ -26,8 +26,10 @@ import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.config.VelocityConfiguration;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
@@ -72,6 +74,17 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
new HandshakeSessionHandler(connection, this.server)); new HandshakeSessionHandler(connection, this.server));
ch.pipeline().addLast(Connections.HANDLER, connection); ch.pipeline().addLast(Connections.HANDLER, connection);
VelocityConfiguration.PacketLimiterConfig packetLimiterConfig =
server.getConfiguration().getPacketLimiterConfig();
int configuredInterval = packetLimiterConfig.interval();
int configuredPacketsPerSecond = packetLimiterConfig.pps();
int configuredBytes = packetLimiterConfig.bytes();
if (configuredInterval > 0 && (configuredBytes > 0 || configuredPacketsPerSecond > 0)) {
ch.pipeline().get(MinecraftVarintFrameDecoder.class).setPacketLimiter(
new SimpleBytesPerSecondLimiter(configuredPacketsPerSecond, configuredBytes, configuredInterval)
);
}
if (this.server.getConfiguration().isProxyProtocol()) { if (this.server.getConfiguration().isProxyProtocol()) {
ch.pipeline().addFirst(new HAProxyMessageDecoder()); ch.pipeline().addFirst(new HAProxyMessageDecoder());
} }
@@ -0,0 +1,32 @@
/*
* 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.network.limiter;
/**
* PacketLimiter enforces a limit on the number of bytes processed over a time window.
* Implementations should be thread-safe.
*/
public interface PacketLimiter {
/**
* Attempts to record the specified number of bytes within the current window.
*
* @param bytes the number of bytes to record
* @return true if the bytes are allowed and recorded; false if the limit would be exceeded
*/
boolean account(int bytes);
}
@@ -0,0 +1,77 @@
/*
* 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.network.limiter;
import com.velocitypowered.proxy.util.IntervalledCounter;
import org.jspecify.annotations.Nullable;
/**
* A moving-window limiter over a configurable number of seconds.
* It enforces both packets-per-second and average bytes-per-second limits.
* The effective cap over the full window equals limitPerSecond * windowSeconds.
*/
public final class SimpleBytesPerSecondLimiter implements PacketLimiter {
@Nullable
private final IntervalledCounter bytesCounter;
@Nullable
private final IntervalledCounter packetsCounter;
private final int packetsPerSecond;
private final int bytesPerSecond;
/**
* Creates a new SimpleBytesPerSecondLimiter.
*
* @param packetsPerSecond maximum average packets per second allowed (> 0)
* @param bytesPerSecond maximum average bytes per second allowed (> 0)
* @param windowSeconds number of seconds in the moving window (> 0)
*/
public SimpleBytesPerSecondLimiter(int packetsPerSecond, int bytesPerSecond, int windowSeconds) {
this.packetsPerSecond = packetsPerSecond;
if (windowSeconds <= 0) {
throw new IllegalArgumentException("windowSeconds must be > 0");
}
this.bytesPerSecond = bytesPerSecond;
this.packetsCounter = packetsPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
this.bytesCounter = bytesPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
}
/**
* Records the given payload length as one packet and returns whether it is allowed.
*/
@SuppressWarnings("RedundantIfStatement")
@Override
public boolean account(int bytes) {
long currTime = System.nanoTime();
if (packetsCounter != null) {
packetsCounter.updateAndAdd(1, currTime);
if (packetsCounter.getRate() > packetsPerSecond) {
return false;
}
}
if (bytesCounter != null) {
bytesCounter.updateAndAdd(bytes, currTime);
if (bytesCounter.getRate() > bytesPerSecond) {
return false;
}
}
return true;
}
}
@@ -71,7 +71,7 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception { protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
List<InetAddress> addresses = cache.getIfPresent(inetHost); List<InetAddress> addresses = cache.getIfPresent(inetHost);
if (addresses != null) { if (addresses != null) {
promise.trySuccess(addresses.get(0)); promise.trySuccess(addresses.getFirst());
return; return;
} }
@@ -83,7 +83,7 @@ public class JavaPluginLoader implements PluginLoader {
@Override @Override
public PluginDescription createPluginFromCandidate(PluginDescription candidate) throws Exception { public PluginDescription createPluginFromCandidate(PluginDescription candidate) throws Exception {
if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate)) { if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate candidateInst)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader"); throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
} }
@@ -93,8 +93,6 @@ public class JavaPluginLoader implements PluginLoader {
PluginClassLoader loader = new PluginClassLoader(new URL[]{pluginJarUrl}); PluginClassLoader loader = new PluginClassLoader(new URL[]{pluginJarUrl});
loader.addToClassloaders(); loader.addToClassloaders();
JavaVelocityPluginDescriptionCandidate candidateInst =
(JavaVelocityPluginDescriptionCandidate) candidate;
Class<?> mainClass = loader.loadClass(candidateInst.getMainClass()); Class<?> mainClass = loader.loadClass(candidateInst.getMainClass());
return createDescription(candidateInst, mainClass); return createDescription(candidateInst, mainClass);
} }
@@ -102,11 +100,10 @@ public class JavaPluginLoader implements PluginLoader {
@Override @Override
public Module createModule(PluginContainer container) { public Module createModule(PluginContainer container) {
PluginDescription description = container.getDescription(); PluginDescription description = container.getDescription();
if (!(description instanceof JavaVelocityPluginDescription)) { if (!(description instanceof JavaVelocityPluginDescription javaDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader"); throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
} }
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
Optional<Path> source = javaDescription.getSource(); Optional<Path> source = javaDescription.getSource();
if (source.isEmpty()) { if (source.isEmpty()) {
@@ -118,24 +115,23 @@ public class JavaPluginLoader implements PluginLoader {
@Override @Override
public void createPlugin(PluginContainer container, Module... modules) { public void createPlugin(PluginContainer container, Module... modules) {
if (!(container instanceof VelocityPluginContainer)) { if (!(container instanceof VelocityPluginContainer pluginContainer)) {
throw new IllegalArgumentException("Container provided isn't of the Java plugin loader"); throw new IllegalArgumentException("Container provided isn't of the Java plugin loader");
} }
PluginDescription description = container.getDescription(); PluginDescription description = pluginContainer.getDescription();
if (!(description instanceof JavaVelocityPluginDescription)) { if (!(description instanceof JavaVelocityPluginDescription javaPluginDescription)) {
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader"); throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
} }
Injector injector = Guice.createInjector(modules); Injector injector = Guice.createInjector(modules);
Object instance = injector Object instance = injector.getInstance(javaPluginDescription.getMainClass());
.getInstance(((JavaVelocityPluginDescription) description).getMainClass());
if (instance == null) { if (instance == null) {
throw new IllegalStateException( throw new IllegalStateException(
"Got nothing from injector for plugin " + description.getId()); "Got nothing from injector for plugin " + description.getId());
} }
((VelocityPluginContainer) container).setInstance(instance); pluginContainer.setInstance(instance);
} }
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source) private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source)
@@ -145,22 +141,23 @@ public class JavaPluginLoader implements PluginLoader {
new BufferedInputStream(Files.newInputStream(source)))) { new BufferedInputStream(Files.newInputStream(source)))) {
JarEntry entry; JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) { while ((entry = in.getNextJarEntry()) != null) {
if (entry.getName().equals("velocity-plugin.json")) { switch (entry.getName()) {
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) { case "velocity-plugin.json" -> {
return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader, try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
SerializedPluginDescription.class)); return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
SerializedPluginDescription.class));
}
}
case "paper-plugin.yml", "plugin.yml", "bungee.yml" -> foundBungeeBukkitPluginFile = true;
default -> {
} }
}
if (entry.getName().equals("plugin.yml") || entry.getName().equals("bungee.yml")) {
foundBungeeBukkitPluginFile = true;
} }
} }
if (foundBungeeBukkitPluginFile) { if (foundBungeeBukkitPluginFile) {
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to " throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
+ "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord " + "be a Paper, Bukkit or BungeeCord plugin. Velocity does not support plugins from these "
+ "plugins."); + "platforms.");
} }
return Optional.empty(); return Optional.empty();
@@ -36,7 +36,9 @@ import io.netty.handler.codec.EncoderException;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.BinaryTag;
@@ -159,6 +161,9 @@ public enum ProtocolUtils {
VAR_INT_LENGTHS[32] = 1; // Special case for the number 0. VAR_INT_LENGTHS[32] = 1; // Special case for the number 0.
} }
public static final int DEFAULT_MAX_STRING_BYTES = varIntBytes(ByteBufUtil.utf8MaxBytes(DEFAULT_MAX_STRING_SIZE))
+ ByteBufUtil.utf8MaxBytes(DEFAULT_MAX_STRING_SIZE);
private static DecoderException badVarint() { private static DecoderException badVarint() {
return MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded") return MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
: BAD_VARINT_CACHED; : BAD_VARINT_CACHED;
@@ -415,7 +420,10 @@ public enum ProtocolUtils {
*/ */
public static int[] readIntegerArray(ByteBuf buf) { public static int[] readIntegerArray(ByteBuf buf) {
int len = readVarInt(buf); int len = readVarInt(buf);
checkArgument(len >= 0, "Got a negative-length integer array (%s)", len); checkFrame(len >= 0, "Got a negative-length integer array (%s)", len);
checkFrame(buf.isReadable(len),
"Trying to read an array that is too long (wanted %s, only have %s)", len,
buf.readableBytes());
int[] array = new int[len]; int[] array = new int[len];
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
array[i] = readVarInt(buf); array[i] = readVarInt(buf);
@@ -535,6 +543,10 @@ public enum ProtocolUtils {
*/ */
public static String[] readStringArray(ByteBuf buf) { public static String[] readStringArray(ByteBuf buf) {
int length = readVarInt(buf); int length = readVarInt(buf);
checkFrame(length >= 0, "Got a negative-length array (%s)", length);
checkFrame(buf.isReadable(length),
"Trying to read an array that is too long (wanted %s, only have %s)", length,
buf.readableBytes());
String[] ret = new String[length]; String[] ret = new String[length];
for (int i = 0; i < length; i++) { for (int i = 0; i < length; i++) {
ret[i] = readString(buf); ret[i] = readString(buf);
@@ -646,6 +658,9 @@ public enum ProtocolUtils {
checkArgument(len <= FORGE_MAX_ARRAY_LENGTH, checkArgument(len <= FORGE_MAX_ARRAY_LENGTH,
"Cannot receive array longer than %s (got %s bytes)", FORGE_MAX_ARRAY_LENGTH, len); "Cannot receive array longer than %s (got %s bytes)", FORGE_MAX_ARRAY_LENGTH, len);
checkFrame(buf.isReadable(len),
"Trying to read an array that is too long (wanted %s, only have %s)", len,
buf.readableBytes());
byte[] ret = new byte[len]; byte[] ret = new byte[len];
buf.readBytes(ret); buf.readBytes(ret);
@@ -844,6 +859,29 @@ public enum ProtocolUtils {
writeVarInt(buf, source.ordinal()); writeVarInt(buf, source.ordinal());
} }
/**
* Returns a pre-sized list with a max initial size of {@code Short.MAX_VALUE}.
*
* @param initialCapacity expected initial capacity
* @param <T> entry type
* @return pre-sized list
*/
public static <T> List<T> newList(int initialCapacity) {
return new ArrayList<>(Math.min(initialCapacity, Short.MAX_VALUE));
}
/**
* Returns a pre-sized map with a max initial size of {@code Short.MAX_VALUE}.
*
* @param initialCapacity expected initial capacity
* @param <K> key type
* @param <V> value type
* @return pre-sized map
*/
public static <K, V> Map<K, V> newMap(int initialCapacity) {
return new HashMap<>(Math.min(initialCapacity, Short.MAX_VALUE));
}
/** /**
* Represents the direction in which a packet flows. * Represents the direction in which a packet flows.
*/ */
@@ -851,4 +889,4 @@ public enum ProtocolUtils {
SERVERBOUND, SERVERBOUND,
CLIENTBOUND CLIENTBOUND
} }
} }
@@ -46,6 +46,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1;
import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION; import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION;
import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS; import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS;
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
@@ -279,7 +280,8 @@ public enum StateRegistry {
map(0x0A, MINECRAFT_1_20_2, false), map(0x0A, MINECRAFT_1_20_2, false),
map(0x0B, MINECRAFT_1_20_5, false), map(0x0B, MINECRAFT_1_20_5, false),
map(0x0D, MINECRAFT_1_21_2, false), map(0x0D, MINECRAFT_1_21_2, false),
map(0x0E, MINECRAFT_1_21_6, false)); map(0x0E, MINECRAFT_1_21_6, false),
map(0x0F, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
LegacyChatPacket.class, LegacyChatPacket.class,
LegacyChatPacket::new, LegacyChatPacket::new,
@@ -293,7 +295,8 @@ public enum StateRegistry {
ChatAcknowledgementPacket::new, ChatAcknowledgementPacket::new,
map(0x03, MINECRAFT_1_19_3, false), map(0x03, MINECRAFT_1_19_3, false),
map(0x04, MINECRAFT_1_21_2, false), map(0x04, MINECRAFT_1_21_2, false),
map(0x05, MINECRAFT_1_21_6, false)); map(0x05, MINECRAFT_1_21_6, false),
map(0x06, MINECRAFT_26_1, false));
serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new, serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new,
map(0x03, MINECRAFT_1_19, false), map(0x03, MINECRAFT_1_19, false),
map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false));
@@ -304,18 +307,21 @@ public enum StateRegistry {
map(0x04, MINECRAFT_1_19_3, false), map(0x04, MINECRAFT_1_19_3, false),
map(0x05, MINECRAFT_1_20_5, false), map(0x05, MINECRAFT_1_20_5, false),
map(0x06, MINECRAFT_1_21_2, false), map(0x06, MINECRAFT_1_21_2, false),
map(0x07, MINECRAFT_1_21_6, false)); map(0x07, MINECRAFT_1_21_6, false),
map(0x08, MINECRAFT_26_1, false));
serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new, serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new,
map(0x04, MINECRAFT_1_20_5, false), map(0x04, MINECRAFT_1_20_5, false),
map(0x05, MINECRAFT_1_21_2, false), map(0x05, MINECRAFT_1_21_2, false),
map(0x06, MINECRAFT_1_21_6, false)); map(0x06, MINECRAFT_1_21_6, false),
map(0x07, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
SessionPlayerChatPacket.class, SessionPlayerChatPacket.class,
SessionPlayerChatPacket::new, SessionPlayerChatPacket::new,
map(0x05, MINECRAFT_1_19_3, false), map(0x05, MINECRAFT_1_19_3, false),
map(0x06, MINECRAFT_1_20_5, false), map(0x06, MINECRAFT_1_20_5, false),
map(0x07, MINECRAFT_1_21_2, false), map(0x07, MINECRAFT_1_21_2, false),
map(0x08, MINECRAFT_1_21_6, false)); map(0x08, MINECRAFT_1_21_6, false),
map(0x09, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
ClientSettingsPacket.class, ClientSettingsPacket.class,
ClientSettingsPacket::new, ClientSettingsPacket::new,
@@ -331,12 +337,14 @@ public enum StateRegistry {
map(0x09, MINECRAFT_1_20_2, false), map(0x09, MINECRAFT_1_20_2, false),
map(0x0A, MINECRAFT_1_20_5, false), map(0x0A, MINECRAFT_1_20_5, false),
map(0x0C, MINECRAFT_1_21_2, false), map(0x0C, MINECRAFT_1_21_2, false),
map(0x0D, MINECRAFT_1_21_6, false)); map(0x0D, MINECRAFT_1_21_6, false),
map(0x0E, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new,
map(0x11, MINECRAFT_1_20_5, false), map(0x11, MINECRAFT_1_20_5, false),
map(0x13, MINECRAFT_1_21_2, false), map(0x13, MINECRAFT_1_21_2, false),
map(0x14, MINECRAFT_1_21_6, false)); map(0x14, MINECRAFT_1_21_6, false),
map(0x15, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -355,7 +363,8 @@ public enum StateRegistry {
map(0x10, MINECRAFT_1_20_3, false), map(0x10, MINECRAFT_1_20_3, false),
map(0x12, MINECRAFT_1_20_5, false), map(0x12, MINECRAFT_1_20_5, false),
map(0x14, MINECRAFT_1_21_2, false), map(0x14, MINECRAFT_1_21_2, false),
map(0x15, MINECRAFT_1_21_6, false)); map(0x15, MINECRAFT_1_21_6, false),
map(0x16, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
KeepAlivePacket.class, KeepAlivePacket.class,
KeepAlivePacket::new, KeepAlivePacket::new,
@@ -375,7 +384,8 @@ public enum StateRegistry {
map(0x15, MINECRAFT_1_20_3, false), map(0x15, MINECRAFT_1_20_3, false),
map(0x18, MINECRAFT_1_20_5, false), map(0x18, MINECRAFT_1_20_5, false),
map(0x1A, MINECRAFT_1_21_2, false), map(0x1A, MINECRAFT_1_21_2, false),
map(0x1B, MINECRAFT_1_21_6, false)); map(0x1B, MINECRAFT_1_21_6, false),
map(0x1C, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
ResourcePackResponsePacket.class, ResourcePackResponsePacket.class,
ResourcePackResponsePacket::new, ResourcePackResponsePacket::new,
@@ -393,13 +403,15 @@ public enum StateRegistry {
map(0x2B, MINECRAFT_1_20_5, false), map(0x2B, MINECRAFT_1_20_5, false),
map(0x2D, MINECRAFT_1_21_2, false), map(0x2D, MINECRAFT_1_21_2, false),
map(0x2F, MINECRAFT_1_21_4, false), map(0x2F, MINECRAFT_1_21_4, false),
map(0x30, MINECRAFT_1_21_6, false)); map(0x30, MINECRAFT_1_21_6, false),
map(0x31, MINECRAFT_26_1, false));
serverbound.register( serverbound.register(
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
map(0x0B, MINECRAFT_1_20_2, false), map(0x0B, MINECRAFT_1_20_2, false),
map(0x0C, MINECRAFT_1_20_5, false), map(0x0C, MINECRAFT_1_20_5, false),
map(0x0E, MINECRAFT_1_21_2, false), map(0x0E, MINECRAFT_1_21_2, false),
map(0x0F, MINECRAFT_1_21_6, false)); map(0x0F, MINECRAFT_1_21_6, false),
map(0x10, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
BossBarPacket.class, BossBarPacket.class,
@@ -460,7 +472,8 @@ public enum StateRegistry {
map(0x67, MINECRAFT_1_20_5, true), map(0x67, MINECRAFT_1_20_5, true),
map(0x6E, MINECRAFT_1_21_2, true), map(0x6E, MINECRAFT_1_21_2, true),
map(0x6D, MINECRAFT_1_21_5, true), map(0x6D, MINECRAFT_1_21_5, true),
map(0x72, MINECRAFT_1_21_9, true)); map(0x72, MINECRAFT_1_21_9, true),
map(0x74, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new, ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true), map(0x5F, MINECRAFT_1_19_3, true),
@@ -470,7 +483,8 @@ public enum StateRegistry {
map(0x6A, MINECRAFT_1_20_5, true), map(0x6A, MINECRAFT_1_20_5, true),
map(0x71, MINECRAFT_1_21_2, true), map(0x71, MINECRAFT_1_21_2, true),
map(0x70, MINECRAFT_1_21_5, true), map(0x70, MINECRAFT_1_21_5, true),
map(0x75, MINECRAFT_1_21_9, true)); map(0x75, MINECRAFT_1_21_9, true),
map(0x77, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
PluginMessagePacket.class, PluginMessagePacket.class,
PluginMessagePacket::new, PluginMessagePacket::new,
@@ -527,7 +541,8 @@ public enum StateRegistry {
map(0x26, MINECRAFT_1_20_5, false), map(0x26, MINECRAFT_1_20_5, false),
map(0x27, MINECRAFT_1_21_2, false), map(0x27, MINECRAFT_1_21_2, false),
map(0x26, MINECRAFT_1_21_5, false), map(0x26, MINECRAFT_1_21_5, false),
map(0x2B, MINECRAFT_1_21_9, false)); map(0x2B, MINECRAFT_1_21_9, false),
map(0x2C, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
JoinGamePacket.class, JoinGamePacket.class,
JoinGamePacket::new, JoinGamePacket::new,
@@ -547,7 +562,8 @@ public enum StateRegistry {
map(0x2B, MINECRAFT_1_20_5, false), map(0x2B, MINECRAFT_1_20_5, false),
map(0x2C, MINECRAFT_1_21_2, false), map(0x2C, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_5, false), map(0x2B, MINECRAFT_1_21_5, false),
map(0x30, MINECRAFT_1_21_9, false)); map(0x30, MINECRAFT_1_21_9, false),
map(0x31, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
RespawnPacket.class, RespawnPacket.class,
RespawnPacket::new, RespawnPacket::new,
@@ -570,7 +586,8 @@ public enum StateRegistry {
map(0x47, MINECRAFT_1_20_5, true), map(0x47, MINECRAFT_1_20_5, true),
map(0x4C, MINECRAFT_1_21_2, true), map(0x4C, MINECRAFT_1_21_2, true),
map(0x4B, MINECRAFT_1_21_5, true), map(0x4B, MINECRAFT_1_21_5, true),
map(0x50, MINECRAFT_1_21_9, true)); map(0x50, MINECRAFT_1_21_9, true),
map(0x52, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
RemoveResourcePackPacket.class, RemoveResourcePackPacket.class,
RemoveResourcePackPacket::new, RemoveResourcePackPacket::new,
@@ -578,7 +595,8 @@ public enum StateRegistry {
map(0x45, MINECRAFT_1_20_5, false), map(0x45, MINECRAFT_1_20_5, false),
map(0x4A, MINECRAFT_1_21_2, false), map(0x4A, MINECRAFT_1_21_2, false),
map(0x49, MINECRAFT_1_21_5, false), map(0x49, MINECRAFT_1_21_5, false),
map(0x4E, MINECRAFT_1_21_9, false)); map(0x4E, MINECRAFT_1_21_9, false),
map(0x50, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
ResourcePackRequestPacket.class, ResourcePackRequestPacket.class,
ResourcePackRequestPacket::new, ResourcePackRequestPacket::new,
@@ -601,7 +619,8 @@ public enum StateRegistry {
map(0x46, MINECRAFT_1_20_5, false), map(0x46, MINECRAFT_1_20_5, false),
map(0x4B, MINECRAFT_1_21_2, false), map(0x4B, MINECRAFT_1_21_2, false),
map(0x4A, MINECRAFT_1_21_5, false), map(0x4A, MINECRAFT_1_21_5, false),
map(0x4F, MINECRAFT_1_21_9, false)); map(0x4F, MINECRAFT_1_21_9, false),
map(0x51, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
HeaderAndFooterPacket.class, HeaderAndFooterPacket.class,
HeaderAndFooterPacket::new, HeaderAndFooterPacket::new,
@@ -625,7 +644,8 @@ public enum StateRegistry {
map(0x6D, MINECRAFT_1_20_5, true), map(0x6D, MINECRAFT_1_20_5, true),
map(0x74, MINECRAFT_1_21_2, true), map(0x74, MINECRAFT_1_21_2, true),
map(0x73, MINECRAFT_1_21_5, true), map(0x73, MINECRAFT_1_21_5, true),
map(0x78, MINECRAFT_1_21_9, true)); map(0x78, MINECRAFT_1_21_9, true),
map(0x7A, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
LegacyTitlePacket.class, LegacyTitlePacket.class,
LegacyTitlePacket::new, LegacyTitlePacket::new,
@@ -648,7 +668,8 @@ public enum StateRegistry {
map(0x63, MINECRAFT_1_20_5, true), map(0x63, MINECRAFT_1_20_5, true),
map(0x6A, MINECRAFT_1_21_2, true), map(0x6A, MINECRAFT_1_21_2, true),
map(0x69, MINECRAFT_1_21_5, true), map(0x69, MINECRAFT_1_21_5, true),
map(0x6E, MINECRAFT_1_21_9, true)); map(0x6E, MINECRAFT_1_21_9, true),
map(0x70, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
TitleTextPacket.class, TitleTextPacket.class,
TitleTextPacket::new, TitleTextPacket::new,
@@ -662,7 +683,8 @@ public enum StateRegistry {
map(0x65, MINECRAFT_1_20_5, true), map(0x65, MINECRAFT_1_20_5, true),
map(0x6C, MINECRAFT_1_21_2, true), map(0x6C, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_5, true), map(0x6B, MINECRAFT_1_21_5, true),
map(0x70, MINECRAFT_1_21_9, true)); map(0x70, MINECRAFT_1_21_9, true),
map(0x72, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
TitleActionbarPacket.class, TitleActionbarPacket.class,
TitleActionbarPacket::new, TitleActionbarPacket::new,
@@ -676,7 +698,8 @@ public enum StateRegistry {
map(0x4C, MINECRAFT_1_20_5, true), map(0x4C, MINECRAFT_1_20_5, true),
map(0x51, MINECRAFT_1_21_2, true), map(0x51, MINECRAFT_1_21_2, true),
map(0x50, MINECRAFT_1_21_5, true), map(0x50, MINECRAFT_1_21_5, true),
map(0x55, MINECRAFT_1_21_9, true)); map(0x55, MINECRAFT_1_21_9, true),
map(0x57, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
TitleTimesPacket.class, TitleTimesPacket.class,
TitleTimesPacket::new, TitleTimesPacket::new,
@@ -690,7 +713,8 @@ public enum StateRegistry {
map(0x66, MINECRAFT_1_20_5, true), map(0x66, MINECRAFT_1_20_5, true),
map(0x6D, MINECRAFT_1_21_2, true), map(0x6D, MINECRAFT_1_21_2, true),
map(0x6C, MINECRAFT_1_21_5, true), map(0x6C, MINECRAFT_1_21_5, true),
map(0x71, MINECRAFT_1_21_9, true)); map(0x71, MINECRAFT_1_21_9, true),
map(0x73, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
TitleClearPacket.class, TitleClearPacket.class,
TitleClearPacket::new, TitleClearPacket::new,
@@ -721,7 +745,8 @@ public enum StateRegistry {
map(0x3D, MINECRAFT_1_20_5, false), map(0x3D, MINECRAFT_1_20_5, false),
map(0x3F, MINECRAFT_1_21_2, false), map(0x3F, MINECRAFT_1_21_2, false),
map(0x3E, MINECRAFT_1_21_5, false), map(0x3E, MINECRAFT_1_21_5, false),
map(0x43, MINECRAFT_1_21_9, false)); map(0x43, MINECRAFT_1_21_9, false),
map(0x45, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
UpsertPlayerInfoPacket.class, UpsertPlayerInfoPacket.class,
UpsertPlayerInfoPacket::new, UpsertPlayerInfoPacket::new,
@@ -731,13 +756,15 @@ public enum StateRegistry {
map(0x3E, MINECRAFT_1_20_5, false), map(0x3E, MINECRAFT_1_20_5, false),
map(0x40, MINECRAFT_1_21_2, false), map(0x40, MINECRAFT_1_21_2, false),
map(0x3F, MINECRAFT_1_21_5, false), map(0x3F, MINECRAFT_1_21_5, false),
map(0x44, MINECRAFT_1_21_9, false)); map(0x44, MINECRAFT_1_21_9, false),
map(0x46, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
map(0x6B, MINECRAFT_1_20_5, false), map(0x6B, MINECRAFT_1_20_5, false),
map(0x72, MINECRAFT_1_21_2, false), map(0x72, MINECRAFT_1_21_2, false),
map(0x71, MINECRAFT_1_21_5, false), map(0x71, MINECRAFT_1_21_5, false),
map(0x76, MINECRAFT_1_21_9, false)); map(0x76, MINECRAFT_1_21_9, false),
map(0x78, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
SystemChatPacket.class, SystemChatPacket.class,
SystemChatPacket::new, SystemChatPacket::new,
@@ -750,7 +777,8 @@ public enum StateRegistry {
map(0x6C, MINECRAFT_1_20_5, true), map(0x6C, MINECRAFT_1_20_5, true),
map(0x73, MINECRAFT_1_21_2, true), map(0x73, MINECRAFT_1_21_2, true),
map(0x72, MINECRAFT_1_21_5, true), map(0x72, MINECRAFT_1_21_5, true),
map(0x77, MINECRAFT_1_21_9, true)); map(0x77, MINECRAFT_1_21_9, true),
map(0x79, MINECRAFT_26_1, true));
clientbound.register( clientbound.register(
PlayerChatCompletionPacket.class, PlayerChatCompletionPacket.class,
PlayerChatCompletionPacket::new, PlayerChatCompletionPacket::new,
@@ -772,7 +800,8 @@ public enum StateRegistry {
map(0x4B, MINECRAFT_1_20_5, false), map(0x4B, MINECRAFT_1_20_5, false),
map(0x50, MINECRAFT_1_21_2, false), map(0x50, MINECRAFT_1_21_2, false),
map(0x4F, MINECRAFT_1_21_5, false), map(0x4F, MINECRAFT_1_21_5, false),
map(0x54, MINECRAFT_1_21_9, false)); map(0x54, MINECRAFT_1_21_9, false),
map(0x56, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
StartUpdatePacket.class, StartUpdatePacket.class,
() -> StartUpdatePacket.INSTANCE, () -> StartUpdatePacket.INSTANCE,
@@ -781,7 +810,8 @@ public enum StateRegistry {
map(0x69, MINECRAFT_1_20_5, false), map(0x69, MINECRAFT_1_20_5, false),
map(0x70, MINECRAFT_1_21_2, false), map(0x70, MINECRAFT_1_21_2, false),
map(0x6F, MINECRAFT_1_21_5, false), map(0x6F, MINECRAFT_1_21_5, false),
map(0x74, MINECRAFT_1_21_9, false)); map(0x74, MINECRAFT_1_21_9, false),
map(0x76, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
BundleDelimiterPacket.class, BundleDelimiterPacket.class,
() -> BundleDelimiterPacket.INSTANCE, () -> BundleDelimiterPacket.INSTANCE,
@@ -791,13 +821,15 @@ public enum StateRegistry {
TransferPacket::new, TransferPacket::new,
map(0x73, MINECRAFT_1_20_5, false), map(0x73, MINECRAFT_1_20_5, false),
map(0x7A, MINECRAFT_1_21_2, false), map(0x7A, MINECRAFT_1_21_2, false),
map(0x7F, MINECRAFT_1_21_9, false)); map(0x7F, MINECRAFT_1_21_9, false),
map(0x81, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
ClientboundCustomReportDetailsPacket.class, ClientboundCustomReportDetailsPacket.class,
ClientboundCustomReportDetailsPacket::new, ClientboundCustomReportDetailsPacket::new,
map(0x7A, MINECRAFT_1_21, false), map(0x7A, MINECRAFT_1_21, false),
map(0x81, MINECRAFT_1_21_2, false), map(0x81, MINECRAFT_1_21_2, false),
map(0x86, MINECRAFT_1_21_9, false)); map(0x86, MINECRAFT_1_21_9, false),
map(0x88, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
ClientboundServerLinksPacket.class, ClientboundServerLinksPacket.class,
ClientboundServerLinksPacket::new, ClientboundServerLinksPacket::new,
@@ -819,7 +851,8 @@ public enum StateRegistry {
map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true), map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true),
map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true), map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true),
map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true) map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true)
); map(0x87, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false));
} }
}, },
LOGIN { LOGIN {
@@ -848,7 +881,7 @@ public enum StateRegistry {
map(0x01, MINECRAFT_1_7_2, false)); map(0x01, MINECRAFT_1_7_2, false));
clientbound.register( clientbound.register(
ServerLoginSuccessPacket.class, ServerLoginSuccessPacket::new, ServerLoginSuccessPacket.class, ServerLoginSuccessPacket::new,
map(0x02, MINECRAFT_1_7_2, false)); map(0x02, MINECRAFT_1_7_2, false));
clientbound.register( clientbound.register(
SetCompressionPacket.class, SetCompressionPacket::new, SetCompressionPacket.class, SetCompressionPacket::new,
map(0x03, MINECRAFT_1_8, false)); map(0x03, MINECRAFT_1_8, false));
@@ -119,7 +119,7 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
int sessionId = queryMessage.readInt(); int sessionId = queryMessage.readInt();
switch (type) { switch (type) {
case QUERY_TYPE_HANDSHAKE: { case QUERY_TYPE_HANDSHAKE -> {
// Generate new challenge token and put it into the sessions cache // Generate new challenge token and put it into the sessions cache
int challengeToken = random.nextInt(); int challengeToken = random.nextInt();
sessions.put(senderAddress, challengeToken); sessions.put(senderAddress, challengeToken);
@@ -132,10 +132,9 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender()); DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender());
ctx.writeAndFlush(responsePacket, ctx.voidPromise()); ctx.writeAndFlush(responsePacket, ctx.voidPromise());
break;
} }
case QUERY_TYPE_STAT: { case QUERY_TYPE_STAT -> {
// Check if query was done with session previously generated using a handshake packet // Check if query was done with session previously generated using a handshake packet
int challengeToken = queryMessage.readInt(); int challengeToken = queryMessage.readInt();
Integer session = sessions.getIfPresent(senderAddress); Integer session = sessions.getIfPresent(senderAddress);
@@ -190,10 +189,10 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
"Exception while writing GS4 response for query from {}", senderAddress, ex); "Exception while writing GS4 response for query from {}", senderAddress, ex);
return null; return null;
}); });
break;
} }
default: default -> {
// Invalid query type - just don't respond // Invalid query type - just don't respond
}
} }
} }
@@ -33,20 +33,34 @@ import java.util.List;
*/ */
public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> { public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
private static final int SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE = 2 * 1024 * 1024; // 2MiB
private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB private static final int VANILLA_MAXIMUM_UNCOMPRESSED_SIZE = 8 * 1024 * 1024; // 8MiB
private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB private static final int HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB
private static final int UNCOMPRESSED_CAP = private static final int CLIENTBOUND_UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap") Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
private static final int SERVERBOUND_UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : SERVERBOUND_MAXIMUM_UNCOMPRESSED_SIZE;
private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation"); private static final boolean SKIP_COMPRESSION_VALIDATION = Boolean.getBoolean("velocity.skip-uncompressed-packet-size-validation");
private static final double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
private final ProtocolUtils.Direction direction;
private int threshold; private int threshold;
private final VelocityCompressor compressor; private final VelocityCompressor compressor;
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) { /**
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
*
* @param threshold the threshold for compression. Packets with uncompressed size below this threshold will not be compressed.
* @param compressor the compressor instance to use
* @param direction the direction of the packets being decoded
*/
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor, ProtocolUtils.Direction direction) {
this.threshold = threshold; this.threshold = threshold;
this.compressor = compressor; this.compressor = compressor;
this.direction = direction;
} }
@Override @Override
@@ -62,17 +76,29 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
out.add(in.retain()); out.add(in.retain());
return; return;
} }
int length = in.readableBytes();
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than" checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
+ " threshold %s", claimedUncompressedSize, threshold); + " threshold %s", claimedUncompressedSize, threshold);
checkFrame(claimedUncompressedSize <= UNCOMPRESSED_CAP, if (direction == ProtocolUtils.Direction.CLIENTBOUND) {
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize, checkFrame(claimedUncompressedSize <= CLIENTBOUND_UNCOMPRESSED_CAP,
UNCOMPRESSED_CAP); "Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
CLIENTBOUND_UNCOMPRESSED_CAP);
} else {
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
SERVERBOUND_UNCOMPRESSED_CAP);
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
maxCompressedAllowed, length);
}
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in); ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize); ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
try { try {
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize); compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
out.add(uncompressed); out.add(uncompressed);
} catch (Exception e) { } catch (Exception e) {
uncompressed.release(); uncompressed.release();
@@ -57,7 +57,11 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
@Override @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf buf) { if (msg instanceof ByteBuf buf) {
tryDecode(ctx, buf); try {
tryDecode(ctx, buf);
} finally {
buf.release();
}
} else { } else {
ctx.fireChannelRead(msg); ctx.fireChannelRead(msg);
} }
@@ -65,7 +69,6 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception { private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
if (!ctx.channel().isActive() || !buf.isReadable()) { if (!ctx.channel().isActive() || !buf.isReadable()) {
buf.release();
return; return;
} }
@@ -74,24 +77,23 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
MinecraftPacket packet = this.registry.createPacket(packetId); MinecraftPacket packet = this.registry.createPacket(packetId);
if (packet == null) { if (packet == null) {
buf.readerIndex(originalReaderIndex); buf.readerIndex(originalReaderIndex);
ctx.fireChannelRead(buf); if (this.direction == ProtocolUtils.Direction.SERVERBOUND && this.state != StateRegistry.PLAY) {
} else { throw this.handleInvalidPacketId(packetId);
try {
doLengthSanityChecks(buf, packet);
try {
packet.decode(buf, direction, registry.version);
} catch (Exception e) {
throw handleDecodeFailure(e, packet, packetId);
}
if (buf.isReadable()) {
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
}
ctx.fireChannelRead(packet);
} finally {
buf.release();
} }
ctx.fireChannelRead(buf.retain());
} else {
doLengthSanityChecks(buf, packet);
try {
packet.decode(buf, direction, registry.version);
} catch (Exception e) {
throw handleDecodeFailure(e, packet, packetId);
}
if (buf.isReadable()) {
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
}
ctx.fireChannelRead(packet);
} }
} }
@@ -133,6 +135,14 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
} }
} }
private Exception handleInvalidPacketId(int packetId) {
if (DEBUG) {
return new CorruptedFrameException("Invalid packet " + getExtraConnectionDetail(packetId));
} else {
return DECODE_FAILED;
}
}
private String getExtraConnectionDetail(int packetId) { private String getExtraConnectionDetail(int packetId) {
return "Direction " + direction + " Protocol " + registry.version + " State " + state return "Direction " + direction + " Protocol " + registry.version + " State " + state
+ " ID 0x" + Integer.toHexString(packetId); + " ID 0x" + Integer.toHexString(packetId);
@@ -20,6 +20,7 @@ package com.velocitypowered.proxy.protocol.netty;
import static io.netty.util.ByteProcessor.FIND_NON_NUL; import static io.netty.util.ByteProcessor.FIND_NON_NUL;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
@@ -32,6 +33,7 @@ import io.netty.handler.codec.CorruptedFrameException;
import java.util.List; import java.util.List;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.jspecify.annotations.Nullable;
/** /**
* Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding. * Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding.
@@ -44,6 +46,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
+ "Velocity with -Dvelocity.packet-decode-logging=true to see more."); + "Velocity with -Dvelocity.packet-decode-logging=true to see more.");
private static final QuietDecoderException BAD_PACKET_LENGTH = private static final QuietDecoderException BAD_PACKET_LENGTH =
new QuietDecoderException("Bad packet length"); new QuietDecoderException("Bad packet length");
private static final QuietDecoderException INVALID_PREAMBLE =
new QuietDecoderException("Invalid packet preamble");
private static final QuietDecoderException VARINT_TOO_BIG = private static final QuietDecoderException VARINT_TOO_BIG =
new QuietDecoderException("VarInt too big"); new QuietDecoderException("VarInt too big");
private static final QuietDecoderException UNKNOWN_PACKET = private static final QuietDecoderException UNKNOWN_PACKET =
@@ -52,6 +56,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
private final ProtocolUtils.Direction direction; private final ProtocolUtils.Direction direction;
private final StateRegistry.PacketRegistry.ProtocolRegistry registry; private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
private StateRegistry state; private StateRegistry state;
@Nullable
private PacketLimiter packetLimiter;
/** /**
* Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}. * Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}.
@@ -74,38 +80,58 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
} }
// skip any runs of 0x00 we might find // skip any runs of 0x00 we might find
int wlen = in.readableBytes();
int packetStart = in.forEachByte(FIND_NON_NUL); int packetStart = in.forEachByte(FIND_NON_NUL);
if (packetStart == -1) { if (packetStart == -1) {
in.clear(); in.clear();
// Apply a more strict check in serverbound direction, we really shouldn't be seeing this many 0x00s
// even from the server, the only reason we even allow these is due to bugged servers
if (direction == ProtocolUtils.Direction.SERVERBOUND && wlen > 16) {
throw INVALID_PREAMBLE;
}
return; return;
} }
in.readerIndex(packetStart); in.readerIndex(packetStart);
// try to read the length of the packet // try to read the length of the packet
in.markReaderIndex(); try {
int length = readRawVarInt21(in); int length = readRawVarInt21(in);
if (packetStart == in.readerIndex()) { if (packetStart == in.readerIndex()) {
return; return;
} }
if (length < 0) { if (length < 0) {
throw BAD_PACKET_LENGTH; throw BAD_PACKET_LENGTH;
} }
if (length > 0) { if (length > 0) {
if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) { if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) {
if (validateServerboundHandshakePacket(in, length)) { if (validateServerboundHandshakePacket(in, length)) {
return; in.readerIndex(packetStart);
return;
}
} }
} }
}
// note that zero-length packets are ignored // note that zero-length packets are ignored
if (length > 0) { if (length > 0) {
if (in.readableBytes() < length) { if (in.readableBytes() < length) {
in.resetReaderIndex(); in.readerIndex(packetStart);
} else { } else {
out.add(in.readRetainedSlice(length)); // If enabled, rate-limit serverbound payload bytes based on frame length
if (packetLimiter != null) {
if (!packetLimiter.account(length)) {
throw new QuietDecoderException(
"Rate limit exceeded while processing packets for %s".formatted(
ctx.channel().remoteAddress()));
}
}
out.add(in.readRetainedSlice(length));
}
} }
} catch (Exception e) {
// Reset buffer to consistent state before propagating exception to prevent memory leaks
in.readerIndex(packetStart);
throw e;
} }
} }
@@ -117,7 +143,6 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
final int packetId = readRawVarInt21(in); final int packetId = readRawVarInt21(in);
// Index hasn't changed, we've read nothing // Index hasn't changed, we've read nothing
if (index == in.readerIndex()) { if (index == in.readerIndex()) {
in.resetReaderIndex();
return true; return true;
} }
final int payloadLength = length - ProtocolUtils.varIntBytes(packetId); final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
@@ -245,4 +270,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
public void setState(StateRegistry stateRegistry) { public void setState(StateRegistry stateRegistry) {
this.state = stateRegistry; this.state = stateRegistry;
} }
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
this.packetLimiter = packetLimiter;
}
} }
@@ -21,6 +21,9 @@ import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCountUtil;
@@ -41,8 +44,13 @@ import org.jetbrains.annotations.NotNull;
*/ */
public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler { public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
private static final int MAXIMUM_SIZE = Integer.getInteger("velocity.maximum-play-queue-size", 128 * 1024 * 1024); // 128MiB by default
private static final QuietDecoderException QUEUE_LIMIT_FAILED = new QuietDecoderException(
"Queue too big (greater than " + MAXIMUM_SIZE + " bytes)");
private final StateRegistry.PacketRegistry.ProtocolRegistry registry; private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
private final Queue<Object> queue = new ArrayDeque<>(); private final Queue<Object> queue = new ArrayDeque<>();
private int queueSize = 0;
/** /**
* Provides registries for client &amp; server bound packets. * Provides registries for client &amp; server bound packets.
@@ -64,6 +72,20 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
} }
} }
int length = 0;
if (msg instanceof ByteBuf) {
// keep track of raw packets
length = ((ByteBuf) msg).readableBytes();
} else if (msg instanceof ByteBufHolder) {
// keep track of bytebufs wrapped inside packets
length = ((ByteBufHolder) msg).content().readableBytes();
}
if (this.queueSize + length > MAXIMUM_SIZE) {
ReferenceCountUtil.release(msg);
throw QUEUE_LIMIT_FAILED;
}
this.queueSize += length;
// Otherwise, queue the packet // Otherwise, queue the packet
this.queue.offer(msg); this.queue.offer(msg);
} }
@@ -90,5 +112,6 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
ReferenceCountUtil.release(msg); ReferenceCountUtil.release(msg);
} }
} }
this.queueSize = 0;
} }
} }
@@ -25,7 +25,6 @@ import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.Suggestions;
import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.suggestion.SuggestionsBuilder;
@@ -45,9 +44,9 @@ import io.netty.buffer.ByteBuf;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap; import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMap;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque; import java.util.Deque;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Queue; import java.util.Queue;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate; import java.util.function.Predicate;
@@ -86,14 +85,14 @@ public class AvailableCommandsPacket implements MinecraftPacket {
@Override @Override
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
int commands = ProtocolUtils.readVarInt(buf); int commands = ProtocolUtils.readVarInt(buf);
WireNode[] wireNodes = new WireNode[commands]; List<WireNode> wireNodes = ProtocolUtils.newList(commands);
for (int i = 0; i < commands; i++) { for (int i = 0; i < commands; i++) {
wireNodes[i] = deserializeNode(buf, i, protocolVersion); wireNodes.add(deserializeNode(buf, i, protocolVersion));
} }
// Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles // Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles
// that exist. // that exist.
Queue<WireNode> nodeQueue = new ArrayDeque<>(Arrays.asList(wireNodes)); Queue<WireNode> nodeQueue = new ArrayDeque<>(wireNodes);
while (!nodeQueue.isEmpty()) { while (!nodeQueue.isEmpty()) {
boolean cycling = false; boolean cycling = false;
@@ -112,7 +111,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
} }
int rootIdx = ProtocolUtils.readVarInt(buf); int rootIdx = ProtocolUtils.readVarInt(buf);
rootNode = (RootCommandNode<CommandSource>) wireNodes[rootIdx].built; rootNode = (RootCommandNode<CommandSource>) wireNodes.get(rootIdx).built;
} }
@Override @Override
@@ -246,17 +245,17 @@ public class AvailableCommandsPacket implements MinecraftPacket {
this.validated = false; this.validated = false;
} }
void validate(WireNode[] wireNodes) { void validate(List<WireNode> wireNodes) {
// Ensure all children exist. Note that we delay checking if the node has been built yet; // Ensure all children exist. Note that we delay checking if the node has been built yet;
// that needs to come after this node is built. // that needs to come after this node is built.
for (int child : children) { for (int child : children) {
if (child < 0 || child >= wireNodes.length) { if (child < 0 || child >= wireNodes.size()) {
throw new IllegalStateException("Node points to non-existent index " + child); throw new IllegalStateException("Node points to non-existent index " + child);
} }
} }
if (redirectTo != -1) { if (redirectTo != -1) {
if (redirectTo < 0 || redirectTo >= wireNodes.length) { if (redirectTo < 0 || redirectTo >= wireNodes.size()) {
throw new IllegalStateException("Redirect node points to non-existent index " throw new IllegalStateException("Redirect node points to non-existent index "
+ redirectTo); + redirectTo);
} }
@@ -265,7 +264,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
this.validated = true; this.validated = true;
} }
boolean toNode(WireNode[] wireNodes) { boolean toNode(List<WireNode> wireNodes) {
if (!this.validated) { if (!this.validated) {
this.validate(wireNodes); this.validate(wireNodes);
} }
@@ -281,7 +280,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
// Add any redirects // Add any redirects
if (redirectTo != -1) { if (redirectTo != -1) {
WireNode redirect = wireNodes[redirectTo]; WireNode redirect = wireNodes.get(redirectTo);
if (redirect.built != null) { if (redirect.built != null) {
args.redirect(redirect.built); args.redirect(redirect.built);
} else { } else {
@@ -305,7 +304,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
} }
for (int child : children) { for (int child : children) {
if (wireNodes[child].built == null) { if (wireNodes.get(child).built == null) {
// The child is not yet deserialized. The node can't be built now. // The child is not yet deserialized. The node can't be built now.
return false; return false;
} }
@@ -313,7 +312,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
// Associate children with nodes // Associate children with nodes
for (int child : children) { for (int child : children) {
CommandNode<CommandSource> childNode = wireNodes[child].built; CommandNode<CommandSource> childNode = wireNodes.get(child).built;
if (!(childNode instanceof RootCommandNode)) { if (!(childNode instanceof RootCommandNode)) {
built.addChild(childNode); built.addChild(childNode);
} }
@@ -331,12 +330,10 @@ public class AvailableCommandsPacket implements MinecraftPacket {
.add("redirectTo", redirectTo); .add("redirectTo", redirectTo);
if (args != null) { if (args != null) {
if (args instanceof LiteralArgumentBuilder) { if (args instanceof LiteralArgumentBuilder literal) {
helper.add("argsLabel", helper.add("argsLabel", literal.getLiteral());
((LiteralArgumentBuilder<CommandSource>) args).getLiteral()); } else if (args instanceof RequiredArgumentBuilder required) {
} else if (args instanceof RequiredArgumentBuilder) { helper.add("argsName", required.getName());
helper.add("argsName",
((RequiredArgumentBuilder<CommandSource, ?>) args).getName());
} }
} }
@@ -348,17 +345,11 @@ public class AvailableCommandsPacket implements MinecraftPacket {
* A placeholder {@link SuggestionProvider} used internally to preserve the suggestion provider * A placeholder {@link SuggestionProvider} used internally to preserve the suggestion provider
* name. * name.
*/ */
public static class ProtocolSuggestionProvider implements SuggestionProvider<CommandSource> { public record ProtocolSuggestionProvider(String name) implements SuggestionProvider<CommandSource> {
private final String name;
public ProtocolSuggestionProvider(String name) {
this.name = name;
}
@Override @Override
public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSource> context, public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSource> context,
SuggestionsBuilder builder) throws CommandSyntaxException { SuggestionsBuilder builder) {
return builder.buildFuture(); return builder.buildFuture();
} }
} }
@@ -207,30 +207,22 @@ public class BossBarPacket implements MinecraftPacket {
this.uuid = ProtocolUtils.readUuid(buf); this.uuid = ProtocolUtils.readUuid(buf);
this.action = ProtocolUtils.readVarInt(buf); this.action = ProtocolUtils.readVarInt(buf);
switch (action) { switch (action) {
case ADD: case ADD -> {
this.name = ComponentHolder.read(buf, version); this.name = ComponentHolder.read(buf, version);
this.percent = buf.readFloat(); this.percent = buf.readFloat();
this.color = ProtocolUtils.readVarInt(buf); this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf); this.overlay = ProtocolUtils.readVarInt(buf);
this.flags = buf.readUnsignedByte(); this.flags = buf.readUnsignedByte();
break; }
case REMOVE: case REMOVE -> {}
break; case UPDATE_PERCENT -> this.percent = buf.readFloat();
case UPDATE_PERCENT: case UPDATE_NAME -> this.name = ComponentHolder.read(buf, version);
this.percent = buf.readFloat(); case UPDATE_STYLE -> {
break;
case UPDATE_NAME:
this.name = ComponentHolder.read(buf, version);
break;
case UPDATE_STYLE:
this.color = ProtocolUtils.readVarInt(buf); this.color = ProtocolUtils.readVarInt(buf);
this.overlay = ProtocolUtils.readVarInt(buf); this.overlay = ProtocolUtils.readVarInt(buf);
break; }
case UPDATE_PROPERTIES: case UPDATE_PROPERTIES -> this.flags = buf.readUnsignedByte();
this.flags = buf.readUnsignedByte(); default -> throw new UnsupportedOperationException("Unknown action " + action);
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
} }
} }
@@ -242,36 +234,30 @@ public class BossBarPacket implements MinecraftPacket {
ProtocolUtils.writeUuid(buf, uuid); ProtocolUtils.writeUuid(buf, uuid);
ProtocolUtils.writeVarInt(buf, action); ProtocolUtils.writeVarInt(buf, action);
switch (action) { switch (action) {
case ADD: case ADD -> {
if (name == null) { if (name == null) {
throw new IllegalStateException("No name specified!"); throw new IllegalStateException("No name specified!");
} }
name.write(buf); name.write(buf);
buf.writeFloat(percent); buf.writeFloat(percent);
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags);
}
case REMOVE -> {}
case UPDATE_PERCENT -> buf.writeFloat(percent);
case UPDATE_NAME -> {
if (name == null) {
throw new IllegalStateException("No name specified!");
}
name.write(buf);
}
case UPDATE_STYLE -> {
ProtocolUtils.writeVarInt(buf, color); ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay); ProtocolUtils.writeVarInt(buf, overlay);
buf.writeByte(flags); }
break; case UPDATE_PROPERTIES -> buf.writeByte(flags);
case REMOVE: default -> throw new UnsupportedOperationException("Unknown action " + action);
break;
case UPDATE_PERCENT:
buf.writeFloat(percent);
break;
case UPDATE_NAME:
if (name == null) {
throw new IllegalStateException("No name specified!");
}
name.write(buf);
break;
case UPDATE_STYLE:
ProtocolUtils.writeVarInt(buf, color);
ProtocolUtils.writeVarInt(buf, overlay);
break;
case UPDATE_PROPERTIES:
buf.writeByte(flags);
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
} }
} }
@@ -22,8 +22,8 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import java.util.Objects; import java.util.Objects;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
public class ClientSettingsPacket implements MinecraftPacket { public class ClientSettingsPacket implements MinecraftPacket {
@@ -135,7 +135,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance + return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance +
", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" + ", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" +
skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + textFilteringEnabled + skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + textFilteringEnabled +
", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}'; ", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}';
} }
@Override @Override
@@ -206,6 +206,16 @@ public class ClientSettingsPacket implements MinecraftPacket {
return handler.handle(this); return handler.handle(this);
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 1 + ByteBufUtil.utf8MaxBytes(16) + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return 1 + 0 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
}
@Override @Override
public boolean equals(@Nullable final Object o) { public boolean equals(@Nullable final Object o) {
if (this == o) { if (this == o) {
@@ -237,7 +247,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
difficulty, difficulty,
skinParts, skinParts,
mainHand, mainHand,
textFilteringEnabled, textFilteringEnabled,
clientListingAllowed, clientListingAllowed,
particleStatus); particleStatus);
} }
@@ -64,6 +64,28 @@ public class KeepAlivePacket implements MinecraftPacket {
} }
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_2)) {
return Long.BYTES;
} else if (version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
return 5;
} else {
return Integer.BYTES;
}
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_2)) {
return Long.BYTES;
} else if (version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
return 1;
} else {
return Integer.BYTES;
}
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -69,33 +69,25 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
Item item = new Item(ProtocolUtils.readUuid(buf)); Item item = new Item(ProtocolUtils.readUuid(buf));
items.add(item); items.add(item);
switch (action) { switch (action) {
case ADD_PLAYER: case ADD_PLAYER -> {
item.setName(ProtocolUtils.readString(buf)); item.setName(ProtocolUtils.readString(buf));
item.setProperties(ProtocolUtils.readProperties(buf)); item.setProperties(ProtocolUtils.readProperties(buf));
item.setGameMode(ProtocolUtils.readVarInt(buf)); item.setGameMode(ProtocolUtils.readVarInt(buf));
item.setLatency(ProtocolUtils.readVarInt(buf)); item.setLatency(ProtocolUtils.readVarInt(buf));
item.setDisplayName(readOptionalComponent(buf, version)); item.setDisplayName(readOptionalComponent(buf, version));
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
if (buf.readBoolean()) { if (buf.readBoolean()) {
item.setPlayerKey(ProtocolUtils.readPlayerKey(version, buf)); item.setPlayerKey(ProtocolUtils.readPlayerKey(version, buf));
} }
} }
break; }
case UPDATE_GAMEMODE: case UPDATE_GAMEMODE -> item.setGameMode(ProtocolUtils.readVarInt(buf));
item.setGameMode(ProtocolUtils.readVarInt(buf)); case UPDATE_LATENCY -> item.setLatency(ProtocolUtils.readVarInt(buf));
break; case UPDATE_DISPLAY_NAME -> item.setDisplayName(readOptionalComponent(buf, version));
case UPDATE_LATENCY: case REMOVE_PLAYER -> {
item.setLatency(ProtocolUtils.readVarInt(buf)); //Do nothing, all that is needed is the uuid
break; }
case UPDATE_DISPLAY_NAME: default -> throw new UnsupportedOperationException("Unknown action " + action);
item.setDisplayName(readOptionalComponent(buf, version));
break;
case REMOVE_PLAYER:
//Do nothing, all that is needed is the uuid
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
} }
} }
} else { } else {
@@ -126,39 +118,32 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
ProtocolUtils.writeUuid(buf, uuid); ProtocolUtils.writeUuid(buf, uuid);
switch (action) { switch (action) {
case ADD_PLAYER: case ADD_PLAYER -> {
ProtocolUtils.writeString(buf, item.getName()); ProtocolUtils.writeString(buf, item.getName());
ProtocolUtils.writeProperties(buf, item.getProperties()); ProtocolUtils.writeProperties(buf, item.getProperties());
ProtocolUtils.writeVarInt(buf, item.getGameMode()); ProtocolUtils.writeVarInt(buf, item.getGameMode());
ProtocolUtils.writeVarInt(buf, item.getLatency()); ProtocolUtils.writeVarInt(buf, item.getLatency());
writeDisplayName(buf, item.getDisplayName(), version); writeDisplayName(buf, item.getDisplayName(), version);
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
if (item.getPlayerKey() != null) { if (item.getPlayerKey() != null) {
buf.writeBoolean(true); buf.writeBoolean(true);
ProtocolUtils.writePlayerKey(buf, item.getPlayerKey()); ProtocolUtils.writePlayerKey(buf, item.getPlayerKey());
} else { } else {
buf.writeBoolean(false); buf.writeBoolean(false);
} }
} }
break; }
case UPDATE_GAMEMODE: case UPDATE_GAMEMODE -> ProtocolUtils.writeVarInt(buf, item.getGameMode());
ProtocolUtils.writeVarInt(buf, item.getGameMode()); case UPDATE_LATENCY -> ProtocolUtils.writeVarInt(buf, item.getLatency());
break; case UPDATE_DISPLAY_NAME -> writeDisplayName(buf, item.getDisplayName(), version);
case UPDATE_LATENCY: case REMOVE_PLAYER -> {
ProtocolUtils.writeVarInt(buf, item.getLatency());
break;
case UPDATE_DISPLAY_NAME:
writeDisplayName(buf, item.getDisplayName(), version);
break;
case REMOVE_PLAYER:
// Do nothing, all that is needed is the uuid // Do nothing, all that is needed is the uuid
break; }
default: default -> throw new UnsupportedOperationException("Unknown action " + action);
throw new UnsupportedOperationException("Unknown action " + action);
} }
} }
} else { } else {
Item item = items.get(0); Item item = items.getFirst();
Component displayNameComponent = item.getDisplayName(); Component displayNameComponent = item.getDisplayName();
if (displayNameComponent != null) { if (displayNameComponent != null) {
String displayName = LegacyComponentSerializer.legacySection() String displayName = LegacyComponentSerializer.legacySection()
@@ -269,7 +254,7 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
return this; return this;
} }
public IdentifiedKey getPlayerKey() { public @Nullable IdentifiedKey getPlayerKey() {
return playerKey; return playerKey;
} }
} }
@@ -42,6 +42,16 @@ public class PingIdentifyPacket implements MinecraftPacket {
buf.writeInt(id); buf.writeInt(id);
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return Integer.BYTES;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
return Integer.BYTES;
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -31,6 +31,9 @@ import org.checkerframework.checker.nullness.qual.Nullable;
public class PluginMessagePacket extends DeferredByteBufHolder implements MinecraftPacket { public class PluginMessagePacket extends DeferredByteBufHolder implements MinecraftPacket {
private static final int MAX_PAYLOAD_SIZE_CLIENTBOUND = getPayloadLimit(Direction.CLIENTBOUND);
private static final int MAX_PAYLOAD_SIZE_SERVERBOUND = getPayloadLimit(Direction.SERVERBOUND);
private @Nullable String channel; private @Nullable String channel;
public PluginMessagePacket() { public PluginMessagePacket() {
@@ -50,6 +53,19 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
return channel; return channel;
} }
private static int getPayloadLimit(Direction direction) {
if (System.getProperty("velocity.max-plugin-message-payload-size") != null) {
return Integer.getInteger("velocity.max-plugin-message-payload-size");
}
if (direction == Direction.SERVERBOUND) {
return Integer.getInteger("velocity.max-plugin-message-payload-size.serverbound", 32767);
} else {
// This is the vanilla expected limit, a payload this large feels like a nightmare given the trust
// we give to servers...
return Integer.getInteger("velocity.max-plugin-message-payload-size.clientbound", 1048576);
}
}
public void setChannel(String channel) { public void setChannel(String channel) {
this.channel = channel; this.channel = channel;
} }
@@ -100,6 +116,17 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES +
(direction == Direction.CLIENTBOUND ? MAX_PAYLOAD_SIZE_CLIENTBOUND : MAX_PAYLOAD_SIZE_SERVERBOUND);
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 1 + 0 + 0;
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -17,7 +17,6 @@
package com.velocitypowered.proxy.protocol.packet; package com.velocitypowered.proxy.protocol.packet;
import com.google.common.collect.Lists;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
@@ -51,7 +50,7 @@ public class RemovePlayerInfoPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) { ProtocolVersion protocolVersion) {
int length = ProtocolUtils.readVarInt(buf); int length = ProtocolUtils.readVarInt(buf);
Collection<UUID> profilesToRemove = Lists.newArrayListWithCapacity(length); Collection<UUID> profilesToRemove = ProtocolUtils.newList(length);
for (int idx = 0; idx < length; idx++) { for (int idx = 0; idx < length; idx++) {
profilesToRemove.add(ProtocolUtils.readUuid(buf)); profilesToRemove.add(ProtocolUtils.readUuid(buf));
} }
@@ -80,6 +80,26 @@ public class ResourcePackResponsePacket implements MinecraftPacket {
ProtocolUtils.writeVarInt(buf, status.ordinal()); ProtocolUtils.writeVarInt(buf, status.ordinal());
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
return Long.BYTES * 2 + 1;
} else if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_9_4)) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + 1;
}
return 1;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
return Long.BYTES * 2 + 1;
} else if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_9_4)) {
return 1 + 0 + 1;
}
return 1;
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -93,4 +113,4 @@ public class ResourcePackResponsePacket implements MinecraftPacket {
", status=" + status + ", status=" + status +
'}'; '}';
} }
} }
@@ -65,6 +65,16 @@ public class ServerboundCookieResponsePacket implements MinecraftPacket {
} }
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + 1 + 2 + 5120;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 1 + 0 + 0;
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -27,6 +27,8 @@ import io.netty.buffer.ByteBuf;
public class ServerboundCustomClickActionPacket extends DeferredByteBufHolder implements MinecraftPacket { public class ServerboundCustomClickActionPacket extends DeferredByteBufHolder implements MinecraftPacket {
private static final int MAX_TAG_SIZE = 65536;
public ServerboundCustomClickActionPacket() { public ServerboundCustomClickActionPacket() {
super(null); super(null);
} }
@@ -41,6 +43,16 @@ public class ServerboundCustomClickActionPacket extends DeferredByteBufHolder im
buf.writeBytes(content()); buf.writeBytes(content());
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + ProtocolUtils.varIntBytes(MAX_TAG_SIZE) + MAX_TAG_SIZE;
}
@Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 1 + 0 + 1 + 0;
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -37,32 +37,21 @@ class StringArgumentPropertySerializer implements ArgumentPropertySerializer<Str
@Override @Override
public StringArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) { public StringArgumentType deserialize(ByteBuf buf, ProtocolVersion protocolVersion) {
int type = ProtocolUtils.readVarInt(buf); int type = ProtocolUtils.readVarInt(buf);
switch (type) { return switch (type) {
case 0: case 0 -> StringArgumentType.word();
return StringArgumentType.word(); case 1 -> StringArgumentType.string();
case 1: case 2 -> StringArgumentType.greedyString();
return StringArgumentType.string(); default -> throw new IllegalArgumentException("Invalid string argument type " + type);
case 2: };
return StringArgumentType.greedyString();
default:
throw new IllegalArgumentException("Invalid string argument type " + type);
}
} }
@Override @Override
public void serialize(StringArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) { public void serialize(StringArgumentType object, ByteBuf buf, ProtocolVersion protocolVersion) {
switch (object.getType()) { switch (object.getType()) {
case SINGLE_WORD: case SINGLE_WORD -> ProtocolUtils.writeVarInt(buf, 0);
ProtocolUtils.writeVarInt(buf, 0); case QUOTABLE_PHRASE -> ProtocolUtils.writeVarInt(buf, 1);
break; case GREEDY_PHRASE -> ProtocolUtils.writeVarInt(buf, 2);
case QUOTABLE_PHRASE: default -> throw new IllegalArgumentException("Invalid string argument type " + object.getType());
ProtocolUtils.writeVarInt(buf, 1);
break;
case GREEDY_PHRASE:
ProtocolUtils.writeVarInt(buf, 2);
break;
default:
throw new IllegalArgumentException("Invalid string argument type " + object.getType());
} }
} }
} }
@@ -113,23 +113,18 @@ public class ComponentHolder {
public static BinaryTag serialize(JsonElement json) { public static BinaryTag serialize(JsonElement json) {
if (json instanceof JsonPrimitive jsonPrimitive) { if (json instanceof JsonPrimitive jsonPrimitive) {
if (jsonPrimitive.isNumber()) { if (jsonPrimitive.isNumber()) {
Number number = json.getAsNumber(); final Number number = json.getAsNumber();
if (number instanceof Byte) { return switch (number) {
return ByteBinaryTag.byteBinaryTag((Byte) number); case Byte b -> ByteBinaryTag.byteBinaryTag(b);
} else if (number instanceof Short) { case Short s -> ShortBinaryTag.shortBinaryTag(s);
return ShortBinaryTag.shortBinaryTag((Short) number); case Integer i -> IntBinaryTag.intBinaryTag(i);
} else if (number instanceof Integer) { case Long l -> LongBinaryTag.longBinaryTag(l);
return IntBinaryTag.intBinaryTag((Integer) number); case Float f -> FloatBinaryTag.floatBinaryTag(f);
} else if (number instanceof Long) { case Double d -> DoubleBinaryTag.doubleBinaryTag(d);
return LongBinaryTag.longBinaryTag((Long) number); case LazilyParsedNumber l -> IntBinaryTag.intBinaryTag(l.intValue());
} else if (number instanceof Float) { default -> throw new IllegalArgumentException("Unknown number type: " + number);
return FloatBinaryTag.floatBinaryTag((Float) number); };
} else if (number instanceof Double) {
return DoubleBinaryTag.doubleBinaryTag((Double) number);
} else if (number instanceof LazilyParsedNumber) {
return IntBinaryTag.intBinaryTag(number.intValue());
}
} else if (jsonPrimitive.isString()) { } else if (jsonPrimitive.isString()) {
return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString()); return StringBinaryTag.stringBinaryTag(jsonPrimitive.getAsString());
} else if (jsonPrimitive.isBoolean()) { } else if (jsonPrimitive.isBoolean()) {
@@ -137,16 +132,16 @@ public class ComponentHolder {
} else { } else {
throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive); throw new IllegalArgumentException("Unknown JSON primitive: " + jsonPrimitive);
} }
} else if (json instanceof JsonObject) { } else if (json instanceof JsonObject object) {
CompoundBinaryTag.Builder compound = CompoundBinaryTag.builder(); CompoundBinaryTag.Builder compound = CompoundBinaryTag.builder();
for (Map.Entry<String, JsonElement> property : ((JsonObject) json).entrySet()) { for (Map.Entry<String, JsonElement> property : object.entrySet()) {
compound.put(property.getKey(), serialize(property.getValue())); compound.put(property.getKey(), serialize(property.getValue()));
} }
return compound.build(); return compound.build();
} else if (json instanceof JsonArray) { } else if (json instanceof JsonArray array) {
List<JsonElement> jsonArray = ((JsonArray) json).asList(); List<JsonElement> jsonArray = array.asList();
if (jsonArray.isEmpty()) { if (jsonArray.isEmpty()) {
return ListBinaryTag.empty(); return ListBinaryTag.empty();
@@ -206,20 +201,21 @@ public class ComponentHolder {
} }
public static JsonElement deserialize(BinaryTag tag) { public static JsonElement deserialize(BinaryTag tag) {
switch (tag.type().id()) { return switch (tag.type().id()) {
case 1://BinaryTagTypes.BYTE: //BinaryTagTypes.BYTE
return new JsonPrimitive(((ByteBinaryTag) tag).value()); case 1 -> new JsonPrimitive(((ByteBinaryTag) tag).value());
case 2://BinaryTagTypes.SHORT: //BinaryTagTypes.SHORT
return new JsonPrimitive(((ShortBinaryTag) tag).value()); case 2 -> new JsonPrimitive(((ShortBinaryTag) tag).value());
case 3://BinaryTagTypes.INT: //BinaryTagTypes.INT:
return new JsonPrimitive(((IntBinaryTag) tag).value()); case 3 -> new JsonPrimitive(((IntBinaryTag) tag).value());
case 4://BinaryTagTypes.LONG: //BinaryTagTypes.LONG:
return new JsonPrimitive(((LongBinaryTag) tag).value()); case 4 -> new JsonPrimitive(((LongBinaryTag) tag).value());
case 5://BinaryTagTypes.FLOAT: //BinaryTagTypes.FLOAT:
return new JsonPrimitive(((FloatBinaryTag) tag).value()); case 5 -> new JsonPrimitive(((FloatBinaryTag) tag).value());
case 6://BinaryTagTypes.DOUBLE: //BinaryTagTypes.DOUBLE:
return new JsonPrimitive(((DoubleBinaryTag) tag).value()); case 6 -> new JsonPrimitive(((DoubleBinaryTag) tag).value());
case 7://BinaryTagTypes.BYTE_ARRAY: //BinaryTagTypes.BYTE_ARRAY:
case 7 -> {
byte[] byteArray = ((ByteArrayBinaryTag) tag).value(); byte[] byteArray = ((ByteArrayBinaryTag) tag).value();
JsonArray jsonByteArray = new JsonArray(byteArray.length); JsonArray jsonByteArray = new JsonArray(byteArray.length);
@@ -227,10 +223,12 @@ public class ComponentHolder {
jsonByteArray.add(new JsonPrimitive(b)); jsonByteArray.add(new JsonPrimitive(b));
} }
return jsonByteArray; yield jsonByteArray;
case 8://BinaryTagTypes.STRING: }
return new JsonPrimitive(((StringBinaryTag) tag).value()); //BinaryTagTypes.STRING:
case 9://BinaryTagTypes.LIST: case 8 -> new JsonPrimitive(((StringBinaryTag) tag).value());
//BinaryTagTypes.LIST:
case 9 -> {
ListBinaryTag items = (ListBinaryTag) tag; ListBinaryTag items = (ListBinaryTag) tag;
JsonArray jsonList = new JsonArray(items.size()); JsonArray jsonList = new JsonArray(items.size());
@@ -238,8 +236,10 @@ public class ComponentHolder {
jsonList.add(deserialize(subTag)); jsonList.add(deserialize(subTag));
} }
return jsonList; yield jsonList;
case 10://BinaryTagTypes.COMPOUND: }
//BinaryTagTypes.COMPOUND:
case 10 -> {
CompoundBinaryTag compound = (CompoundBinaryTag) tag; CompoundBinaryTag compound = (CompoundBinaryTag) tag;
JsonObject jsonObject = new JsonObject(); JsonObject jsonObject = new JsonObject();
@@ -252,8 +252,10 @@ public class ComponentHolder {
jsonObject.add(key.isEmpty() ? "text" : key, deserialize(compound.get(key))); jsonObject.add(key.isEmpty() ? "text" : key, deserialize(compound.get(key)));
}); });
return jsonObject; yield jsonObject;
case 11://BinaryTagTypes.INT_ARRAY: }
//BinaryTagTypes.INT_ARRAY:
case 11 -> {
int[] intArray = ((IntArrayBinaryTag) tag).value(); int[] intArray = ((IntArrayBinaryTag) tag).value();
JsonArray jsonIntArray = new JsonArray(intArray.length); JsonArray jsonIntArray = new JsonArray(intArray.length);
@@ -261,8 +263,10 @@ public class ComponentHolder {
jsonIntArray.add(new JsonPrimitive(i)); jsonIntArray.add(new JsonPrimitive(i));
} }
return jsonIntArray; yield jsonIntArray;
case 12://BinaryTagTypes.LONG_ARRAY: }
//BinaryTagTypes.LONG_ARRAY:
case 12 -> {
long[] longArray = ((LongArrayBinaryTag) tag).value(); long[] longArray = ((LongArrayBinaryTag) tag).value();
JsonArray jsonLongArray = new JsonArray(longArray.length); JsonArray jsonLongArray = new JsonArray(longArray.length);
@@ -270,10 +274,10 @@ public class ComponentHolder {
jsonLongArray.add(new JsonPrimitive(l)); jsonLongArray.add(new JsonPrimitive(l));
} }
return jsonLongArray; yield jsonLongArray;
default: }
throw new IllegalArgumentException("Unknown NBT tag: " + tag); default -> throw new IllegalArgumentException("Unknown NBT tag: " + tag);
} };
} }
public static ComponentHolder read(ByteBuf buf, ProtocolVersion version) { public static ComponentHolder read(ByteBuf buf, ProtocolVersion version) {
@@ -59,14 +59,9 @@ public class SystemChatPacket implements MinecraftPacket {
component.write(buf); component.write(buf);
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19_1)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19_1)) {
switch (type) { switch (type) {
case SYSTEM: case SYSTEM -> buf.writeBoolean(false);
buf.writeBoolean(false); case GAME_INFO -> buf.writeBoolean(true);
break; default -> throw new IllegalArgumentException("Invalid chat type");
case GAME_INFO:
buf.writeBoolean(true);
break;
default:
throw new IllegalArgumentException("Invalid chat type");
} }
} else { } else {
ProtocolUtils.writeVarInt(buf, type.getId()); ProtocolUtils.writeVarInt(buf, type.getId());
@@ -32,12 +32,24 @@ public class LegacyChatPacket implements MinecraftPacket {
public static final byte GAME_INFO_TYPE = (byte) 2; public static final byte GAME_INFO_TYPE = (byte) 2;
public static final int MAX_SERVERBOUND_MESSAGE_LENGTH = 256; public static final int MAX_SERVERBOUND_MESSAGE_LENGTH = 256;
private static final int MAX_SERVERBOUND_MESSAGE_LENGTH_LEGACY = getMaxServerboundMessageLength();
public static final UUID EMPTY_SENDER = new UUID(0, 0); public static final UUID EMPTY_SENDER = new UUID(0, 0);
private @Nullable String message; private @Nullable String message;
private byte type; private byte type;
private @Nullable UUID sender; private @Nullable UUID sender;
private static int getMaxServerboundMessageLength() {
final String value = System.getProperty("velocity.legacyChatMaxServerboundLength");
if (value != null) {
try {
return Integer.parseInt(value.trim());
} catch (final NumberFormatException e) {
}
}
return 100;
}
public LegacyChatPacket() { public LegacyChatPacket() {
} }
@@ -92,7 +104,10 @@ public class LegacyChatPacket implements MinecraftPacket {
@Override @Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
message = ProtocolUtils.readString(buf, direction == ProtocolUtils.Direction.CLIENTBOUND message = ProtocolUtils.readString(buf, direction == ProtocolUtils.Direction.CLIENTBOUND
? 262144 : version.noLessThan(ProtocolVersion.MINECRAFT_1_11) ? 256 : 100); ? 262144
: version.noLessThan(ProtocolVersion.MINECRAFT_1_11)
? MAX_SERVERBOUND_MESSAGE_LENGTH
: MAX_SERVERBOUND_MESSAGE_LENGTH_LEGACY);
if (direction == ProtocolUtils.Direction.CLIENTBOUND if (direction == ProtocolUtils.Direction.CLIENTBOUND
&& version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) { && version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
type = buf.readByte(); type = buf.readByte();
@@ -22,7 +22,6 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class ClientboundCustomReportDetailsPacket implements MinecraftPacket { public class ClientboundCustomReportDetailsPacket implements MinecraftPacket {
@@ -40,7 +39,7 @@ public class ClientboundCustomReportDetailsPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int detailsCount = ProtocolUtils.readVarInt(buf); int detailsCount = ProtocolUtils.readVarInt(buf);
this.details = new HashMap<>(detailsCount); this.details = ProtocolUtils.newMap(detailsCount);
for (int i = 0; i < detailsCount; i++) { for (int i = 0; i < detailsCount; i++) {
details.put(ProtocolUtils.readString(buf), ProtocolUtils.readString(buf)); details.put(ProtocolUtils.readString(buf), ProtocolUtils.readString(buf));
} }
@@ -18,13 +18,11 @@
package com.velocitypowered.proxy.protocol.packet.config; package com.velocitypowered.proxy.protocol.packet.config;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.api.util.ServerLink;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import java.util.ArrayList;
import java.util.List; import java.util.List;
public class ClientboundServerLinksPacket implements MinecraftPacket { public class ClientboundServerLinksPacket implements MinecraftPacket {
@@ -42,7 +40,7 @@ public class ClientboundServerLinksPacket implements MinecraftPacket {
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
int linksCount = ProtocolUtils.readVarInt(buf); int linksCount = ProtocolUtils.readVarInt(buf);
this.serverLinks = new ArrayList<>(linksCount); this.serverLinks = ProtocolUtils.newList(linksCount);
for (int i = 0; i < linksCount; i++) { for (int i = 0; i < linksCount; i++) {
serverLinks.add(ServerLink.read(buf, version)); serverLinks.add(ServerLink.read(buf, version));
} }
@@ -38,6 +38,11 @@ public class CodeOfConductAcceptPacket implements MinecraftPacket {
public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) { public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
} }
@Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
return 0;
}
@Override @Override
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
@@ -23,6 +23,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.util.except.QuietDecoderException; import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import java.util.List;
public class KnownPacksPacket implements MinecraftPacket { public class KnownPacksPacket implements MinecraftPacket {
@@ -30,7 +31,7 @@ public class KnownPacksPacket implements MinecraftPacket {
private static final QuietDecoderException TOO_MANY_PACKS = private static final QuietDecoderException TOO_MANY_PACKS =
new QuietDecoderException("too many known packs"); new QuietDecoderException("too many known packs");
private KnownPack[] packs; private List<KnownPack> packs;
@Override @Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
@@ -40,10 +41,10 @@ public class KnownPacksPacket implements MinecraftPacket {
throw TOO_MANY_PACKS; throw TOO_MANY_PACKS;
} }
final KnownPack[] packs = new KnownPack[packCount]; final List<KnownPack> packs = ProtocolUtils.newList(packCount);
for (int i = 0; i < packCount; i++) { for (int i = 0; i < packCount; i++) {
packs[i] = KnownPack.read(buf); packs.add(KnownPack.read(buf));
} }
this.packs = packs; this.packs = packs;
@@ -52,7 +53,7 @@ public class KnownPacksPacket implements MinecraftPacket {
@Override @Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, public void encode(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion protocolVersion) { ProtocolVersion protocolVersion) {
ProtocolUtils.writeVarInt(buf, packs.length); ProtocolUtils.writeVarInt(buf, packs.size());
for (KnownPack pack : packs) { for (KnownPack pack : packs) {
pack.write(buf); pack.write(buf);
@@ -105,26 +105,14 @@ public abstract class GenericTitlePacket implements MinecraftPacket {
public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) { public static GenericTitlePacket constructTitlePacket(ActionType type, ProtocolVersion version) {
GenericTitlePacket packet = null; GenericTitlePacket packet = null;
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) {
switch (type) { packet = switch (type) {
case SET_ACTION_BAR: case SET_ACTION_BAR -> new TitleActionbarPacket();
packet = new TitleActionbarPacket(); case SET_SUBTITLE -> new TitleSubtitlePacket();
break; case SET_TIMES -> new TitleTimesPacket();
case SET_SUBTITLE: case SET_TITLE -> new TitleTextPacket();
packet = new TitleSubtitlePacket(); case HIDE, RESET -> new TitleClearPacket();
break; default -> throw new IllegalArgumentException("Invalid ActionType");
case SET_TIMES: };
packet = new TitleTimesPacket();
break;
case SET_TITLE:
packet = new TitleTextPacket();
break;
case HIDE:
case RESET:
packet = new TitleClearPacket();
break;
default:
throw new IllegalArgumentException("Invalid ActionType");
}
} else { } else {
packet = new LegacyTitlePacket(); packet = new LegacyTitlePacket();
} }
@@ -40,24 +40,19 @@ public class LegacyTitlePacket extends GenericTitlePacket {
ProtocolUtils.writeVarInt(buf, getAction().getAction(version)); ProtocolUtils.writeVarInt(buf, getAction().getAction(version));
switch (getAction()) { switch (getAction()) {
case SET_TITLE: case SET_TITLE, SET_SUBTITLE, SET_ACTION_BAR -> {
case SET_SUBTITLE:
case SET_ACTION_BAR:
if (component == null) { if (component == null) {
throw new IllegalStateException("No component found for " + getAction()); throw new IllegalStateException("No component found for " + getAction());
} }
component.write(buf); component.write(buf);
break; }
case SET_TIMES: case SET_TIMES -> {
buf.writeInt(fadeIn); buf.writeInt(fadeIn);
buf.writeInt(stay); buf.writeInt(stay);
buf.writeInt(fadeOut); buf.writeInt(fadeOut);
break; }
case HIDE: case HIDE, RESET -> {}
case RESET: default -> throw new UnsupportedOperationException("Unknown action " + getAction());
break;
default:
throw new UnsupportedOperationException("Unknown action " + getAction());
} }
} }
@@ -230,23 +230,20 @@ public final class PluginMessageUtil {
} }
// Before falling into the fallback, explicitly rewrite certain messages. // Before falling into the fallback, explicitly rewrite certain messages.
switch (name) { return switch (name) {
case REGISTER_CHANNEL_LEGACY: case REGISTER_CHANNEL_LEGACY -> REGISTER_CHANNEL;
return REGISTER_CHANNEL; case UNREGISTER_CHANNEL_LEGACY -> UNREGISTER_CHANNEL;
case UNREGISTER_CHANNEL_LEGACY: case BRAND_CHANNEL_LEGACY -> BRAND_CHANNEL;
return UNREGISTER_CHANNEL; // This is a special historical case we are compelled to support for the benefit of
case BRAND_CHANNEL_LEGACY: // BungeeQuack.
return BRAND_CHANNEL; case "BungeeCord" -> "bungeecord:main";
case "BungeeCord": default -> {
// This is a special historical case we are compelled to support for the benefit of
// BungeeQuack.
return "bungeecord:main";
default:
// This is very likely a legacy name, so transform it. Velocity uses the same scheme as // This is very likely a legacy name, so transform it. Velocity uses the same scheme as
// BungeeCord does to transform channels, but also removes clearly invalid characters as // BungeeCord does to transform channels, but also removes clearly invalid characters as
// well. // well.
String lower = name.toLowerCase(Locale.ROOT); final String lower = name.toLowerCase(Locale.ROOT);
return "legacy:" + INVALID_IDENTIFIER_REGEX.matcher(lower).replaceAll(""); yield "legacy:" + INVALID_IDENTIFIER_REGEX.matcher(lower).replaceAll("");
} }
};
} }
} }
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2018-2026 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.scheduler;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* A {@link SchedulerBackend} backed by a real {@link ScheduledExecutorService}.
*/
public class ExecutorSchedulerBackend implements SchedulerBackend {
private final ScheduledExecutorService executor;
/**
* Creates a ExecutorSchedulerBackend with a default executor.
*/
public ExecutorSchedulerBackend() {
this(Executors.newSingleThreadScheduledExecutor(
new ThreadFactoryBuilder()
.setDaemon(true)
.setNameFormat("Velocity Task Scheduler Timer")
.build()
));
}
/**
* Creates a ExecutorSchedulerBackend with a given executor.
*
* @param executor The executor to use.
*/
public ExecutorSchedulerBackend(ScheduledExecutorService executor) {
this.executor = checkNotNull(executor, "executor");
}
@Override
public ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit) {
return executor.schedule(task, delay, unit);
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) {
return executor.scheduleAtFixedRate(task, initialDelay, period, unit);
}
@Override
public void shutdown() {
executor.shutdown();
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2018-2026 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.scheduler;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
* Backend interface used by {@link VelocityScheduler} to schedule timer callbacks.
*
* <p>This is an internal abstraction that allows tests to replace the real-time scheduler
* with a deterministic implementation.
*/
interface SchedulerBackend {
/**
* Schedules a task to run once after the given delay.
*
* @param task the task to run
* @param delay the delay
* @param unit the delay unit
* @return a future representing the scheduled task
*/
ScheduledFuture<?> schedule(Runnable task, long delay, TimeUnit unit);
/**
* Schedules a task to run at a fixed rate.
*
* @param task the task to run
* @param initialDelay the initial delay
* @param period the period between runs
* @param unit the time unit
* @return a future representing the scheduled task
*/
ScheduledFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit);
/**
* Shuts down the backend.
*/
void shutdown();
}
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018-2023 Velocity Contributors * Copyright (C) 2018-2026 Velocity Contributors
* *
* This program is free software: you can redistribute it and/or modify * 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 * it under the terms of the GNU General Public License as published by
@@ -23,7 +23,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps; import com.google.common.collect.Multimaps;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.scheduler.ScheduledTask; import com.velocitypowered.api.scheduler.ScheduledTask;
@@ -40,7 +39,6 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -60,20 +58,23 @@ import org.jetbrains.annotations.VisibleForTesting;
public class VelocityScheduler implements Scheduler { public class VelocityScheduler implements Scheduler {
private final PluginManager pluginManager; private final PluginManager pluginManager;
private final ScheduledExecutorService timerExecutionService; private final SchedulerBackend backend;
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedMultimap( private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedMultimap(
Multimaps.newSetMultimap(new IdentityHashMap<>(), HashSet::new)); Multimaps.newSetMultimap(new IdentityHashMap<>(), HashSet::new));
/** /**
* Initalizes the scheduler. * Initializes the scheduler.
* *
* @param pluginManager the Velocity plugin manager * @param pluginManager the Velocity plugin manager
*/ */
public VelocityScheduler(PluginManager pluginManager) { public VelocityScheduler(PluginManager pluginManager) {
this(pluginManager, new ExecutorSchedulerBackend());
}
@VisibleForTesting
VelocityScheduler(PluginManager pluginManager, SchedulerBackend backend) {
this.pluginManager = pluginManager; this.pluginManager = pluginManager;
this.timerExecutionService = Executors this.backend = backend;
.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true)
.setNameFormat("Velocity Task Scheduler Timer").build());
} }
@Override @Override
@@ -118,7 +119,7 @@ public class VelocityScheduler implements Scheduler {
for (ScheduledTask task : terminating) { for (ScheduledTask task : terminating) {
task.cancel(); task.cancel();
} }
timerExecutionService.shutdown(); backend.shutdown();
final List<PluginContainer> plugins = new ArrayList<>(this.pluginManager.getPlugins()); final List<PluginContainer> plugins = new ArrayList<>(this.pluginManager.getPlugins());
final Iterator<PluginContainer> pluginIterator = plugins.iterator(); final Iterator<PluginContainer> pluginIterator = plugins.iterator();
while (pluginIterator.hasNext()) { while (pluginIterator.hasNext()) {
@@ -232,10 +233,9 @@ public class VelocityScheduler implements Scheduler {
void schedule() { void schedule() {
if (repeat == 0) { if (repeat == 0) {
this.future = timerExecutionService.schedule(this, delay, TimeUnit.MILLISECONDS); this.future = backend.schedule(this, delay, TimeUnit.MILLISECONDS);
} else { } else {
this.future = timerExecutionService this.future = backend.scheduleAtFixedRate(this, delay, repeat, TimeUnit.MILLISECONDS);
.scheduleAtFixedRate(this, delay, repeat, TimeUnit.MILLISECONDS);
} }
} }
@@ -85,7 +85,7 @@ public class VelocityTabListLegacy extends KeyedVelocityTabList {
@Override @Override
public void processLegacy(LegacyPlayerListItemPacket packet) { public void processLegacy(LegacyPlayerListItemPacket packet) {
Item item = packet.getItems().get(0); // Only one item per packet in 1.7 Item item = packet.getItems().getFirst(); // Only one item per packet in 1.7
switch (packet.getAction()) { switch (packet.getAction()) {
case LegacyPlayerListItemPacket.ADD_PLAYER: case LegacyPlayerListItemPacket.ADD_PLAYER:
@@ -145,45 +145,44 @@ public enum InformationUtils {
* @return {@link String} address with public parts redacted * @return {@link String} address with public parts redacted
*/ */
public static String anonymizeInetAddress(InetAddress address) { public static String anonymizeInetAddress(InetAddress address) {
if (address instanceof Inet4Address) { return switch (address) {
Inet4Address v4 = (Inet4Address) address; case Inet4Address v4 -> {
if (v4.isAnyLocalAddress() || v4.isLoopbackAddress() if (v4.isAnyLocalAddress() || v4.isLoopbackAddress()
|| v4.isLinkLocalAddress() || v4.isLinkLocalAddress()
|| v4.isSiteLocalAddress()) { || v4.isSiteLocalAddress()) {
return address.getHostAddress(); yield address.getHostAddress();
} else { } else {
byte[] addr = v4.getAddress(); byte[] addr = v4.getAddress();
return (addr[0] & 0xff) + "." + (addr[1] & 0xff) + ".XXX.XXX"; yield (addr[0] & 0xff) + "." + (addr[1] & 0xff) + ".XXX.XXX";
} }
} else if (address instanceof Inet6Address) { } case Inet6Address v6 -> {
Inet6Address v6 = (Inet6Address) address; if (v6.isAnyLocalAddress() || v6.isLoopbackAddress()
if (v6.isAnyLocalAddress() || v6.isLoopbackAddress() || v6.isSiteLocalAddress()
|| v6.isSiteLocalAddress() || v6.isSiteLocalAddress()) {
|| v6.isSiteLocalAddress()) { yield address.getHostAddress();
return address.getHostAddress(); } else {
} else { String[] bits = v6.getHostAddress().split(":");
String[] bits = v6.getHostAddress().split(":"); String ret = "";
String ret = ""; boolean flag = false;
boolean flag = false; for (int iter = 0; iter < bits.length; iter++) {
for (int iter = 0; iter < bits.length; iter++) { if (flag) {
if (flag) { ret += ":X";
ret += ":X"; continue;
continue; }
} if (!bits[iter].equals("0")) {
if (!bits[iter].equals("0")) { if (iter == 0) {
if (iter == 0) { ret = bits[iter];
ret = bits[iter]; } else {
} else { ret = "::" + bits[iter];
ret = "::" + bits[iter]; }
} flag = true;
flag = true; }
} }
yield ret;
} }
return ret;
} }
} else { default -> address.getHostAddress();
return address.getHostAddress(); };
}
} }
/** /**
@@ -0,0 +1,252 @@
/*
* 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.util;
/**
* IntervalledCounter maintains a rolling sum of values associated with timestamps, keeping
* only those entries that fall within a fixed time interval from the most recent timestamp.
*
* <p>Time values must be provided in the same unit as {@link System#nanoTime()} (nanoseconds),
* and the configured interval is also expressed in nanoseconds. Callers are expected to
* periodically advance the counter to the current time using {@link #updateCurrentTime()} or
* {@link #updateCurrentTime(long)} to evict expired entries before adding new ones via
* {@link #addTime(long)} or {@link #addTime(long, long)}.</p>
*
* <p>This class is not thread-safe. If multiple threads access an instance concurrently,
* external synchronization is required.</p>
*/
@SuppressWarnings("checkstyle:WhitespaceAfter") // Not our class
public final class IntervalledCounter {
private static final int INITIAL_SIZE = 8;
/**
* Ring buffer holding the timestamp (in nanoseconds) for each data point.
*/
protected long[] times;
/**
* Ring buffer holding the count associated with each timestamp.
*/
protected long[] counts;
/**
* The sliding window size in nanoseconds. Only entries with time >= (currentTime - interval)
* are considered part of the window.
*/
protected final long interval;
/**
* Cached lower bound of the window (in nanoseconds) after the last update.
*/
protected long minTime;
/**
* Running sum of all counts currently within the window.
*/
protected long sum;
/**
* Head index (inclusive) of the ring buffer.
*/
protected int head; // inclusive
/**
* Tail index (exclusive) of the ring buffer.
*/
protected int tail; // exclusive
/**
* Creates a new counter with the specified interval.
*
* @param interval the window size in nanoseconds (compatible with {@link System#nanoTime()})
*/
public IntervalledCounter(final long interval) {
this.times = new long[INITIAL_SIZE];
this.counts = new long[INITIAL_SIZE];
this.interval = interval;
}
/**
* Advances the window to the current time using {@link System#nanoTime()}, evicting any
* data points that have fallen outside of the interval and updating the running sum.
*/
public void updateCurrentTime() {
this.updateCurrentTime(System.nanoTime());
}
/**
* Advances the window to the provided time, evicting any data points older than
* {@code currentTime - interval} and updating the running sum.
*
* @param currentTime the current time in nanoseconds (as from {@link System#nanoTime()})
*/
public void updateCurrentTime(final long currentTime) {
long sum = this.sum;
int head = this.head;
final int tail = this.tail;
final long minTime = currentTime - this.interval;
final int arrayLen = this.times.length;
// guard against overflow by using subtraction
while (head != tail && this.times[head] - minTime < 0) {
sum -= this.counts[head];
// there are two ways we can do this:
// 1. free the count when adding
// 2. free it now
// option #2
this.counts[head] = 0;
if (++head >= arrayLen) {
head = 0;
}
}
this.sum = sum;
this.head = head;
this.minTime = minTime;
}
/**
* Adds a single unit at the specified timestamp, assuming the timestamp is within the current
* window. If the timestamp is older than the current window lower bound, the value is ignored.
* This method does not automatically advance the window; callers should invoke
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
*
* @param currTime the timestamp in nanoseconds
*/
public void addTime(final long currTime) {
this.addTime(currTime, 1L);
}
/**
* Adds {@code count} units at the specified timestamp, assuming the timestamp is within the
* current window. If the timestamp is older than {@code minTime}, the value is ignored.
* This method does not automatically advance the window; callers should invoke
* {@link #updateCurrentTime()} or {@link #updateCurrentTime(long)} beforehand.
*
* @param currTime the timestamp in nanoseconds
* @param count the amount to add (non-negative)
*/
public void addTime(final long currTime, final long count) {
// guard against overflow by using subtraction
if (currTime - this.minTime < 0) {
return;
}
int nextTail = (this.tail + 1) % this.times.length;
if (nextTail == this.head) {
this.resize();
nextTail = (this.tail + 1) % this.times.length;
}
this.times[this.tail] = currTime;
this.counts[this.tail] += count;
this.sum += count;
this.tail = nextTail;
}
/**
* Convenience method that advances the window to the current time and then adds {@code count}
* units at that time.
*
* @param count the amount to add (non-negative)
*/
public void updateAndAdd(final long count) {
final long currTime = System.nanoTime();
this.updateCurrentTime(currTime);
this.addTime(currTime, count);
}
/**
* Convenience method that advances the window to {@code currTime} and then adds {@code count}
* units at that time.
*
* @param count the amount to add (non-negative)
* @param currTime the timestamp in nanoseconds
*/
public void updateAndAdd(final long count, final long currTime) {
this.updateCurrentTime(currTime);
this.addTime(currTime, count);
}
/**
* Doubles the capacity of the internal ring buffers, preserving the order of existing data.
*/
private void resize() {
final long[] oldElements = this.times;
final long[] oldCounts = this.counts;
final long[] newElements = new long[this.times.length * 2];
final long[] newCounts = new long[this.times.length * 2];
this.times = newElements;
this.counts = newCounts;
final int head = this.head;
final int tail = this.tail;
final int size = tail >= head ? (tail - head) : (tail + (oldElements.length - head));
this.head = 0;
this.tail = size;
if (tail >= head) {
// sequentially ordered from [head, tail)
System.arraycopy(oldElements, head, newElements, 0, size);
System.arraycopy(oldCounts, head, newCounts, 0, size);
} else {
// ordered from [head, length)
// then followed by [0, tail)
System.arraycopy(oldElements, head, newElements, 0, oldElements.length - head);
System.arraycopy(oldElements, 0, newElements, oldElements.length - head, tail);
System.arraycopy(oldCounts, head, newCounts, 0, oldCounts.length - head);
System.arraycopy(oldCounts, 0, newCounts, oldCounts.length - head, tail);
}
}
/**
* Returns the current rate in units per second based on the rolling sum and the configured
* interval. Specifically: {@code sum / (intervalSeconds)} where {@code intervalSeconds}
* equals {@code interval / 1e9}.
*
* @return the rate in units per second for the current window
*/
public double getRate() {
return (double)this.sum / ((double)this.interval * 1.0E-9);
}
/**
* Returns the configured interval size in nanoseconds.
*
* @return the interval size in nanoseconds
*/
public long getInterval() {
return this.interval;
}
/**
* Returns the rolling sum of all counts currently within the window.
*
* @return the rolling sum
*/
public long getSum() {
return this.sum;
}
/**
* Returns the number of data points currently stored in the internal ring buffer. This may be
* less than or equal to the number of points added since older entries may have been evicted.
*
* @return the number of stored data points
*/
public int totalDataPoints() {
return this.tail >= this.head ? (this.tail - this.head) : (this.tail + (this.counts.length - this.head));
}
}
@@ -43,6 +43,12 @@ public enum TranslatableMapper implements BiConsumer<TranslatableComponent, Cons
final Locale locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault()); final Locale locale = ClosestLocaleMatcher.INSTANCE.lookupClosest(Locale.getDefault());
if (GlobalTranslator.translator().canTranslate(translatableComponent.key(), locale)) { if (GlobalTranslator.translator().canTranslate(translatableComponent.key(), locale)) {
componentConsumer.accept(GlobalTranslator.render(translatableComponent, locale)); componentConsumer.accept(GlobalTranslator.render(translatableComponent, locale));
} else {
String fallback = translatableComponent.fallback();
if (fallback == null) {
fallback = translatableComponent.key();
}
componentConsumer.accept(Component.text(fallback));
} }
} }
} }
@@ -18,42 +18,42 @@
velocity.error.already-connected=You are already connected to this server! velocity.error.already-connected=You are already connected to this server!
velocity.error.already-connected-proxy=You are already connected to this proxy! velocity.error.already-connected-proxy=You are already connected to this proxy!
velocity.error.already-connecting=You are already trying to connect to a server! velocity.error.already-connecting=You are already trying to connect to a server!
velocity.error.cant-connect=Unable to connect to {0}: {1} velocity.error.cant-connect=Unable to connect to <arg:0>: <arg:1>
velocity.error.connecting-server-error=Unable to connect you to {0}. Please try again later. velocity.error.connecting-server-error=Unable to connect you to <arg:0>. Please try again later.
velocity.error.connected-server-error=Your connection to {0} encountered a problem. velocity.error.connected-server-error=Your connection to <arg:0> encountered a problem.
velocity.error.internal-server-connection-error=An internal server connection error occurred. velocity.error.internal-server-connection-error=An internal server connection error occurred.
velocity.error.logging-in-too-fast=You are logging in too fast, try again later. velocity.error.logging-in-too-fast=You are logging in too fast, try again later.
velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client. velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client.
velocity.error.player-connection-error=An internal error occurred in your connection. velocity.error.player-connection-error=An internal error occurred in your connection.
velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above. velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above.
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding. velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
velocity.error.moved-to-new-server=You were kicked from {0}: {1} velocity.error.moved-to-new-server=You were kicked from <arg:0>: <arg:1>
velocity.error.no-available-servers=There are no available servers to connect you to. Try again later or contact an admin. velocity.error.no-available-servers=There are no available servers to connect you to. Try again later or contact an admin.
velocity.error.illegal-chat-characters=Illegal characters in chat velocity.error.illegal-chat-characters=Illegal characters in chat
# Commands # Commands
velocity.command.generic-error=An error occurred while running this command. velocity.command.generic-error=An error occurred while running this command.
velocity.command.command-does-not-exist=This command does not exist. velocity.command.command-does-not-exist=This command does not exist.
velocity.command.players-only=Only players can run this command. velocity.command.players-only=Only players can run this command.
velocity.command.server-does-not-exist=The specified server {0} does not exist. velocity.command.server-does-not-exist=The specified server <arg:0> does not exist.
velocity.command.player-not-found=The specified player {0} does not exist. velocity.command.player-not-found=The specified player <arg:0> does not exist.
velocity.command.server-current-server=You are currently connected to {0}. velocity.command.server-current-server=You are currently connected to <arg:0>.
velocity.command.server-too-many=There are too many servers set up. Use tab completion to view all servers available. velocity.command.server-too-many=There are too many servers set up. Use tab completion to view all servers available.
velocity.command.server-available=Available servers: velocity.command.server-available=Available servers:
velocity.command.server-tooltip-player-online={0} player online velocity.command.server-tooltip-player-online=<arg:0> player online
velocity.command.server-tooltip-players-online={0} players online velocity.command.server-tooltip-players-online=<arg:0> players online
velocity.command.server-tooltip-current-server=Currently connected to this server velocity.command.server-tooltip-current-server=Currently connected to this server
velocity.command.server-tooltip-offer-connect-server=Click to connect to this server velocity.command.server-tooltip-offer-connect-server=Click to connect to this server
velocity.command.glist-player-singular={0} player is currently connected to the proxy. velocity.command.glist-player-singular=<arg:0> player is currently connected to the proxy.
velocity.command.glist-player-plural={0} players are currently connected to the proxy. velocity.command.glist-player-plural=<arg:0> players are currently connected to the proxy.
velocity.command.glist-view-all=To view all players on servers, use /glist all. velocity.command.glist-view-all=To view all players on servers, use /glist all.
velocity.command.reload-success=Velocity configuration successfully reloaded. velocity.command.reload-success=Velocity configuration successfully reloaded.
velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details. velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details.
velocity.command.version-copyright=Copyright 2018-{2} {0}. {1} is licensed under the terms of the GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3.
velocity.command.no-plugins=There are no plugins currently installed. velocity.command.no-plugins=There are no plugins currently installed.
velocity.command.plugins-list=Plugins: {0} velocity.command.plugins-list=Plugins: <arg:0>
velocity.command.plugin-tooltip-website=Website: {0} velocity.command.plugin-tooltip-website=Website: <arg:0>
velocity.command.plugin-tooltip-author=Author: {0} velocity.command.plugin-tooltip-author=Author: <arg:0>
velocity.command.plugin-tooltip-authors=Authors: {0} velocity.command.plugin-tooltip-authors=Authors: <arg:0>
velocity.command.dump-uploading=Uploading gathered information... velocity.command.dump-uploading=Uploading gathered information...
velocity.command.dump-send-error=An error occurred while communicating with the Velocity servers. The servers may be temporarily unavailable or there is an issue with your network settings. You can find more information in the log or console of your Velocity server. velocity.command.dump-send-error=An error occurred while communicating with the Velocity servers. The servers may be temporarily unavailable or there is an issue with your network settings. You can find more information in the log or console of your Velocity server.
velocity.command.dump-success=Created an anonymised report containing useful information about this proxy. If a developer requested it, you may share the following link with them: velocity.command.dump-success=Created an anonymised report containing useful information about this proxy. If a developer requested it, you may share the following link with them:
@@ -18,42 +18,42 @@
velocity.error.already-connected=أنت بالفعل متصل بهذا السيرفر\! velocity.error.already-connected=أنت بالفعل متصل بهذا السيرفر\!
velocity.error.already-connected-proxy=أنت بالفعل متصل بهذا الوكيل\! velocity.error.already-connected-proxy=أنت بالفعل متصل بهذا الوكيل\!
velocity.error.already-connecting=أنت بالفعل تحاول الاتصال بأحد السيرفرات\! velocity.error.already-connecting=أنت بالفعل تحاول الاتصال بأحد السيرفرات\!
velocity.error.cant-connect=فشل الاتصال بـ{0}\: {1} velocity.error.cant-connect=فشل الاتصال بـ<arg:0>\: <arg:1>
velocity.error.connecting-server-error=فشل الاتصال بـ{0}، حاول في وقتٍ لاحق. velocity.error.connecting-server-error=فشل الاتصال بـ<arg:0>، حاول في وقتٍ لاحق.
velocity.error.connected-server-error=واجه اتصالك بـ{0} مشكلة. velocity.error.connected-server-error=واجه اتصالك بـ<arg:0> مشكلة.
velocity.error.internal-server-connection-error=حدث خطأ في الاتصال بالسيرفر الداخلي. velocity.error.internal-server-connection-error=حدث خطأ في الاتصال بالسيرفر الداخلي.
velocity.error.logging-in-too-fast=لقد حاولت تسجيل الدخول كثيرًا مؤخرًا، حاول في وقتٍ لاحق. velocity.error.logging-in-too-fast=لقد حاولت تسجيل الدخول كثيرًا مؤخرًا، حاول في وقتٍ لاحق.
velocity.error.online-mode-only=لم تقم بتسجيل الدخول بحساب ماينكرافت. إذا كنت مسجل بالفعل جرب إعادة تشغيل اللعبة. velocity.error.online-mode-only=لم تقم بتسجيل الدخول بحساب ماينكرافت. إذا كنت مسجل بالفعل جرب إعادة تشغيل اللعبة.
velocity.error.player-connection-error=حدث خطأ داخلي في الاتصال الخاص بك. velocity.error.player-connection-error=حدث خطأ داخلي في الاتصال الخاص بك.
velocity.error.modern-forwarding-needs-new-client=هذا السيرفر متوافق فقط مع ماينكرافت 1.13 و ما فوق. velocity.error.modern-forwarding-needs-new-client=هذا السيرفر متوافق فقط مع ماينكرافت 1.13 و ما فوق.
velocity.error.modern-forwarding-failed=السيرفر الخاص بك لم يرسل طلب إعادة توجيه إلى الوكيل. تأكد من إعداد الخادم لإعادة التوجيه بـVelocity. velocity.error.modern-forwarding-failed=السيرفر الخاص بك لم يرسل طلب إعادة توجيه إلى الوكيل. تأكد من إعداد الخادم لإعادة التوجيه بـVelocity.
velocity.error.moved-to-new-server=لقد تم طردك من {0}\: {1} velocity.error.moved-to-new-server=لقد تم طردك من <arg:0>\: <arg:1>
velocity.error.no-available-servers=لا توجد سيرفرات متاحة للاتصال. حاول مرة أخرى أو اتصل بالأدمِن. velocity.error.no-available-servers=لا توجد سيرفرات متاحة للاتصال. حاول مرة أخرى أو اتصل بالأدمِن.
velocity.error.illegal-chat-characters=Illegal characters in chat velocity.error.illegal-chat-characters=Illegal characters in chat
# Commands # Commands
velocity.command.generic-error=حدث خطأ أثناء تنفيذ هذا الأمر. velocity.command.generic-error=حدث خطأ أثناء تنفيذ هذا الأمر.
velocity.command.command-does-not-exist=هذا الأمر غير موجود. velocity.command.command-does-not-exist=هذا الأمر غير موجود.
velocity.command.players-only=يمكن للاعبين فقط تشغيل هذا الأمر. velocity.command.players-only=يمكن للاعبين فقط تشغيل هذا الأمر.
velocity.command.server-does-not-exist=السيرفر المطلوب {0} غير موجود. velocity.command.server-does-not-exist=السيرفر المطلوب <arg:0> غير موجود.
velocity.command.player-not-found=The specified player {0} does not exist. velocity.command.player-not-found=The specified player <arg:0> does not exist.
velocity.command.server-current-server=أنت الآن متصل بـ{0} velocity.command.server-current-server=أنت الآن متصل بـ<arg:0>
velocity.command.server-too-many=هناك العديد من السيرفرات المتاحة، استخدم البحث بزر tab لتصفح قائمة السيرفرات. velocity.command.server-too-many=هناك العديد من السيرفرات المتاحة، استخدم البحث بزر tab لتصفح قائمة السيرفرات.
velocity.command.server-available=السيرفرات المتاحة\: velocity.command.server-available=السيرفرات المتاحة\:
velocity.command.server-tooltip-player-online=لاعب واحد متصل velocity.command.server-tooltip-player-online=لاعب واحد متصل
velocity.command.server-tooltip-players-online={0} لاعبين متصلون velocity.command.server-tooltip-players-online=<arg:0> لاعبين متصلون
velocity.command.server-tooltip-current-server=انت متصل حاليًا بهذا السيرفر velocity.command.server-tooltip-current-server=انت متصل حاليًا بهذا السيرفر
velocity.command.server-tooltip-offer-connect-server=انقر للاتصال بهذا السيرفر velocity.command.server-tooltip-offer-connect-server=انقر للاتصال بهذا السيرفر
velocity.command.glist-player-singular=هناك لاعب واحد متصل بالوكيل. velocity.command.glist-player-singular=هناك لاعب واحد متصل بالوكيل.
velocity.command.glist-player-plural=هناك {0} لاعبين متصلون بالوكيل. velocity.command.glist-player-plural=هناك <arg:0> لاعبين متصلون بالوكيل.
velocity.command.glist-view-all=لعرض اللاعبين على جميع السيرفرات استخدم /glist all velocity.command.glist-view-all=لعرض اللاعبين على جميع السيرفرات استخدم /glist all
velocity.command.reload-success=تم إعادة تحميل إعدادات Velocity بنجاح. velocity.command.reload-success=تم إعادة تحميل إعدادات Velocity بنجاح.
velocity.command.reload-failure=فشلت إعادة تحميل إعدادات Velocity. تفقد الـconsole للمزيد من التفاصيل. velocity.command.reload-failure=فشلت إعادة تحميل إعدادات Velocity. تفقد الـconsole للمزيد من التفاصيل.
velocity.command.version-copyright=حقوق الطبع والنشر 2018-{2} {0}. {1} مرخصة بموجب شروط الإصدار الثالث لرخصة GNU العامة (GPLv3). velocity.command.version-copyright=حقوق الطبع والنشر 2018-<arg:2> <arg:0>. <arg:1> مرخصة بموجب شروط الإصدار الثالث لرخصة GNU العامة (GPLv3).
velocity.command.no-plugins=لا توجد إضافات مثبتة على Velocity. velocity.command.no-plugins=لا توجد إضافات مثبتة على Velocity.
velocity.command.plugins-list=الإضافات\: {0} velocity.command.plugins-list=الإضافات\: <arg:0>
velocity.command.plugin-tooltip-website=موقعها\: {0} velocity.command.plugin-tooltip-website=موقعها\: <arg:0>
velocity.command.plugin-tooltip-author=تصميم\: {0} velocity.command.plugin-tooltip-author=تصميم\: <arg:0>
velocity.command.plugin-tooltip-authors=تصميم\: {0} velocity.command.plugin-tooltip-authors=تصميم\: <arg:0>
velocity.command.dump-uploading=جاري تجميع و رفع معلومات نظامك... velocity.command.dump-uploading=جاري تجميع و رفع معلومات نظامك...
velocity.command.dump-send-error=حدث خطأ أثناء الاتصال بسيرفر Velocity. قد يكون السيرفر غير متاح مؤقتاً أو هناك مشكلة في إعدادات الشبكة الخاصة بك. يمكنك العثور على مزيد من المعلومات في log أو console وكيل Velocity الخاص بك. velocity.command.dump-send-error=حدث خطأ أثناء الاتصال بسيرفر Velocity. قد يكون السيرفر غير متاح مؤقتاً أو هناك مشكلة في إعدادات الشبكة الخاصة بك. يمكنك العثور على مزيد من المعلومات في log أو console وكيل Velocity الخاص بك.
velocity.command.dump-success=تم إنشاء تقرير مفصل يحتوي على معلومات مفيدة عن الوكيل الخاص بك. إذا طلبه المطور، يمكنك مشاركة الرابط التالي معه\: velocity.command.dump-success=تم إنشاء تقرير مفصل يحتوي على معلومات مفيدة عن الوكيل الخاص بك. إذا طلبه المطور، يمكنك مشاركة الرابط التالي معه\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=Вече сте свързани към този сървър\! velocity.error.already-connected=Вече сте свързани към този сървър\!
velocity.error.already-connected-proxy=Вече сте свързани към това прокси\! velocity.error.already-connected-proxy=Вече сте свързани към това прокси\!
velocity.error.already-connecting=Вече се опитвате да се свържете към сървър\! velocity.error.already-connecting=Вече се опитвате да се свържете към сървър\!
velocity.error.cant-connect=Не успяхме да Ви свържем към {0}\: {1} velocity.error.cant-connect=Не успяхме да Ви свържем към <arg:0>\: <arg:1>
velocity.error.connecting-server-error=Не успяхме да Ви свържем към {0}. Моля, опитайте по-късно. velocity.error.connecting-server-error=Не успяхме да Ви свържем към <arg:0>. Моля, опитайте по-късно.
velocity.error.connected-server-error=Възникна грешка, докато бяхте свързан към {0}. velocity.error.connected-server-error=Възникна грешка, докато бяхте свързан към <arg:0>.
velocity.error.internal-server-connection-error=Възникна вътрешна грешка със сървърната връзка. velocity.error.internal-server-connection-error=Възникна вътрешна грешка със сървърната връзка.
velocity.error.logging-in-too-fast=Опитвате се да влизате твърде бързо - моля, опитайте по-късно. velocity.error.logging-in-too-fast=Опитвате се да влизате твърде бързо - моля, опитайте по-късно.
velocity.error.online-mode-only=Не сте влезли в своя Minecraft акаунт. Ако вече сте го направили, опитайте да рестартирате играта и лаунчера и опитайте отново. velocity.error.online-mode-only=Не сте влезли в своя Minecraft акаунт. Ако вече сте го направили, опитайте да рестартирате играта и лаунчера и опитайте отново.
velocity.error.player-connection-error=Възникна грешка с вашата връзка. velocity.error.player-connection-error=Възникна грешка с вашата връзка.
velocity.error.modern-forwarding-needs-new-client=Този сървър е съвместим само с Minecraft 1.13 или по-нова версия. velocity.error.modern-forwarding-needs-new-client=Този сървър е съвместим само с Minecraft 1.13 или по-нова версия.
velocity.error.modern-forwarding-failed=Сървъра Ви не изпрати заявка за препращане на информация към проксито. Моля, убедете се, че сървъра Ви е настроен за работа с Velocity. velocity.error.modern-forwarding-failed=Сървъра Ви не изпрати заявка за препращане на информация към проксито. Моля, убедете се, че сървъра Ви е настроен за работа с Velocity.
velocity.error.moved-to-new-server=Ти беше изхвърлен от {0}\: {1} velocity.error.moved-to-new-server=Ти беше изхвърлен от <arg:0>\: <arg:1>
velocity.error.no-available-servers=Няма налични сървъри, км които да Ви свържем. Моля, опитайте по-късно, или се свържете с администратор. velocity.error.no-available-servers=Няма налични сървъри, км които да Ви свържем. Моля, опитайте по-късно, или се свържете с администратор.
velocity.error.illegal-chat-characters=Неодобрени символи в чата velocity.error.illegal-chat-characters=Неодобрени символи в чата
# Commands # Commands
velocity.command.generic-error=Възникна грешка при изпълняването на командата. velocity.command.generic-error=Възникна грешка при изпълняването на командата.
velocity.command.command-does-not-exist=Тази команда не съществува. velocity.command.command-does-not-exist=Тази команда не съществува.
velocity.command.players-only=Само играчи могат да изпълняват тази команда. velocity.command.players-only=Само играчи могат да изпълняват тази команда.
velocity.command.server-does-not-exist=Сървър с името {0} не съществува. velocity.command.server-does-not-exist=Сървър с името <arg:0> не съществува.
velocity.command.player-not-found=Играч с името {0} не съществува. velocity.command.player-not-found=Играч с името <arg:0> не съществува.
velocity.command.server-current-server=В момента сте свързан към {0}. velocity.command.server-current-server=В момента сте свързан към <arg:0>.
velocity.command.server-too-many=Има прекалено много регистрирани сървъри. Използвайте TAB, за да видите всички налични сървъри. velocity.command.server-too-many=Има прекалено много регистрирани сървъри. Използвайте TAB, за да видите всички налични сървъри.
velocity.command.server-available=Налични сървъри\: velocity.command.server-available=Налични сървъри\:
velocity.command.server-tooltip-player-online={0} играч на линия velocity.command.server-tooltip-player-online=<arg:0> играч на линия
velocity.command.server-tooltip-players-online={0} играчи на линия velocity.command.server-tooltip-players-online=<arg:0> играчи на линия
velocity.command.server-tooltip-current-server=В момента сте свързани към този сървър velocity.command.server-tooltip-current-server=В момента сте свързани към този сървър
velocity.command.server-tooltip-offer-connect-server=Натиснете тук, за да Ви свържем към този сървър velocity.command.server-tooltip-offer-connect-server=Натиснете тук, за да Ви свържем към този сървър
velocity.command.glist-player-singular={0} играч е свързан към проксито. velocity.command.glist-player-singular=<arg:0> играч е свързан към проксито.
velocity.command.glist-player-plural={0} играчи са свързани към проксито. velocity.command.glist-player-plural=<arg:0> играчи са свързани към проксито.
velocity.command.glist-view-all=За да видите всички играчи, разпределени по сървъри, използвайте /glist all. velocity.command.glist-view-all=За да видите всички играчи, разпределени по сървъри, използвайте /glist all.
velocity.command.reload-success=Настройките на Velocity бяха презаредени успешно. velocity.command.reload-success=Настройките на Velocity бяха презаредени успешно.
velocity.command.reload-failure=Не успяхме да презаредим настройките на Velocity. Моля, проверете конзолата за повече информация. velocity.command.reload-failure=Не успяхме да презаредим настройките на Velocity. Моля, проверете конзолата за повече информация.
velocity.command.version-copyright=Авторско право 2018-{2} {0}. {1} е лицензиран под условията на GNU General Public License v3. velocity.command.version-copyright=Авторско право 2018-<arg:2> <arg:0>. <arg:1> е лицензиран под условията на GNU General Public License v3.
velocity.command.no-plugins=За момента няма инсталирани добавки. velocity.command.no-plugins=За момента няма инсталирани добавки.
velocity.command.plugins-list=Добавки\: {0} velocity.command.plugins-list=Добавки\: <arg:0>
velocity.command.plugin-tooltip-website=Уебсайт\: {0} velocity.command.plugin-tooltip-website=Уебсайт\: <arg:0>
velocity.command.plugin-tooltip-author=Автор\: {0} velocity.command.plugin-tooltip-author=Автор\: <arg:0>
velocity.command.plugin-tooltip-authors=Автори\: {0} velocity.command.plugin-tooltip-authors=Автори\: <arg:0>
velocity.command.dump-uploading=Качваме събраната информация... velocity.command.dump-uploading=Качваме събраната информация...
velocity.command.dump-send-error=Възникна грешка при комуникацията със сървърите на Velocity. Сървърите може временно да не са налични, или да имате проблем с мрежовите настройки. Ще откриете повече информация в логовете или конзолата на Вашето Velocity прокси. velocity.command.dump-send-error=Възникна грешка при комуникацията със сървърите на Velocity. Сървърите може временно да не са налични, или да имате проблем с мрежовите настройки. Ще откриете повече информация в логовете или конзолата на Вашето Velocity прокси.
velocity.command.dump-success=Създадохме анонимен доклад, съдържащ полезна информация относно това прокси. Ако разработчик Ви го е поискал, може да споделите този линк с тях\: velocity.command.dump-success=Създадохме анонимен доклад, съдържащ полезна информация относно това прокси. Ако разработчик Ви го е поискал, може да споделите този линк с тях\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=K tomuto serveru jsi již připojen\! velocity.error.already-connected=K tomuto serveru jsi již připojen\!
velocity.error.already-connected-proxy=K tomuto proxy serveru jsi již připojen\! velocity.error.already-connected-proxy=K tomuto proxy serveru jsi již připojen\!
velocity.error.already-connecting=Již se pokoušíš o připojení k serveru\! velocity.error.already-connecting=Již se pokoušíš o připojení k serveru\!
velocity.error.cant-connect=Nepodařilo se připojit k serveru {0}\: {1} velocity.error.cant-connect=Nepodařilo se připojit k serveru <arg:0>\: <arg:1>
velocity.error.connecting-server-error=Nepodařilo se tě připojit k serveru {0}. Zkus to prosím později. velocity.error.connecting-server-error=Nepodařilo se tě připojit k serveru <arg:0>. Zkus to prosím později.
velocity.error.connected-server-error=Nastala chyba ve tvém připojení k serveru {0}. velocity.error.connected-server-error=Nastala chyba ve tvém připojení k serveru <arg:0>.
velocity.error.internal-server-connection-error=V připojení k serveru se vyskytla interní chyba. velocity.error.internal-server-connection-error=V připojení k serveru se vyskytla interní chyba.
velocity.error.logging-in-too-fast=Přihlašuješ se příliš rychle, počkej chvíli. velocity.error.logging-in-too-fast=Přihlašuješ se příliš rychle, počkej chvíli.
velocity.error.online-mode-only=Nejsi připojen ke svému Minecraft účtu. Pokud ano, nastala chyba. Zkus restartovat hru. velocity.error.online-mode-only=Nejsi připojen ke svému Minecraft účtu. Pokud ano, nastala chyba. Zkus restartovat hru.
velocity.error.player-connection-error=Ve tvém připojení nastala chyba. velocity.error.player-connection-error=Ve tvém připojení nastala chyba.
velocity.error.modern-forwarding-needs-new-client=Tento server je kompatibilní pouze s verzí Minecraftu 1.13 a vyšší. velocity.error.modern-forwarding-needs-new-client=Tento server je kompatibilní pouze s verzí Minecraftu 1.13 a vyšší.
velocity.error.modern-forwarding-failed=Tvůj server neodeslal přesměrovávací požadavek na proxy server. Ujisti se, že je server nastaven na Velocity přesměrování. velocity.error.modern-forwarding-failed=Tvůj server neodeslal přesměrovávací požadavek na proxy server. Ujisti se, že je server nastaven na Velocity přesměrování.
velocity.error.moved-to-new-server=Byl jsi vyhozen ze serveru {0}\: {1} velocity.error.moved-to-new-server=Byl jsi vyhozen ze serveru <arg:0>\: <arg:1>
velocity.error.no-available-servers=Nejsou k dispozici žádné servery, ke kterým by ses mohl připojit. Zkus to později nebo kontaktuj správce. velocity.error.no-available-servers=Nejsou k dispozici žádné servery, ke kterým by ses mohl připojit. Zkus to později nebo kontaktuj správce.
velocity.error.illegal-chat-characters=Nepovolené znaky v chatu velocity.error.illegal-chat-characters=Nepovolené znaky v chatu
# Commands # Commands
velocity.command.generic-error=Při vykonávání tohoto příkazu nastala chyba. velocity.command.generic-error=Při vykonávání tohoto příkazu nastala chyba.
velocity.command.command-does-not-exist=Tento příkaz neexistuje. velocity.command.command-does-not-exist=Tento příkaz neexistuje.
velocity.command.players-only=Tento příkaz mohou vykonávat pouze hráči. velocity.command.players-only=Tento příkaz mohou vykonávat pouze hráči.
velocity.command.server-does-not-exist=Zadaný server {0} neexistuje. velocity.command.server-does-not-exist=Zadaný server <arg:0> neexistuje.
velocity.command.player-not-found=Zadaný hráč {0} neexistuje. velocity.command.player-not-found=Zadaný hráč <arg:0> neexistuje.
velocity.command.server-current-server=Právě jsi připojen k serveru {0}. velocity.command.server-current-server=Právě jsi připojen k serveru <arg:0>.
velocity.command.server-too-many=Je nastaveno příliš mnoho serverů. Klávesa tab ukáže všechny dostupné servery. velocity.command.server-too-many=Je nastaveno příliš mnoho serverů. Klávesa tab ukáže všechny dostupné servery.
velocity.command.server-available=Dostupné servery\: velocity.command.server-available=Dostupné servery\:
velocity.command.server-tooltip-player-online={0} hráč online velocity.command.server-tooltip-player-online=<arg:0> hráč online
velocity.command.server-tooltip-players-online=Počet hráčů online\: {0} velocity.command.server-tooltip-players-online=Počet hráčů online\: <arg:0>
velocity.command.server-tooltip-current-server=Právě jsi připojen k tomuto serveru velocity.command.server-tooltip-current-server=Právě jsi připojen k tomuto serveru
velocity.command.server-tooltip-offer-connect-server=Kliknutím se připojíš k tomuto serveru velocity.command.server-tooltip-offer-connect-server=Kliknutím se připojíš k tomuto serveru
velocity.command.glist-player-singular=K tomuto proxy serveru je připojen {0} hráč. velocity.command.glist-player-singular=K tomuto proxy serveru je připojen <arg:0> hráč.
velocity.command.glist-player-plural=Počet hráčů připojených k tomuto proxy serveru\: {0} velocity.command.glist-player-plural=Počet hráčů připojených k tomuto proxy serveru\: <arg:0>
velocity.command.glist-view-all=Ke zobrazení všech hráčů na všech serverech použij /glist all. velocity.command.glist-view-all=Ke zobrazení všech hráčů na všech serverech použij /glist all.
velocity.command.reload-success=Konfigurace Velocity úspěšně načtena. velocity.command.reload-success=Konfigurace Velocity úspěšně načtena.
velocity.command.reload-failure=Nebylo možné načíst konfiguraci Velocity. Podrobnosti jsou na konzoli. velocity.command.reload-failure=Nebylo možné načíst konfiguraci Velocity. Podrobnosti jsou na konzoli.
velocity.command.version-copyright=Copyright 2018-{2} {0}. {1} je licencovaný pod podmínkami GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> je licencovaný pod podmínkami GNU General Public License v3.
velocity.command.no-plugins=V tuto chvíli nejsou nainstalovány žádné zásuvné moduly. velocity.command.no-plugins=V tuto chvíli nejsou nainstalovány žádné zásuvné moduly.
velocity.command.plugins-list=Zásuvné moduly\: {0} velocity.command.plugins-list=Zásuvné moduly\: <arg:0>
velocity.command.plugin-tooltip-website=Webová stránka\: {0} velocity.command.plugin-tooltip-website=Webová stránka\: <arg:0>
velocity.command.plugin-tooltip-author=Autor\: {0} velocity.command.plugin-tooltip-author=Autor\: <arg:0>
velocity.command.plugin-tooltip-authors=Autoři\: {0} velocity.command.plugin-tooltip-authors=Autoři\: <arg:0>
velocity.command.dump-uploading=Nahrávání získaných informací... velocity.command.dump-uploading=Nahrávání získaných informací...
velocity.command.dump-send-error=Nastala chyba při komunikaci s Velocity servery. Servery mohou být dočasně nedostupné nebo je chyba v přístupu na internet. Podrobnosti jsou v logu a na konzoli Velocity serveru. velocity.command.dump-send-error=Nastala chyba při komunikaci s Velocity servery. Servery mohou být dočasně nedostupné nebo je chyba v přístupu na internet. Podrobnosti jsou v logu a na konzoli Velocity serveru.
velocity.command.dump-success=Byla vytvořena anonymizovaná zpráva obsahující užitečné informace o tomto serveru. Vyžádal-li si je vývojář, můžeš mu poslat nasledující odkaz\: velocity.command.dump-success=Byla vytvořena anonymizovaná zpráva obsahující užitečné informace o tomto serveru. Vyžádal-li si je vývojář, můžeš mu poslat nasledující odkaz\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=Du er allerede tilsluttet til den server\! velocity.error.already-connected=Du er allerede tilsluttet til den server\!
velocity.error.already-connected-proxy=Du er allerede tilsluttet til proxyen\! velocity.error.already-connected-proxy=Du er allerede tilsluttet til proxyen\!
velocity.error.already-connecting=Du forsøger allerede at oprette forbindelse til en server\! velocity.error.already-connecting=Du forsøger allerede at oprette forbindelse til en server\!
velocity.error.cant-connect=Kan ikke forbinde til {0}\: {1} velocity.error.cant-connect=Kan ikke forbinde til <arg:0>\: <arg:1>
velocity.error.connecting-server-error=Kan ikke forbinde dig til {0}. Prøv igen senere. velocity.error.connecting-server-error=Kan ikke forbinde dig til <arg:0>. Prøv igen senere.
velocity.error.connected-server-error=Din forbindelse til {0} stødte på et problem. velocity.error.connected-server-error=Din forbindelse til <arg:0> stødte på et problem.
velocity.error.internal-server-connection-error=Der opstod en intern server forbindelsesfejl. velocity.error.internal-server-connection-error=Der opstod en intern server forbindelsesfejl.
velocity.error.logging-in-too-fast=Du logger ind for hurtigt, prøv igen senere. velocity.error.logging-in-too-fast=Du logger ind for hurtigt, prøv igen senere.
velocity.error.online-mode-only=Du er ikke logget ind på din Minecraft-konto. Hvis du er logget ind på din Minecraft-konto, så prøv at genstarte din Minecraft-klient. velocity.error.online-mode-only=Du er ikke logget ind på din Minecraft-konto. Hvis du er logget ind på din Minecraft-konto, så prøv at genstarte din Minecraft-klient.
velocity.error.player-connection-error=Der opstod en intern fejl i din forbindelse. velocity.error.player-connection-error=Der opstod en intern fejl i din forbindelse.
velocity.error.modern-forwarding-needs-new-client=Denne server er kun kompatibel med Minecraft 1.13 og derover. velocity.error.modern-forwarding-needs-new-client=Denne server er kun kompatibel med Minecraft 1.13 og derover.
velocity.error.modern-forwarding-failed=Din server sendte ikke en viderestillingsanmodning til proxyen. Sørg for, at serveren er konfigureret til Velocity forwarding. velocity.error.modern-forwarding-failed=Din server sendte ikke en viderestillingsanmodning til proxyen. Sørg for, at serveren er konfigureret til Velocity forwarding.
velocity.error.moved-to-new-server=Du blev smidt ud fra {0}\: {1} velocity.error.moved-to-new-server=Du blev smidt ud fra <arg:0>\: <arg:1>
velocity.error.no-available-servers=Der er ingen tilgængelige servere at forbinde dig til. Prøv igen senere eller kontakt en administrator. velocity.error.no-available-servers=Der er ingen tilgængelige servere at forbinde dig til. Prøv igen senere eller kontakt en administrator.
velocity.error.illegal-chat-characters=Illegal characters in chat velocity.error.illegal-chat-characters=Illegal characters in chat
# Commands # Commands
velocity.command.generic-error=Der opstod en fejl da du kørte kommandoen. velocity.command.generic-error=Der opstod en fejl da du kørte kommandoen.
velocity.command.command-does-not-exist=Denne kommando eksisterer ikke. velocity.command.command-does-not-exist=Denne kommando eksisterer ikke.
velocity.command.players-only=Kun spillere kan køre denne kommando. velocity.command.players-only=Kun spillere kan køre denne kommando.
velocity.command.server-does-not-exist=Den angivne server {0} findes ikke. velocity.command.server-does-not-exist=Den angivne server <arg:0> findes ikke.
velocity.command.player-not-found=The specified player {0} does not exist. velocity.command.player-not-found=The specified player <arg:0> does not exist.
velocity.command.server-current-server=Du er i øjeblikket forbundet til {0}. velocity.command.server-current-server=Du er i øjeblikket forbundet til <arg:0>.
velocity.command.server-too-many=Der er sat for mange servere op. Brug tab færdiggørelse til at se alle tilgængelige servere. velocity.command.server-too-many=Der er sat for mange servere op. Brug tab færdiggørelse til at se alle tilgængelige servere.
velocity.command.server-available=Tilgængelige servere\: velocity.command.server-available=Tilgængelige servere\:
velocity.command.server-tooltip-player-online={0} spiller online velocity.command.server-tooltip-player-online=<arg:0> spiller online
velocity.command.server-tooltip-players-online={0} spillere online velocity.command.server-tooltip-players-online=<arg:0> spillere online
velocity.command.server-tooltip-current-server=I øjeblikket forbundet til serveren velocity.command.server-tooltip-current-server=I øjeblikket forbundet til serveren
velocity.command.server-tooltip-offer-connect-server=Klik for at forbinde til denne server velocity.command.server-tooltip-offer-connect-server=Klik for at forbinde til denne server
velocity.command.glist-player-singular={0} spiller er i øjeblikket forbundet til proxyen. velocity.command.glist-player-singular=<arg:0> spiller er i øjeblikket forbundet til proxyen.
velocity.command.glist-player-plural={0} spillere er i øjeblikket forbundet til proxyen. velocity.command.glist-player-plural=<arg:0> spillere er i øjeblikket forbundet til proxyen.
velocity.command.glist-view-all=For at se alle spillere på servere, brug /glist all. velocity.command.glist-view-all=For at se alle spillere på servere, brug /glist all.
velocity.command.reload-success=Velocity konfiguration blev genindlæst. velocity.command.reload-success=Velocity konfiguration blev genindlæst.
velocity.command.reload-failure=Kan ikke genindlæse din Velocity konfiguration. Tjek konsollen for flere detaljer. velocity.command.reload-failure=Kan ikke genindlæse din Velocity konfiguration. Tjek konsollen for flere detaljer.
velocity.command.version-copyright=Ophavsret 2018-{2} {0}. {1} er licenseret under betingelserne i GNU General Public License v3. velocity.command.version-copyright=Ophavsret 2018-<arg:2> <arg:0>. <arg:1> er licenseret under betingelserne i GNU General Public License v3.
velocity.command.no-plugins=Der er ingen plugins installeret i øjeblikket. velocity.command.no-plugins=Der er ingen plugins installeret i øjeblikket.
velocity.command.plugins-list=Plugins\: {0} velocity.command.plugins-list=Plugins\: <arg:0>
velocity.command.plugin-tooltip-website=Hjemmeside\: {0} velocity.command.plugin-tooltip-website=Hjemmeside\: <arg:0>
velocity.command.plugin-tooltip-author=Forfatter\: {0} velocity.command.plugin-tooltip-author=Forfatter\: <arg:0>
velocity.command.plugin-tooltip-authors=Skabere\: {0} velocity.command.plugin-tooltip-authors=Skabere\: <arg:0>
velocity.command.dump-uploading=Uploader indsamlet information... velocity.command.dump-uploading=Uploader indsamlet information...
velocity.command.dump-send-error=Der opstod en fejl under kommunikation med Velocity serverne. Serverne kan være midlertidigt utilgængelige, eller der er et problem med dine netværksindstillinger. Du kan finde mere information i loggen eller konsollen på din Velocity server. velocity.command.dump-send-error=Der opstod en fejl under kommunikation med Velocity serverne. Serverne kan være midlertidigt utilgængelige, eller der er et problem med dine netværksindstillinger. Du kan finde mere information i loggen eller konsollen på din Velocity server.
velocity.command.dump-success=Oprettet en anonymiseret rapport med nyttige oplysninger om denne proxy. Hvis en udvikler anmodede om det, kan du dele følgende link med dem\: velocity.command.dump-success=Oprettet en anonymiseret rapport med nyttige oplysninger om denne proxy. Hvis en udvikler anmodede om det, kan du dele følgende link med dem\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=Du bist bereits mit diesem Server verbunden\! velocity.error.already-connected=Du bist bereits mit diesem Server verbunden\!
velocity.error.already-connected-proxy=Du bist bereits mit diesem Proxy verbunden\! velocity.error.already-connected-proxy=Du bist bereits mit diesem Proxy verbunden\!
velocity.error.already-connecting=Du versuchst dich bereits mit einem Server zu verbinden\! velocity.error.already-connecting=Du versuchst dich bereits mit einem Server zu verbinden\!
velocity.error.cant-connect=Kein Verbindungsaufbau zu {0} möglich\: {1} velocity.error.cant-connect=Kein Verbindungsaufbau zu <arg:0> möglich\: <arg:1>
velocity.error.connecting-server-error=Kein Verbindungsaufbau zu {0} möglich. Bitte versuche es später erneut. velocity.error.connecting-server-error=Kein Verbindungsaufbau zu <arg:0> möglich. Bitte versuche es später erneut.
velocity.error.connected-server-error=Bei der Verbindung zu {0} ist ein Problem aufgetreten. velocity.error.connected-server-error=Bei der Verbindung zu <arg:0> ist ein Problem aufgetreten.
velocity.error.internal-server-connection-error=Bei der Verbindung mit dem Server ist ein interner Fehler aufgetreten. velocity.error.internal-server-connection-error=Bei der Verbindung mit dem Server ist ein interner Fehler aufgetreten.
velocity.error.logging-in-too-fast=Du meldest dich zu schnell an, versuche es später noch einmal. velocity.error.logging-in-too-fast=Du meldest dich zu schnell an, versuche es später noch einmal.
velocity.error.online-mode-only=Du bist nicht in deinem Minecraft Konto eingeloggt. Wenn du in deinem Minecraft Konto eingeloggt bist, versuche deinen Minecraft Client neu zu starten. velocity.error.online-mode-only=Du bist nicht in deinem Minecraft Konto eingeloggt. Wenn du in deinem Minecraft Konto eingeloggt bist, versuche deinen Minecraft Client neu zu starten.
velocity.error.player-connection-error=Bei deiner Verbindung ist ein interner Fehler aufgetreten. velocity.error.player-connection-error=Bei deiner Verbindung ist ein interner Fehler aufgetreten.
velocity.error.modern-forwarding-needs-new-client=Dieser Server ist nur mit der Minecraft Version 1.13 und höher kompatibel. velocity.error.modern-forwarding-needs-new-client=Dieser Server ist nur mit der Minecraft Version 1.13 und höher kompatibel.
velocity.error.modern-forwarding-failed=Dein Server hat keine Weiterleitungsanforderung an den Proxy gesendet. Stelle sicher, dass der Server für die Velocity Weiterleitung konfiguriert ist. velocity.error.modern-forwarding-failed=Dein Server hat keine Weiterleitungsanforderung an den Proxy gesendet. Stelle sicher, dass der Server für die Velocity Weiterleitung konfiguriert ist.
velocity.error.moved-to-new-server=Du wurdest von {0} vom Server geworfen\: {1} velocity.error.moved-to-new-server=Du wurdest von <arg:0> vom Server geworfen\: <arg:1>
velocity.error.no-available-servers=Es gibt keine verfügbaren Server mit denen du dich verbinden kannst. Versuche es später erneut oder kontaktiere einen Admin. velocity.error.no-available-servers=Es gibt keine verfügbaren Server mit denen du dich verbinden kannst. Versuche es später erneut oder kontaktiere einen Admin.
velocity.error.illegal-chat-characters=Ungültige Zeichen im Chat velocity.error.illegal-chat-characters=Ungültige Zeichen im Chat
# Commands # Commands
velocity.command.generic-error=Beim Ausführen des Befehls ist ein Fehler aufgetreten. velocity.command.generic-error=Beim Ausführen des Befehls ist ein Fehler aufgetreten.
velocity.command.command-does-not-exist=Dieser Befehl existiert nicht. velocity.command.command-does-not-exist=Dieser Befehl existiert nicht.
velocity.command.players-only=Nur Spieler können diesen Befehl ausführen. velocity.command.players-only=Nur Spieler können diesen Befehl ausführen.
velocity.command.server-does-not-exist=Der angegebene Server {0} existiert nicht. velocity.command.server-does-not-exist=Der angegebene Server <arg:0> existiert nicht.
velocity.command.player-not-found=Der angegebene Spieler {0} existiert nicht. velocity.command.player-not-found=Der angegebene Spieler <arg:0> existiert nicht.
velocity.command.server-current-server=Du bist derzeit mit {0} verbunden. velocity.command.server-current-server=Du bist derzeit mit <arg:0> verbunden.
velocity.command.server-too-many=Es sind zu viele Server eingerichtet. Verwende die Tabvervollständigung, um alle verfügbaren Server aufzulisten. velocity.command.server-too-many=Es sind zu viele Server eingerichtet. Verwende die Tabvervollständigung, um alle verfügbaren Server aufzulisten.
velocity.command.server-available=Verfügbare Server\: velocity.command.server-available=Verfügbare Server\:
velocity.command.server-tooltip-player-online={0} Spieler online velocity.command.server-tooltip-player-online=<arg:0> Spieler online
velocity.command.server-tooltip-players-online={0} Spieler online velocity.command.server-tooltip-players-online=<arg:0> Spieler online
velocity.command.server-tooltip-current-server=Du bist derzeit mit diesem Server verbunden velocity.command.server-tooltip-current-server=Du bist derzeit mit diesem Server verbunden
velocity.command.server-tooltip-offer-connect-server=Klicke, um dich mit diesem Server zu verbinden velocity.command.server-tooltip-offer-connect-server=Klicke, um dich mit diesem Server zu verbinden
velocity.command.glist-player-singular={0} Spieler ist derzeit mit dem Proxy verbunden. velocity.command.glist-player-singular=<arg:0> Spieler ist derzeit mit dem Proxy verbunden.
velocity.command.glist-player-plural={0} Spieler sind derzeit mit dem Proxy verbunden. velocity.command.glist-player-plural=<arg:0> Spieler sind derzeit mit dem Proxy verbunden.
velocity.command.glist-view-all=Um alle Spieler auf Servern aufzulisten, verwende /glist all. velocity.command.glist-view-all=Um alle Spieler auf Servern aufzulisten, verwende /glist all.
velocity.command.reload-success=Velocity-Konfiguration erfolgreich neu geladen. velocity.command.reload-success=Velocity-Konfiguration erfolgreich neu geladen.
velocity.command.reload-failure=Die Velocity-Konfiguration konnte nicht neu geladen werden. Prüfe die Konsole für weitere Details. velocity.command.reload-failure=Die Velocity-Konfiguration konnte nicht neu geladen werden. Prüfe die Konsole für weitere Details.
velocity.command.version-copyright=Copyright 2018-{2} {0}. {1} ist lizenziert unter den Bedingungen der GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> ist lizenziert unter den Bedingungen der GNU General Public License v3.
velocity.command.no-plugins=Es sind derzeit keine Plugins installiert. velocity.command.no-plugins=Es sind derzeit keine Plugins installiert.
velocity.command.plugins-list=Plugins\: {0} velocity.command.plugins-list=Plugins\: <arg:0>
velocity.command.plugin-tooltip-website=Webseite\: {0} velocity.command.plugin-tooltip-website=Webseite\: <arg:0>
velocity.command.plugin-tooltip-author=Entwickler\: {0} velocity.command.plugin-tooltip-author=Entwickler\: <arg:0>
velocity.command.plugin-tooltip-authors=Entwickler\: {0} velocity.command.plugin-tooltip-authors=Entwickler\: <arg:0>
velocity.command.dump-uploading=Erfasste Daten werden hochgeladen... velocity.command.dump-uploading=Erfasste Daten werden hochgeladen...
velocity.command.dump-send-error=Bei der Kommunikation mit den Velocity-Servern ist ein Fehler aufgetreten. Diese Server sind möglicherweise vorübergehend nicht verfügbar oder es gibt ein Problem mit deinen Netzwerkeinstellungen. Weitere Informationen findest du in der Log-Datei oder in der Konsole deines Velocity-Servers. velocity.command.dump-send-error=Bei der Kommunikation mit den Velocity-Servern ist ein Fehler aufgetreten. Diese Server sind möglicherweise vorübergehend nicht verfügbar oder es gibt ein Problem mit deinen Netzwerkeinstellungen. Weitere Informationen findest du in der Log-Datei oder in der Konsole deines Velocity-Servers.
velocity.command.dump-success=Ein anonymisierter Bericht mit nützlichen Informationen über diesen Proxy wurde erstellt. Wenn ein Entwickler den Bericht angefordert hat, kannst du diesen über folgenden Link mit ihm teilen\: velocity.command.dump-success=Ein anonymisierter Bericht mit nützlichen Informationen über diesen Proxy wurde erstellt. Wenn ein Entwickler den Bericht angefordert hat, kannst du diesen über folgenden Link mit ihm teilen\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=¡Ya estás conectado a este servidor\! velocity.error.already-connected=¡Ya estás conectado a este servidor\!
velocity.error.already-connected-proxy=¡Ya estás conectado a este proxy\! velocity.error.already-connected-proxy=¡Ya estás conectado a este proxy\!
velocity.error.already-connecting=¡Ya estás intentando conectarte a un servidor\! velocity.error.already-connecting=¡Ya estás intentando conectarte a un servidor\!
velocity.error.cant-connect=No se ha podido conectar a {0}\: {1} velocity.error.cant-connect=No se ha podido conectar a <arg:0>\: <arg:1>
velocity.error.connecting-server-error=No hemos podido conectarte a {0}. Por favor, inténtalo de nuevo más tarde. velocity.error.connecting-server-error=No hemos podido conectarte a <arg:0>. Por favor, inténtalo de nuevo más tarde.
velocity.error.connected-server-error=Tu conexión a {0} ha sufrido un problema. velocity.error.connected-server-error=Tu conexión a <arg:0> ha sufrido un problema.
velocity.error.internal-server-connection-error=Se ha producido un error interno en la conexión al servidor. velocity.error.internal-server-connection-error=Se ha producido un error interno en la conexión al servidor.
velocity.error.logging-in-too-fast=Estás iniciando sesión demasiado rápido, inténtalo de nuevo más tarde. velocity.error.logging-in-too-fast=Estás iniciando sesión demasiado rápido, inténtalo de nuevo más tarde.
velocity.error.online-mode-only=No has iniciado sesión con tu cuenta de Minecraft. Si crees que ya lo estás, intenta reiniciar tu cliente de Minecraft. velocity.error.online-mode-only=No has iniciado sesión con tu cuenta de Minecraft. Si crees que ya lo estás, intenta reiniciar tu cliente de Minecraft.
velocity.error.player-connection-error=Se ha producido un error interno en tu conexión. velocity.error.player-connection-error=Se ha producido un error interno en tu conexión.
velocity.error.modern-forwarding-needs-new-client=Este servidor solo es compatible con Minecraft 1.13 y superior. velocity.error.modern-forwarding-needs-new-client=Este servidor solo es compatible con Minecraft 1.13 y superior.
velocity.error.modern-forwarding-failed=El servidor no ha enviado una solicitud de reenvío al proxy. Asegúrate de que tu servidor está configurado para usar el método de reenvío de Velocity. velocity.error.modern-forwarding-failed=El servidor no ha enviado una solicitud de reenvío al proxy. Asegúrate de que tu servidor está configurado para usar el método de reenvío de Velocity.
velocity.error.moved-to-new-server=Has sido echado de {0}\: {1} velocity.error.moved-to-new-server=Has sido echado de <arg:0>\: <arg:1>
velocity.error.no-available-servers=No hay servidores disponibles a los que conectarte. Inténtalo de nuevo más tarde o contacta con un administrador. velocity.error.no-available-servers=No hay servidores disponibles a los que conectarte. Inténtalo de nuevo más tarde o contacta con un administrador.
velocity.error.illegal-chat-characters=Caracteres no válidos en el chat velocity.error.illegal-chat-characters=Caracteres no válidos en el chat
# Commands # Commands
velocity.command.generic-error=Se ha producido un error al ejecutar este comando. velocity.command.generic-error=Se ha producido un error al ejecutar este comando.
velocity.command.command-does-not-exist=Este comando no existe. velocity.command.command-does-not-exist=Este comando no existe.
velocity.command.players-only=Solo los jugadores pueden ejecutar este comando. velocity.command.players-only=Solo los jugadores pueden ejecutar este comando.
velocity.command.server-does-not-exist=El servidor especificado {0} no existe. velocity.command.server-does-not-exist=El servidor especificado <arg:0> no existe.
velocity.command.player-not-found=El jugador especificado {0} no existe. velocity.command.player-not-found=El jugador especificado <arg:0> no existe.
velocity.command.server-current-server=Estás conectado a {0}. velocity.command.server-current-server=Estás conectado a <arg:0>.
velocity.command.server-too-many=Hay demasiados servidores registrados. Usa la finalización con tabulación para ver todos los servidores disponibles. velocity.command.server-too-many=Hay demasiados servidores registrados. Usa la finalización con tabulación para ver todos los servidores disponibles.
velocity.command.server-available=Servidores disponibles\: velocity.command.server-available=Servidores disponibles\:
velocity.command.server-tooltip-player-online={0} jugador conectado velocity.command.server-tooltip-player-online=<arg:0> jugador conectado
velocity.command.server-tooltip-players-online={0} jugadores conectados velocity.command.server-tooltip-players-online=<arg:0> jugadores conectados
velocity.command.server-tooltip-current-server=Estás conectado a este servidor velocity.command.server-tooltip-current-server=Estás conectado a este servidor
velocity.command.server-tooltip-offer-connect-server=Haz clic para conectarte a este servidor velocity.command.server-tooltip-offer-connect-server=Haz clic para conectarte a este servidor
velocity.command.glist-player-singular={0} jugador está conectado al proxy. velocity.command.glist-player-singular=<arg:0> jugador está conectado al proxy.
velocity.command.glist-player-plural={0} jugadores están conectados al proxy. velocity.command.glist-player-plural=<arg:0> jugadores están conectados al proxy.
velocity.command.glist-view-all=Para ver todos los jugadores por servidores, usa /glist all. velocity.command.glist-view-all=Para ver todos los jugadores por servidores, usa /glist all.
velocity.command.reload-success=La configuración de Velocity ha sido recargada correctamente. velocity.command.reload-success=La configuración de Velocity ha sido recargada correctamente.
velocity.command.reload-failure=No ha sido posible recargar la configuración de Velocity. Para obtener más información, revisa la consola. velocity.command.reload-failure=No ha sido posible recargar la configuración de Velocity. Para obtener más información, revisa la consola.
velocity.command.version-copyright=Copyright 2018-{2} {0}. {1} está licenciado bajo los términos de la Licencia Pública General de GNU v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> está licenciado bajo los términos de la Licencia Pública General de GNU v3.
velocity.command.no-plugins=Actualmente no hay plugins instalados. velocity.command.no-plugins=Actualmente no hay plugins instalados.
velocity.command.plugins-list=Complementos\: {0} velocity.command.plugins-list=Complementos\: <arg:0>
velocity.command.plugin-tooltip-website=Página web\: {0} velocity.command.plugin-tooltip-website=Página web\: <arg:0>
velocity.command.plugin-tooltip-author=Autor\: {0} velocity.command.plugin-tooltip-author=Autor\: <arg:0>
velocity.command.plugin-tooltip-authors=Autores\: {0} velocity.command.plugin-tooltip-authors=Autores\: <arg:0>
velocity.command.dump-uploading=Subiendo la información recopilada... velocity.command.dump-uploading=Subiendo la información recopilada...
velocity.command.dump-send-error=Se ha producido un error al comunicarse con los servidores de Velocity. Es posible que los servidores no estén disponibles temporalmente o que exista un problema en tu configuración de red. Puedes encontrar más información en el archivo de registro o la consola de tu servidor Velocity. velocity.command.dump-send-error=Se ha producido un error al comunicarse con los servidores de Velocity. Es posible que los servidores no estén disponibles temporalmente o que exista un problema en tu configuración de red. Puedes encontrar más información en el archivo de registro o la consola de tu servidor Velocity.
velocity.command.dump-success=Se ha creado un informe anónimo que contiene información útil sobre este proxy. Si un desarrollador lo solicita, puedes compartir el siguiente enlace con él\: velocity.command.dump-success=Se ha creado un informe anónimo que contiene información útil sobre este proxy. Si un desarrollador lo solicita, puedes compartir el siguiente enlace con él\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=Sa oled juba antud serveriga ühendatud\! velocity.error.already-connected=Sa oled juba antud serveriga ühendatud\!
velocity.error.already-connected-proxy=Sa oled juba antud proksiga ühendatud\! velocity.error.already-connected-proxy=Sa oled juba antud proksiga ühendatud\!
velocity.error.already-connecting=Sa juba ühendad severiga\! velocity.error.already-connecting=Sa juba ühendad severiga\!
velocity.error.cant-connect=Unable to connect to {0}\: {1} velocity.error.cant-connect=Unable to connect to <arg:0>\: <arg:1>
velocity.error.connecting-server-error=Unable to connect you to {0}. Please try again later. velocity.error.connecting-server-error=Unable to connect you to <arg:0>. Please try again later.
velocity.error.connected-server-error=Your connection to {0} encountered a problem. velocity.error.connected-server-error=Your connection to <arg:0> encountered a problem.
velocity.error.internal-server-connection-error=An internal server connection error occurred. velocity.error.internal-server-connection-error=An internal server connection error occurred.
velocity.error.logging-in-too-fast=Sa logid sisse liiga kiiresti, proovi hiljem uuesti. velocity.error.logging-in-too-fast=Sa logid sisse liiga kiiresti, proovi hiljem uuesti.
velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client. velocity.error.online-mode-only=You are not logged into your Minecraft account. If you are logged into your Minecraft account, try restarting your Minecraft client.
velocity.error.player-connection-error=An internal error occurred in your connection. velocity.error.player-connection-error=An internal error occurred in your connection.
velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above. velocity.error.modern-forwarding-needs-new-client=This server is only compatible with Minecraft 1.13 and above.
velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding. velocity.error.modern-forwarding-failed=Your server did not send a forwarding request to the proxy. Make sure the server is configured for Velocity forwarding.
velocity.error.moved-to-new-server=You were kicked from {0}\: {1} velocity.error.moved-to-new-server=You were kicked from <arg:0>\: <arg:1>
velocity.error.no-available-servers=There are no available servers to connect you to. Try again later or contact an admin. velocity.error.no-available-servers=There are no available servers to connect you to. Try again later or contact an admin.
velocity.error.illegal-chat-characters=Illegal characters in chat velocity.error.illegal-chat-characters=Illegal characters in chat
# Commands # Commands
velocity.command.generic-error=An error occurred while running this command. velocity.command.generic-error=An error occurred while running this command.
velocity.command.command-does-not-exist=Antud käsklust ei eksisteeri. velocity.command.command-does-not-exist=Antud käsklust ei eksisteeri.
velocity.command.players-only=Ainult mängijad saavad antud käsklust kasutada. velocity.command.players-only=Ainult mängijad saavad antud käsklust kasutada.
velocity.command.server-does-not-exist=Server {0} ei eksisteeri. velocity.command.server-does-not-exist=Server <arg:0> ei eksisteeri.
velocity.command.player-not-found=The specified player {0} does not exist. velocity.command.player-not-found=The specified player <arg:0> does not exist.
velocity.command.server-current-server=Sa oled hetkel ühendatud serveriga {0}. velocity.command.server-current-server=Sa oled hetkel ühendatud serveriga <arg:0>.
velocity.command.server-too-many=There are too many servers set up. Use tab completion to view all servers available. velocity.command.server-too-many=There are too many servers set up. Use tab completion to view all servers available.
velocity.command.server-available=Saadaolevad serverid\: velocity.command.server-available=Saadaolevad serverid\:
velocity.command.server-tooltip-player-online={0} mängija online velocity.command.server-tooltip-player-online=<arg:0> mängija online
velocity.command.server-tooltip-players-online={0} mängijat online velocity.command.server-tooltip-players-online=<arg:0> mängijat online
velocity.command.server-tooltip-current-server=Currently connected to this server velocity.command.server-tooltip-current-server=Currently connected to this server
velocity.command.server-tooltip-offer-connect-server=Vajuta, et ühendada antud serveriga velocity.command.server-tooltip-offer-connect-server=Vajuta, et ühendada antud serveriga
velocity.command.glist-player-singular={0} player is currently connected to the proxy. velocity.command.glist-player-singular=<arg:0> player is currently connected to the proxy.
velocity.command.glist-player-plural={0} players are currently connected to the proxy. velocity.command.glist-player-plural=<arg:0> players are currently connected to the proxy.
velocity.command.glist-view-all=Et näha kõiki mängijaid kõikides serverites, kasuta käsklust /glist all. velocity.command.glist-view-all=Et näha kõiki mängijaid kõikides serverites, kasuta käsklust /glist all.
velocity.command.reload-success=Velocity seadistus edukalt taaslaetud. velocity.command.reload-success=Velocity seadistus edukalt taaslaetud.
velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details. velocity.command.reload-failure=Unable to reload your Velocity configuration. Check the console for more details.
velocity.command.version-copyright=Copyright 2018-{2} {0}. {1} is licensed under the terms of the GNU General Public License v3. velocity.command.version-copyright=Copyright 2018-<arg:2> <arg:0>. <arg:1> is licensed under the terms of the GNU General Public License v3.
velocity.command.no-plugins=There are no plugins currently installed. velocity.command.no-plugins=There are no plugins currently installed.
velocity.command.plugins-list=Pluginad\: {0} velocity.command.plugins-list=Pluginad\: <arg:0>
velocity.command.plugin-tooltip-website=Veebileht\: {0} velocity.command.plugin-tooltip-website=Veebileht\: <arg:0>
velocity.command.plugin-tooltip-author=Autor\: {0} velocity.command.plugin-tooltip-author=Autor\: <arg:0>
velocity.command.plugin-tooltip-authors=Autorid\: {0} velocity.command.plugin-tooltip-authors=Autorid\: <arg:0>
velocity.command.dump-uploading=Kogutud informatsiooni üleslaadimine... velocity.command.dump-uploading=Kogutud informatsiooni üleslaadimine...
velocity.command.dump-send-error=An error occurred while communicating with the Velocity servers. The servers may be temporarily unavailable or there is an issue with your network settings. You can find more information in the log or console of your Velocity server. velocity.command.dump-send-error=An error occurred while communicating with the Velocity servers. The servers may be temporarily unavailable or there is an issue with your network settings. You can find more information in the log or console of your Velocity server.
velocity.command.dump-success=Created an anonymised report containing useful information about this proxy. If a developer requested it, you may share the following link with them\: velocity.command.dump-success=Created an anonymised report containing useful information about this proxy. If a developer requested it, you may share the following link with them\:
@@ -18,42 +18,42 @@
velocity.error.already-connected=Olet jo yhteydessä tälle palvelimelle\! velocity.error.already-connected=Olet jo yhteydessä tälle palvelimelle\!
velocity.error.already-connected-proxy=Olet jo yhteydessä tälle välityspalvelimelle\! velocity.error.already-connected-proxy=Olet jo yhteydessä tälle välityspalvelimelle\!
velocity.error.already-connecting=Yrität jo yhdistää palvelimeen\! velocity.error.already-connecting=Yrität jo yhdistää palvelimeen\!
velocity.error.cant-connect={0} ei saatu yhteyttä\: {1} velocity.error.cant-connect=<arg:0> ei saatu yhteyttä\: <arg:1>
velocity.error.connecting-server-error=Yhteyttä palvelimeen {0} ei voitu muodostaa. Yritä myöhemmin uudelleen. velocity.error.connecting-server-error=Yhteyttä palvelimeen <arg:0> ei voitu muodostaa. Yritä myöhemmin uudelleen.
velocity.error.connected-server-error=Yhteydessäsi palvelimeen {0} tapahtui virhe. velocity.error.connected-server-error=Yhteydessäsi palvelimeen <arg:0> tapahtui virhe.
velocity.error.internal-server-connection-error=Tapahtui palvelimen sisäinen yhteysvirhe. velocity.error.internal-server-connection-error=Tapahtui palvelimen sisäinen yhteysvirhe.
velocity.error.logging-in-too-fast=Kirjaudut sisään liian nopeasti, yritä uudelleen hetken kuluttua. velocity.error.logging-in-too-fast=Kirjaudut sisään liian nopeasti, yritä uudelleen hetken kuluttua.
velocity.error.online-mode-only=Et ole kirjautuneena Minecraft-tilillesi. Jos olet kirjautuneena sisään, yritä pelin uudelleenkäynnistämistä. velocity.error.online-mode-only=Et ole kirjautuneena Minecraft-tilillesi. Jos olet kirjautuneena sisään, yritä pelin uudelleenkäynnistämistä.
velocity.error.player-connection-error=Yhteydessäsi tapahtui sisäinen virhe. velocity.error.player-connection-error=Yhteydessäsi tapahtui sisäinen virhe.
velocity.error.modern-forwarding-needs-new-client=Tämä palvelin on yhteensopiva vain Minecraft 1.13\:n ja sitä uudempien versioiden kanssa. velocity.error.modern-forwarding-needs-new-client=Tämä palvelin on yhteensopiva vain Minecraft 1.13\:n ja sitä uudempien versioiden kanssa.
velocity.error.modern-forwarding-failed=Valitsemasi palvelin ei lähettänyt välityspyyntöä välityspalvelimelle. Tarkista että palvelin on määritetty oikein Velocityä varten. velocity.error.modern-forwarding-failed=Valitsemasi palvelin ei lähettänyt välityspyyntöä välityspalvelimelle. Tarkista että palvelin on määritetty oikein Velocityä varten.
velocity.error.moved-to-new-server=Sinut potkittiin pois palvelimelta {0}\: {1} velocity.error.moved-to-new-server=Sinut potkittiin pois palvelimelta <arg:0>\: <arg:1>
velocity.error.no-available-servers=Yhtään palvelinta ei ole tällä hetkellä saatavilla. Yritä myöhemmin uudelleen tai ota yhteyttä palvelimen ylläpitäjään. velocity.error.no-available-servers=Yhtään palvelinta ei ole tällä hetkellä saatavilla. Yritä myöhemmin uudelleen tai ota yhteyttä palvelimen ylläpitäjään.
velocity.error.illegal-chat-characters=Kiellettyjä merkkejä chatissa velocity.error.illegal-chat-characters=Kiellettyjä merkkejä chatissa
# Commands # Commands
velocity.command.generic-error=Tämän komennon suorittamisessa tapahtui virhe. velocity.command.generic-error=Tämän komennon suorittamisessa tapahtui virhe.
velocity.command.command-does-not-exist=Tuota komentoa ei ole olemassa. velocity.command.command-does-not-exist=Tuota komentoa ei ole olemassa.
velocity.command.players-only=Vain pelaajat voivat käyttää tuota komentoa. velocity.command.players-only=Vain pelaajat voivat käyttää tuota komentoa.
velocity.command.server-does-not-exist=Palvelinta {0} ei ole olemassa. velocity.command.server-does-not-exist=Palvelinta <arg:0> ei ole olemassa.
velocity.command.player-not-found=Annettua pelaajaa {0} ei ole olemassa. velocity.command.player-not-found=Annettua pelaajaa <arg:0> ei ole olemassa.
velocity.command.server-current-server=Olet tällä hetkellä yhdistettynä palvelimeen {0}. velocity.command.server-current-server=Olet tällä hetkellä yhdistettynä palvelimeen <arg:0>.
velocity.command.server-too-many=Liian monta palvelinta on määritetty. Paina Tab -näppäintä nähdäksesi kaikki saatavilla olevat palvelimet. velocity.command.server-too-many=Liian monta palvelinta on määritetty. Paina Tab -näppäintä nähdäksesi kaikki saatavilla olevat palvelimet.
velocity.command.server-available=Saatavilla olevat palvelimet\: velocity.command.server-available=Saatavilla olevat palvelimet\:
velocity.command.server-tooltip-player-online={0} pelaaja paikalla velocity.command.server-tooltip-player-online=<arg:0> pelaaja paikalla
velocity.command.server-tooltip-players-online={0} pelaajaa paikalla velocity.command.server-tooltip-players-online=<arg:0> pelaajaa paikalla
velocity.command.server-tooltip-current-server=Tällä hetkellä yhdistetty tähän palvelimeen velocity.command.server-tooltip-current-server=Tällä hetkellä yhdistetty tähän palvelimeen
velocity.command.server-tooltip-offer-connect-server=Napsauta yhdistääksesi tähän palvelimeen velocity.command.server-tooltip-offer-connect-server=Napsauta yhdistääksesi tähän palvelimeen
velocity.command.glist-player-singular={0} pelaaja on tällä hetkellä yhdistänyt välityspalvelimelle. velocity.command.glist-player-singular=<arg:0> pelaaja on tällä hetkellä yhdistänyt välityspalvelimelle.
velocity.command.glist-player-plural={0} pelaajaa on tällä hetkellä yhdistänyt välityspalvelimelle. velocity.command.glist-player-plural=<arg:0> pelaajaa on tällä hetkellä yhdistänyt välityspalvelimelle.
velocity.command.glist-view-all=Nähdäksesi pelaajat kaikilla palvelimilla, käytä komentoa /glist all. velocity.command.glist-view-all=Nähdäksesi pelaajat kaikilla palvelimilla, käytä komentoa /glist all.
velocity.command.reload-success=Velocityn konfiguraatio uudelleenladattiin onnistuneesti. velocity.command.reload-success=Velocityn konfiguraatio uudelleenladattiin onnistuneesti.
velocity.command.reload-failure=Velocityn konfiguraation uudelleenlataus epäonnistui. Katso tarkemmat lisätiedot konsolista. velocity.command.reload-failure=Velocityn konfiguraation uudelleenlataus epäonnistui. Katso tarkemmat lisätiedot konsolista.
velocity.command.version-copyright=Tekijänoikeus 2018-{2} {0}. {1} on lisensoitu GNU General Public License v3\:n ehtojen mukaisesti. velocity.command.version-copyright=Tekijänoikeus 2018-<arg:2> <arg:0>. <arg:1> on lisensoitu GNU General Public License v3\:n ehtojen mukaisesti.
velocity.command.no-plugins=Yhtäkään pluginia ei ole asennettu. velocity.command.no-plugins=Yhtäkään pluginia ei ole asennettu.
velocity.command.plugins-list=Pluginit\: {0} velocity.command.plugins-list=Pluginit\: <arg:0>
velocity.command.plugin-tooltip-website=Verkkosivu\: {0} velocity.command.plugin-tooltip-website=Verkkosivu\: <arg:0>
velocity.command.plugin-tooltip-author=Tekijä\: {0} velocity.command.plugin-tooltip-author=Tekijä\: <arg:0>
velocity.command.plugin-tooltip-authors=Tekijät\: {0} velocity.command.plugin-tooltip-authors=Tekijät\: <arg:0>
velocity.command.dump-uploading=Lähetetään kerättyjä tietoja... velocity.command.dump-uploading=Lähetetään kerättyjä tietoja...
velocity.command.dump-send-error=Velocity-palvelimien kanssa kommunikoidessa tapahtui virhe. Palvelimet eivät ehkä ole tilapäisesti käytettävissä tai verkkoasetuksissa on ongelma. Löydät lisätietoja Velocity-palvelimesi lokista tai konsolista. velocity.command.dump-send-error=Velocity-palvelimien kanssa kommunikoidessa tapahtui virhe. Palvelimet eivät ehkä ole tilapäisesti käytettävissä tai verkkoasetuksissa on ongelma. Löydät lisätietoja Velocity-palvelimesi lokista tai konsolista.
velocity.command.dump-success=Luotiin anonyymi raportti, joka sisältää hyödyllistä tietoa tästä välityspalvelimesta. Jos jokin kehittäjä on pyytänyt sitä, voit jakaa seuraavan linkin heidän kanssaan\: velocity.command.dump-success=Luotiin anonyymi raportti, joka sisältää hyödyllistä tietoa tästä välityspalvelimesta. Jos jokin kehittäjä on pyytänyt sitä, voit jakaa seuraavan linkin heidän kanssaan\:

Some files were not shown because too many files have changed in this diff Show More