Compare commits

..
3 Commits
Author SHA1 Message Date
Chaoscaot 47f36e3ff9 Merge remote-tracking branch 'upstream/dev/3.0.0' into updatev2
SteamWarCI Build successful
2025-06-26 22:52:31 +02:00
Chaoscaot 75bb48d00e Merge remote-tracking branch 'upstream/dev/3.0.0' into updatev2
SteamWarCI Build successful
2025-06-03 23:12:58 +02:00
Chaoscaot 11834de220 Revert "Disable io_uring transport by default"
SteamWarCI Build successful
This reverts commit ae312339a3.
2025-05-02 20:49:11 +02:00
180 changed files with 1605 additions and 4206 deletions
+7 -7
View File
@@ -6,18 +6,18 @@ on: [push, pull_request]
jobs: jobs:
build: build:
runs-on: ubuntu-24.04 runs-on: ubuntu-22.04
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@v6 uses: actions/checkout@v4
with: with:
persist-credentials: false persist-credentials: false
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v5 uses: gradle/actions/setup-gradle@v4
- name: Set up JDK 21 - name: Set up JDK 17
uses: actions/setup-java@v5 uses: actions/setup-java@v4
with: with:
java-version: 21 java-version: 17
distribution: 'zulu' distribution: 'temurin'
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew build run: ./gradlew build
-6
View File
@@ -34,9 +34,3 @@ 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.
+7 -5
View File
@@ -59,16 +59,18 @@ tasks {
val o = options as StandardJavadocDocletOptions val o = options as StandardJavadocDocletOptions
o.encoding = "UTF-8" o.encoding = "UTF-8"
o.source = "25" o.source = "17"
o.use() o.use()
o.links( o.links(
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/", "https://www.slf4j.org/apidocs/",
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/", "https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
"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/25/docs/api/", "https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jd.papermc.io/adventure/${libs.adventure.bom.get().version}/", "https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/", "https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
"https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
"https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
) )
o.tags( o.tags(
@@ -14,6 +14,7 @@ import com.velocitypowered.api.plugin.Plugin;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.io.Writer; import java.io.Writer;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.ProcessingEnvironment;
@@ -67,8 +68,8 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
Name qualifiedName = ((TypeElement) element).getQualifiedName(); Name qualifiedName = ((TypeElement) element).getQualifiedName();
if (pluginClassFound != null) { if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
if (!pluginClassFound.equals(qualifiedName.toString()) && !warnedAboutMultiplePlugins) { if (!warnedAboutMultiplePlugins) {
environment.getMessager() environment.getMessager()
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support " .printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
+ "multiple plugins. We are using " + pluginClassFound + "multiple plugins. We are using " + pluginClassFound
@@ -24,8 +24,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/ */
public final class SerializedPluginDescription { public final class SerializedPluginDescription {
public static final String ID_PATTERN_STRING = "[a-z][a-z0-9-_]{0,63}"; public static final Pattern ID_PATTERN = Pattern.compile("[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.papermc.io/adventure/minimessage/format/">MiniMessage docs</a> * @see <a href="https://docs.advntr.dev/minimessage/format.html">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.papermc.io/adventure/minimessage/">MiniMessage docs</a> * @see <a href="https://docs.advntr.dev/minimessage/">MiniMessage docs</a>
* and <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a> * and <a href="https://docs.advntr.dev/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
@@ -60,8 +60,7 @@ public interface EventManager {
* *
* @param plugin the plugin to associate with the handler * @param plugin the plugin to associate with the handler
* @param eventClass the class for the event handler to register * @param eventClass the class for the event handler to register
* @param postOrder the relative order in which events should be posted to the handler. The higher * @param postOrder the relative order in which events should be posted to the handler
* the priority, the earlier the event handler will be called
* @param handler the handler to register * @param handler the handler to register
* @param <E> the event type to handle * @param <E> the event type to handle
*/ */
@@ -1,11 +0,0 @@
/*
* Copyright (C) 2018 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.
*/
/**
* Provides events for handling command execution.
*/
package com.velocitypowered.api.event.command;
@@ -11,7 +11,6 @@ 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.
@@ -23,24 +22,10 @@ import org.checkerframework.checker.nullness.qual.Nullable;
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();
} }
@@ -48,16 +33,6 @@ 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;
@@ -1,44 +0,0 @@
/*
* 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 connections * @return a result to deny conneections
*/ */
public static ServerResult denied() { public static ServerResult denied() {
return DENIED; return DENIED;
@@ -1,11 +0,0 @@
/*
* Copyright (C) 2018 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.
*/
/**
* Provides events for handling the player configuration phase.
*/
package com.velocitypowered.api.event.player.configuration;
@@ -1,33 +0,0 @@
/*
* Copyright (C) 2018-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.proxy;
import com.google.common.annotations.Beta;
import com.velocitypowered.api.event.annotation.AwaitingEvent;
/**
* This event is fired by the proxy after it has stopped accepting new connections,
* but before players are disconnected.
* This is the last point at which you can interact with currently connected players,
* for example to transfer them to another proxy or perform other cleanup tasks.
*
* @implNote Velocity will wait for all event listeners to complete before disconnecting players,
* but note that the event will time out after the configured value of the
* <code>velocity.pre-shutdown-timeout</code> system property, default 10 seconds,
* in seconds to prevent shutdown from hanging indefinitely
* @since 3.4.0
*/
@Beta
@AwaitingEvent
public final class ProxyPreShutdownEvent {
@Override
public String toString() {
return "ProxyPreShutdownEvent";
}
}
@@ -1,11 +0,0 @@
/*
* Copyright (C) 2018 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.
*/
/**
* Provides events for handling registration of servers on the proxy.
*/
package com.velocitypowered.api.event.proxy.server;
@@ -91,12 +91,7 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"), MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
MINECRAFT_1_21_4(769, "1.21.4"), MINECRAFT_1_21_4(769, "1.21.4"),
MINECRAFT_1_21_5(770, "1.21.5"), MINECRAFT_1_21_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_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"),
MINECRAFT_26_2(776, "26.2");
private static final int SNAPSHOT_BIT = 30; private static final int SNAPSHOT_BIT = 30;
@@ -7,11 +7,9 @@
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.
@@ -26,7 +24,6 @@ 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,12 +7,10 @@
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.
@@ -28,7 +26,6 @@ public @interface Plugin {
* *
* @return the ID for this plugin * @return the ID for this plugin
*/ */
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
String id(); String id();
/** /**
@@ -29,7 +29,6 @@ import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
import net.kyori.adventure.dialog.DialogLike;
import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.inventory.Book; import net.kyori.adventure.inventory.Book;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
@@ -39,7 +38,6 @@ 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;
@@ -50,8 +48,7 @@ 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 {
PlayerHeadObjectContents.SkinSource {
/** /**
* Returns the player's current username. * Returns the player's current username.
@@ -197,7 +194,7 @@ public interface Player extends
* *
* @param reason component with the reason * @param reason component with the reason
*/ */
void disconnect(@NotNull Component reason); void disconnect(Component reason);
/** /**
* Sends chat input onto the players current server as if they typed it into the client chat box. * Sends chat input onto the players current server as if they typed it into the client chat box.
@@ -338,15 +335,6 @@ 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.
@@ -395,12 +383,8 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* * <b>This method is not currently implemented in Velocity
* @apiNote <b>This method is not currently implemented in Velocity * and will not perform any actions.</b>
* and will not perform any actions.</b>
* @see #playSound(Sound, Sound.Emitter)
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/ */
@Override @Override
default void playSound(@NotNull Sound sound) { default void playSound(@NotNull Sound sound) {
@@ -409,11 +393,8 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* @apiNote <b>This method is not currently implemented in Velocity * <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b> * and will not perform any actions.</b>
* @see #playSound(Sound, Sound.Emitter)
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/ */
@Override @Override
default void playSound(@NotNull Sound sound, double x, double y, double z) { default void playSound(@NotNull Sound sound, double x, double y, double z) {
@@ -422,28 +403,18 @@ public interface Player extends
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* <p><b>Note</b>: Due to <a href="https://bugs.mojang.com/browse/MC/issues/MC-146721">MC-146721</a>, stereo sounds are always played globally in 1.14+. * <b>This method is not currently implemented in Velocity
* * and will not perform any actions.</b>
* <p><b>Note</b>: Due to <a href="https://bugs.mojang.com/browse/MC/issues/MC-138832">MC-138832</a>, the volume and pitch are ignored when using this method in 1.14 to 1.16.5.
*
* @param sound the sound to play
* @param emitter the emitter of the sound; may be another player of this player's server
* @since 3.4.0
* @sinceMinecraft 1.19.3
* @apiNote This method is currently only implemented for players on 1.19.3+
* and requires a present {@link #getCurrentServer} for the emitting player as well as this player.
*/ */
@Override @Override
default void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) { default void playSound(@NotNull Sound sound, Sound.Emitter emitter) {
} }
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* @param stop the sound and/or a sound source, to stop * <b>This method is not currently implemented in Velocity
* @since 3.4.0 * and will not perform any actions.</b>
* @sinceMinecraft 1.19.3
* @apiNote This method is currently only implemented for players on 1.19.3+.
*/ */
@Override @Override
default void stopSound(@NotNull SoundStop stop) { default void stopSound(@NotNull SoundStop stop) {
@@ -454,40 +425,11 @@ public interface Player extends
* *
* <b>This method is not currently implemented in Velocity * <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b> * and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/ */
@Override @Override
default void openBook(@NotNull Book book) { default void openBook(@NotNull Book book) {
} }
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void showDialog(@NotNull DialogLike dialog) {
}
/**
* {@inheritDoc}
*
* <b>This method is not currently implemented in Velocity
* and will not perform any actions.</b>
*
* @see <a href="https://docs.papermc.io/velocity/dev/pitfalls/#audience-operations-are-not-fully-supported">
* Unsupported Adventure Operations</a>
*/
@Override
default void closeDialog() {
}
/** /**
* Transfers a Player to a host. * Transfers a Player to a host.
* *
@@ -19,9 +19,7 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.Nullable;
import org.jspecify.annotations.Nullable;
/** /**
* Represents a 1.7 and above server list ping response. This class is immutable. * Represents a 1.7 and above server list ping response. This class is immutable.
@@ -30,7 +28,7 @@ public final class ServerPing {
private final Version version; private final Version version;
private final @Nullable Players players; private final @Nullable Players players;
private final @Nullable Component description; private final net.kyori.adventure.text.Component description;
private final @Nullable Favicon favicon; private final @Nullable Favicon favicon;
private final @Nullable ModInfo modinfo; private final @Nullable ModInfo modinfo;
@@ -49,8 +47,8 @@ public final class ServerPing {
* @param modinfo the mods this server runs * @param modinfo the mods this server runs
*/ */
public ServerPing(Version version, @Nullable Players players, public ServerPing(Version version, @Nullable Players players,
Component description, @Nullable Favicon favicon, net.kyori.adventure.text.Component description, @Nullable Favicon favicon,
@Nullable ModInfo modinfo) { @Nullable ModInfo modinfo) {
this.version = Preconditions.checkNotNull(version, "version"); this.version = Preconditions.checkNotNull(version, "version");
this.players = players; this.players = players;
this.description = Preconditions.checkNotNull(description, "description"); this.description = Preconditions.checkNotNull(description, "description");
@@ -66,8 +64,7 @@ public final class ServerPing {
return Optional.ofNullable(players); return Optional.ofNullable(players);
} }
@Nullable public net.kyori.adventure.text.Component getDescriptionComponent() {
public Component getDescriptionComponent() {
return description; return description;
} }
@@ -154,7 +151,7 @@ public final class ServerPing {
private final List<SamplePlayer> samplePlayers = new ArrayList<>(); private final List<SamplePlayer> samplePlayers = new ArrayList<>();
private String modType = "FML"; private String modType = "FML";
private final List<ModInfo.Mod> mods = new ArrayList<>(); private final List<ModInfo.Mod> mods = new ArrayList<>();
private Component description; private net.kyori.adventure.text.Component description;
private @Nullable Favicon favicon; private @Nullable Favicon favicon;
private boolean nullOutPlayers; private boolean nullOutPlayers;
private boolean nullOutModinfo; private boolean nullOutModinfo;
@@ -302,7 +299,7 @@ public final class ServerPing {
* @param description Component to use as the description. * @param description Component to use as the description.
* @return this builder, for chaining * @return this builder, for chaining
*/ */
public Builder description(Component description) { public Builder description(net.kyori.adventure.text.Component description) {
this.description = Preconditions.checkNotNull(description, "description"); this.description = Preconditions.checkNotNull(description, "description");
return this; return this;
} }
@@ -362,7 +359,7 @@ public final class ServerPing {
return samplePlayers; return samplePlayers;
} }
public Optional<Component> getDescriptionComponent() { public Optional<net.kyori.adventure.text.Component> getDescriptionComponent() {
return Optional.ofNullable(description); return Optional.ofNullable(description);
} }
@@ -11,14 +11,11 @@ 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 implements PlayerHeadObjectContents.SkinSource { public final class GameProfile {
private final UUID id; private final UUID id;
private final String undashedId; private final String undashedId;
@@ -172,23 +169,6 @@ public final class GameProfile implements PlayerHeadObjectContents.SkinSource {
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{"
@@ -2,15 +2,8 @@ import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
// This interface is needed as a workaround to get an instance of ExecOperations
interface Injected {
@get:Inject
val execOps: ExecOperations
}
val currentShortRevision = ByteArrayOutputStream().use { val currentShortRevision = ByteArrayOutputStream().use {
val execOps = objects.newInstance<Injected>().execOps exec {
execOps.exec {
executable = "git" executable = "git"
args = listOf("rev-parse", "HEAD") args = listOf("rev-parse", "HEAD")
standardOutput = it standardOutput = it
@@ -8,10 +8,10 @@ extensions.configure<PublishingExtension> {
maven { maven {
credentials(PasswordCredentials::class.java) credentials(PasswordCredentials::class.java)
name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined name = "paper"
val base = "https://artifactory.papermc.io/artifactory" val base = "https://repo.papermc.io/repository/maven"
val releasesRepoUrl = "$base/releases/" val releasesRepoUrl = "$base-releases/"
val snapshotsRepoUrl = "$base/snapshots/" val snapshotsRepoUrl = "$base-snapshots/"
setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl) setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
} }
} }
+6 -6
View File
@@ -12,7 +12,7 @@ subprojects {
java { java {
toolchain { toolchain {
languageVersion.set(JavaLanguageVersion.of(25)) languageVersion.set(JavaLanguageVersion.of(17))
} }
} }
@@ -20,11 +20,11 @@ subprojects {
testImplementation(rootProject.libs.junit) testImplementation(rootProject.libs.junit)
} }
testing.suites.named<JvmTestSuite>("test") { tasks {
useJUnitJupiter() test {
targets.all { useJUnitPlatform()
testTask.configure { reports {
reports.junitXml.required = true junitXml.required.set(true)
} }
} }
} }
+1 -1
View File
@@ -1,2 +1,2 @@
group=com.velocitypowered group=com.velocitypowered
version=4.0.0-SNAPSHOT version=3.4.0-SNAPSHOT
+26 -25
View File
@@ -1,25 +1,26 @@
[versions] [versions]
configurate3 = "3.7.3" configurate3 = "3.7.3"
configurate4 = "4.2.0" configurate4 = "4.1.2"
flare = "2.0.1" flare = "2.0.1"
log4j = "2.26.0" log4j = "2.24.3"
netty = "4.2.15.Final" netty = "4.2.1.Final"
[plugins] [plugins]
fill = "io.papermc.fill.gradle:1.0.12" indra-publishing = "net.kyori.indra.publishing:2.0.6"
shadow = "com.gradleup.shadow:9.5.1" shadow = "io.github.goooler.shadow:8.1.5"
spotless = "com.diffplug.spotless:8.2.0" spotless = "com.diffplug.spotless:6.25.0"
[libraries] [libraries]
adventure-bom = "net.kyori:adventure-bom:5.2.0" adventure-bom = "net.kyori:adventure-bom:4.21.0"
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:5.2.0" adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.21.0"
asm = "org.ow2.asm:asm:9.9.1" adventure-facet = "net.kyori:adventure-platform-facet:4.3.4"
auto-service = "com.google.auto.service:auto-service:1.1.1" asm = "org.ow2.asm:asm:9.8"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1" auto-service = "com.google.auto.service:auto-service:1.0.1"
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
bstats = "org.bstats:bstats-base:3.1.0" bstats = "org.bstats:bstats-base:3.0.3"
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3" caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
checker-qual = "org.checkerframework:checker-qual:3.53.0" checker-qual = "org.checkerframework:checker-qual:3.42.0"
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
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" }
@@ -29,24 +30,24 @@ configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.r
configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" } configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" }
configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" }
disruptor = "com.lmax:disruptor:4.0.0" disruptor = "com.lmax:disruptor:4.0.0"
fastutil = "it.unimi.dsi:fastutil:8.5.18" 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.6" jline = "org.jline:jline-terminal-jansi:3.30.2"
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4" jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
junit = "org.junit.jupiter:junit-jupiter:6.0.3" junit = "org.junit.jupiter:junit-jupiter:5.10.2"
jspecify = "org.jspecify:jspecify:1.0.0" jspecify = "org.jspecify:jspecify:0.3.0"
kyori-ansi = "net.kyori:ansi:1.1.1" kyori-ansi = "net.kyori:ansi:1.1.1"
guava = "com.google.guava:guava:33.6.0-jre" guava = "com.google.guava:guava:25.1-jre"
gson = "com.google.code.gson:gson:2.14.0" gson = "com.google.code.gson:gson:2.10.1"
guice = "com.google.inject:guice:7.0.0" guice = "com.google.inject:guice:6.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.22.0" mockito = "org.mockito:mockito-core:5.10.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" }
@@ -54,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.8.3" nightconfig = "com.electronwill.night-config:toml:3.6.7"
slf4j = "org.slf4j:slf4j-api:2.0.17" slf4j = "org.slf4j:slf4j-api:2.0.17"
snakeyaml = "org.yaml:snakeyaml:2.5" snakeyaml = "org.yaml:snakeyaml:1.33"
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8" spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
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-9.6.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
Vendored
+10 -6
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright © 2015 the original authors. # Copyright © 2015-2021 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,7 +86,8 @@ 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\n' "$PWD" ) || exit APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$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
@@ -114,6 +115,7 @@ 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.
@@ -171,6 +173,7 @@ 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" )
@@ -200,17 +203,18 @@ 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='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * DEFAULT_JVM_OPTS, JAVA_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" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.
@@ -245,4 +249,4 @@ eval "set -- $(
tr '\n' ' ' tr '\n' ' '
)" '"$@"' )" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"
Vendored
+4 -3
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="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
@@ -70,10 +70,11 @@ 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%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
@@ -90,4 +91,4 @@ exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
:omega :omega
+3 -26
View File
@@ -1,16 +1,14 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
import io.papermc.fill.model.BuildChannel
plugins { plugins {
application application
id("velocity-init-manifest") id("velocity-init-manifest")
alias(libs.plugins.shadow) alias(libs.plugins.shadow)
alias(libs.plugins.fill)
} }
application { application {
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,10 +25,6 @@ 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
@@ -114,27 +108,10 @@ tasks {
} }
} }
val projectVersion = version as String
fill {
project("velocity")
build {
channel = BuildChannel.STABLE
versionFamily("3.0.0")
version(projectVersion)
downloads {
register("server:default") {
file = tasks.shadowJar.flatMap { it.archiveFile }
nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" }
}
}
}
}
dependencies { dependencies {
implementation(project(":velocity-api")) implementation(project(":velocity-api"))
implementation(project(":velocity-native")) implementation(project(":velocity-native"))
implementation(project(":velocity-proxy-log4j2-plugin"))
implementation(libs.bundles.log4j) implementation(libs.bundles.log4j)
implementation(libs.kyori.ansi) implementation(libs.kyori.ansi)
@@ -159,6 +136,7 @@ dependencies {
implementation(libs.fastutil) implementation(libs.fastutil)
implementation(platform(libs.adventure.bom)) implementation(platform(libs.adventure.bom))
implementation(libs.adventure.text.serializer.json.legacy.impl) implementation(libs.adventure.text.serializer.json.legacy.impl)
implementation(libs.adventure.facet)
implementation(libs.completablefutures) implementation(libs.completablefutures)
implementation(libs.nightconfig) implementation(libs.nightconfig)
implementation(libs.bstats) implementation(libs.bstats)
@@ -170,5 +148,4 @@ dependencies {
testImplementation(libs.mockito) testImplementation(libs.mockito)
annotationProcessor(libs.auto.service) annotationProcessor(libs.auto.service)
annotationProcessor(libs.log4j.core)
} }
+4
View File
@@ -0,0 +1,4 @@
dependencies {
implementation(libs.bundles.log4j)
annotationProcessor(libs.log4j.core)
}
@@ -47,6 +47,11 @@ public class Velocity {
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir")); System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir"));
} }
// Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator
if (System.getProperty("io.netty.allocator.type") == null) {
System.setProperty("io.netty.allocator.type", "pooled");
}
// Disable the resource leak detector by default as it reduces performance. Allow the user to // Disable the resource leak detector by default as it reduces performance. Allow the user to
// override this if desired. // override this if desired.
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) { if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {
@@ -24,7 +24,6 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.BrigadierCommand;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyPreShutdownEvent;
import com.velocitypowered.api.event.proxy.ProxyReloadEvent; import com.velocitypowered.api.event.proxy.ProxyReloadEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.network.ProtocolVersion;
@@ -82,6 +81,7 @@ 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;
@@ -104,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;
@@ -119,7 +119,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
*/ */
public class VelocityServer implements ProxyServer, ForwardingAudience { public class VelocityServer implements ProxyServer, ForwardingAudience {
public static final String VELOCITY_URL = "https://papermc.io/software/velocity"; public static final String VELOCITY_URL = "https://velocitypowered.com";
private static final Logger logger = LogManager.getLogger(VelocityServer.class); private static final Logger logger = LogManager.getLogger(VelocityServer.class);
public static final Gson GENERAL_GSON = new GsonBuilder() public static final Gson GENERAL_GSON = new GsonBuilder()
@@ -150,8 +150,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
) )
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE) .registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
.create(); .create();
private static final int PRE_SHUTDOWN_TIMEOUT =
Integer.getInteger("velocity.pre-shutdown-timeout", 10);
private final ConnectionManager cm; private final ConnectionManager cm;
private final ProxyOptions options; private final ProxyOptions options;
@@ -165,8 +163,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>(); private final Map<UUID, ConnectedPlayer> connectionsByUuid = new ConcurrentHashMap<>();
private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>(); private final Map<String, ConnectedPlayer> connectionsByName = new ConcurrentHashMap<>();
private final Object sessionIdLock = new Object();
private volatile @Nullable UUID sessionId;
private final VelocityConsole console; private final VelocityConsole console;
private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter; private @MonotonicNonNull Ratelimiter<InetAddress> ipAttemptLimiter;
private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter; private @MonotonicNonNull Ratelimiter<UUID> commandRateLimiter;
@@ -220,8 +216,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
ProxyVersion version = getVersion(); ProxyVersion version = getVersion();
PluginDescription description = new VelocityPluginDescription( PluginDescription description = new VelocityPluginDescription(
"velocity", version.getName(), version.getVersion(), "The Velocity proxy", "velocity", version.getName(), version.getVersion(), "The Velocity proxy",
version.getName().equals("Velocity") ? VELOCITY_URL : null, VELOCITY_URL, ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
VelocityPluginContainer container = new VelocityPluginContainer(description); VelocityPluginContainer container = new VelocityPluginContainer(description);
container.setInstance(VelocityVirtualPlugin.INSTANCE); container.setInstance(VelocityVirtualPlugin.INSTANCE);
return container; return container;
@@ -243,6 +238,8 @@ 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...
// //
@@ -291,8 +288,6 @@ 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);
} }
@@ -343,8 +338,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
} }
private void registerTranslations() { private void registerTranslations() {
final MiniMessageTranslationStore translationRegistry = final TranslationStore.StringBased<MessageFormat> translationRegistry =
MiniMessageTranslationStore.create(Key.key("velocity", "translations")); TranslationStore.messageFormat(Key.key("velocity", "translations"));
translationRegistry.defaultLocale(Locale.US); translationRegistry.defaultLocale(Locale.US);
try { try {
ResourceUtils.visitResources(VelocityServer.class, path -> { ResourceUtils.visitResources(VelocityServer.class, path -> {
@@ -583,20 +578,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
// done first to refuse new connections // done first to refuse new connections
cm.shutdown(); cm.shutdown();
try {
eventManager.fire(new ProxyPreShutdownEvent())
.toCompletableFuture()
.get(PRE_SHUTDOWN_TIMEOUT, TimeUnit.SECONDS);
} catch (TimeoutException ignored) {
logger.warn("Your plugins took over {} seconds during pre shutdown.",
PRE_SHUTDOWN_TIMEOUT);
} catch (ExecutionException ee) {
logger.error("Exception in ProxyPreShutdownEvent handler; continuing shutdown.", ee);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
logger.warn("Interrupted while waiting for ProxyPreShutdownEvent; continuing shutdown.");
}
ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values()); ImmutableList<ConnectedPlayer> players = ImmutableList.copyOf(connectionsByUuid.values());
for (ConnectedPlayer player : players) { for (ConnectedPlayer player : players) {
player.disconnect(reason); player.disconnect(reason);
@@ -745,36 +726,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection); connectionsByName.remove(connection.getUsername().toLowerCase(Locale.US), connection);
connectionsByUuid.remove(connection.getUniqueId(), connection); connectionsByUuid.remove(connection.getUniqueId(), connection);
connection.disconnected(); connection.disconnected();
if (this.sessionId != null && connectionsByUuid.isEmpty()) {
synchronized (this.sessionIdLock) {
if (connectionsByUuid.isEmpty()) {
this.sessionId = null;
}
}
}
}
/**
* Returns the metrics session ID for this proxy, generating one if none is currently active. The
* ID is shared by every player connected during a populated period and is regenerated once the
* proxy empties.
*
* @return the current session ID
*/
public UUID getSessionId() {
UUID uuid = this.sessionId;
if (uuid != null) {
return uuid;
}
synchronized (this.sessionIdLock) {
uuid = this.sessionId;
if (uuid == null) {
uuid = UUID.randomUUID();
this.sessionId = uuid;
}
return uuid;
}
} }
@Override @Override
@@ -866,7 +817,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();
@@ -53,23 +53,15 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
viewer.getProtocolVersion(), viewer.getProtocolVersion(),
viewer.translateMessage(this.bar.name()) viewer.translateMessage(this.bar.name())
); );
viewer.getBossBarManager().writeUpdate(this, BossBarPacket.createAddPacket(this.id, this.bar, name)); viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
return true; return true;
} }
return false; return false;
} }
public void createDirect(final ConnectedPlayer viewer) {
final ComponentHolder name = new ComponentHolder(
viewer.getProtocolVersion(),
viewer.translateMessage(this.bar.name())
);
viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
}
public boolean viewerRemove(final ConnectedPlayer viewer) { public boolean viewerRemove(final ConnectedPlayer viewer) {
if (this.viewers.remove(viewer)) { if (this.viewers.remove(viewer)) {
viewer.getBossBarManager().remove(this, BossBarPacket.createRemovePacket(this.id, this.bar)); viewer.getConnection().write(BossBarPacket.createRemovePacket(this.id, this.bar));
return true; return true;
} }
return false; return false;
@@ -92,7 +84,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
this.bar, this.bar,
new ComponentHolder(viewer.getProtocolVersion(), translated) new ComponentHolder(viewer.getProtocolVersion(), translated)
); );
viewer.getBossBarManager().writeUpdate(this, packet); viewer.getConnection().write(packet);
} }
} }
@@ -104,7 +96,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) { ) {
final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar); final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { for (final ConnectedPlayer viewer : this.viewers) {
viewer.getBossBarManager().writeUpdate(this, packet); viewer.getConnection().write(packet);
} }
} }
@@ -116,7 +108,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) { ) {
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar); final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { for (final ConnectedPlayer viewer : this.viewers) {
viewer.getBossBarManager().writeUpdate(this, packet); viewer.getConnection().write(packet);
} }
} }
@@ -128,7 +120,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) { ) {
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar); final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { for (final ConnectedPlayer viewer : this.viewers) {
viewer.getBossBarManager().writeUpdate(this, packet); viewer.getConnection().write(packet);
} }
} }
@@ -140,7 +132,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
) { ) {
final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar); final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar);
for (final ConnectedPlayer viewer : this.viewers) { for (final ConnectedPlayer viewer : this.viewers) {
viewer.getBossBarManager().writeUpdate(this, packet); viewer.getConnection().write(packet);
} }
} }
} }
@@ -344,7 +344,7 @@ final class SuggestionsProvider<S> {
return 0; return 0;
}); });
} }
return potentials.getFirst(); return potentials.get(0);
} }
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap()); return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
} }
@@ -35,6 +35,7 @@ import com.velocitypowered.api.command.CommandManager;
import com.velocitypowered.api.command.CommandMeta; import com.velocitypowered.api.command.CommandMeta;
import com.velocitypowered.api.command.CommandResult; import com.velocitypowered.api.command.CommandResult;
import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.command.VelocityBrigadierMessage;
import com.velocitypowered.api.event.command.CommandExecuteEvent; import com.velocitypowered.api.event.command.CommandExecuteEvent;
import com.velocitypowered.api.event.command.PostCommandInvocationEvent; import com.velocitypowered.api.event.command.PostCommandInvocationEvent;
import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.plugin.PluginManager;
@@ -58,7 +59,6 @@ import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.checkerframework.checker.lock.qual.GuardedBy; import org.checkerframework.checker.lock.qual.GuardedBy;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
@@ -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.getFirst(), command, meta); this.internalRegister(commandRegistrars.get(0), command, meta);
} }
} }
@@ -242,8 +242,8 @@ public class VelocityCommandManager implements CommandManager {
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand()); CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
if (isSyntaxError) { if (isSyntaxError) {
final Message message = e.getRawMessage(); final Message message = e.getRawMessage();
if (message instanceof ComponentLike componentLike) { if (message instanceof VelocityBrigadierMessage velocityMessage) {
source.sendMessage(componentLike.asComponent().applyFallbackStyle(NamedTextColor.RED)); source.sendMessage(velocityMessage.asComponent().applyFallbackStyle(NamedTextColor.RED));
} else { } else {
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED)); source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
} }
@@ -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,34 +70,33 @@ public final class VelocityCommands {
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant); maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
} }
return switch (delegate) { if (delegate instanceof LiteralCommandNode<CommandSource> lcn) {
case LiteralCommandNode<CommandSource> lcn -> { var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true); literalBuilder.executes(maybeCommand);
literalBuilder.executes(maybeCommand); // we also need to wrap any children
// we also need to wrap any children for (final CommandNode<CommandSource> child : delegate.getChildren()) {
for (final CommandNode<CommandSource> child : delegate.getChildren()) { literalBuilder.then(wrap(child, registrant));
literalBuilder.then(wrap(child, registrant));
}
if (delegate.getRedirect() != null) {
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
}
yield literalBuilder.build();
} }
case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand) if (delegate.getRedirect() != null) {
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null); literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
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();
} }
default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass()); return literalBuilder.build();
}; } 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
@@ -134,7 +133,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.getFirst().getNode().getName(); return nodes.get(0).getNode().getName();
} }
public static final String ARGS_NODE_NAME = "arguments"; public static final String ARGS_NODE_NAME = "arguments";
@@ -118,12 +118,14 @@ public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, St
if (this == o) { if (this == o) {
return true; return true;
} }
if (!(o instanceof VelocityArgumentCommandNode that)) { if (!(o instanceof VelocityArgumentCommandNode)) {
return false; return false;
} }
if (!super.equals(that)) { if (!super.equals(o)) {
return false; return false;
} }
final VelocityArgumentCommandNode<?, ?> that = (VelocityArgumentCommandNode<?, ?>) o;
return this.type.equals(that.type); return this.type.equals(that.type);
} }
@@ -38,7 +38,6 @@ 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.
@@ -112,7 +111,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(Argument.string("server", serverName))); .arguments(Component.text(serverName)));
return -1; return -1;
} }
sendServerPlayers(source, registeredServer.get(), false); sendServerPlayers(source, registeredServer.get(), false);
@@ -127,8 +126,7 @@ 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(Argument.component( .arguments(Component.text(Integer.toString(online), NamedTextColor.GREEN));
"players", Component.text(Integer.toString(online), NamedTextColor.GREEN)));
target.sendMessage(msg.build()); target.sendMessage(msg.build());
} }
@@ -35,7 +35,6 @@ 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.
@@ -122,7 +121,7 @@ public class SendCommand {
if (maybeServer.isEmpty()) { if (maybeServer.isEmpty()) {
context.getSource().sendMessage( context.getSource().sendMessage(
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Argument.string("server", serverName)) CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName))
); );
return 0; return 0;
} }
@@ -134,7 +133,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(Argument.string("player", player)) CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player))
); );
return 0; return 0;
} }
@@ -37,7 +37,6 @@ 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.
@@ -77,7 +76,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(Argument.string("server", serverName))); .arguments(Component.text(serverName)));
return -1; return -1;
} }
@@ -136,7 +135,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(Argument.component("players", Component.text(connectedPlayers))); playersTextComponent.arguments(Component.text(connectedPlayers));
if (serverInfo.getName().equals(currentPlayerServer)) { if (serverInfo.getName().equals(currentPlayerServer)) {
serverTextComponent.color(NamedTextColor.GREEN) serverTextComponent.color(NamedTextColor.GREEN)
.hoverEvent( .hoverEvent(
@@ -62,7 +62,6 @@ 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;
@@ -166,9 +165,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",
Argument.string("vendor", version.getVendor()), Component.text(version.getVendor()),
Argument.string("name", version.getName()), Component.text(version.getName()),
Argument.component("year", Component.text(LocalDate.now().getYear()))); Component.text(LocalDate.now().getYear()));
source.sendMessage(velocity); source.sendMessage(velocity);
source.sendMessage(copyright); source.sendMessage(copyright);
@@ -177,7 +176,8 @@ public final class VelocityCommand {
.append(Component.text() .append(Component.text()
.content("PaperMC") .content("PaperMC")
.color(NamedTextColor.GREEN) .color(NamedTextColor.GREEN)
.clickEvent(ClickEvent.openUrl(VelocityServer.VELOCITY_URL)) .clickEvent(
ClickEvent.openUrl("https://papermc.io/software/velocity"))
.build()) .build())
.append(Component.text(" - ")) .append(Component.text(" - "))
.append(Component.text() .append(Component.text()
@@ -221,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(Argument.component("plugins", listBuilder.build())) .arguments(listBuilder.build())
.build(); .build();
source.sendMessage(output); source.sendMessage(output);
return Command.SINGLE_SUCCESS; return Command.SINGLE_SUCCESS;
@@ -237,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",
Argument.component("url", Component.text(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().getFirst()))); Component.text(description.getAuthors().get(0))));
} else { } else {
hoverText.append( hoverText.append(
Component.translatable("velocity.command.plugin-tooltip-author", Component.translatable("velocity.command.plugin-tooltip-author",
Argument.string("authors", String.join(", ", description.getAuthors())) Component.text(String.join(", ", description.getAuthors()))
) )
); );
} }
@@ -103,17 +103,13 @@ abstract class InvocableCommandRegistrar<T extends InvocableCommand<I>,
.requiresWithContext((context, reader) -> requirement.test(context)) .requiresWithContext((context, reader) -> requirement.test(context))
.executes(callback) .executes(callback)
.suggests((context, builder) -> { .suggests((context, builder) -> {
// Offset the suggestion to the last space seperated word
int lastSpace = builder.getRemaining().lastIndexOf(' ') + 1;
final var offsetBuilder = builder.createOffset(builder.getStart() + lastSpace);
final I invocation = invocationFactory.create(context); final I invocation = invocationFactory.create(context);
return command.suggestAsync(invocation).thenApply(suggestions -> { return command.suggestAsync(invocation).thenApply(suggestions -> {
for (String value : suggestions) { for (String value : suggestions) {
Preconditions.checkNotNull(value, "suggestion"); Preconditions.checkNotNull(value, "suggestion");
offsetBuilder.suggest(value); builder.suggest(value);
} }
return offsetBuilder.build(); return builder.build();
}); });
}) })
.build(); .build();
@@ -29,9 +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.PacketLimiterMigration;
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;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
@@ -95,8 +93,6 @@ 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) {
@@ -113,7 +109,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, PacketLimiterConfig packetLimiterConfig) { boolean forceKeyAuthentication) {
this.bind = bind; this.bind = bind;
this.motd = motd; this.motd = motd;
this.showMaxPlayers = showMaxPlayers; this.showMaxPlayers = showMaxPlayers;
@@ -132,7 +128,6 @@ public class VelocityConfiguration implements ProxyConfig {
this.query = query; this.query = query;
this.metrics = metrics; this.metrics = metrics;
this.forceKeyAuthentication = forceKeyAuthentication; this.forceKeyAuthentication = forceKeyAuthentication;
this.packetLimiterConfig = packetLimiterConfig;
} }
/** /**
@@ -161,16 +156,19 @@ public class VelocityConfiguration implements ProxyConfig {
} }
switch (playerInfoForwardingMode) { switch (playerInfoForwardingMode) {
case NONE -> logger.warn("Player info forwarding is disabled! All players will appear to be connecting " case NONE:
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.");
case MODERN, BUNGEEGUARD -> { break;
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()) {
@@ -451,10 +449,6 @@ 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)
@@ -472,7 +466,6 @@ public class VelocityConfiguration implements ProxyConfig {
.add("favicon", favicon) .add("favicon", favicon)
.add("enablePlayerAddressLogging", enablePlayerAddressLogging) .add("enablePlayerAddressLogging", enablePlayerAddressLogging)
.add("forceKeyAuthentication", forceKeyAuthentication) .add("forceKeyAuthentication", forceKeyAuthentication)
.add("packetLimiterConfig", packetLimiterConfig)
.toString(); .toString();
} }
@@ -511,9 +504,7 @@ public class VelocityConfiguration implements ProxyConfig {
new ForwardingMigration(), new ForwardingMigration(),
new KeyAuthenticationMigration(), new KeyAuthenticationMigration(),
new MotdMigration(), new MotdMigration(),
new MiniMessageTranslationsMigration(), new TransferIntegrationMigration()
new TransferIntegrationMigration(),
new PacketLimiterMigration()
}; };
for (final ConfigurationMigration migration : migrations) { for (final ConfigurationMigration migration : migrations) {
@@ -524,7 +515,7 @@ public class VelocityConfiguration implements ProxyConfig {
String forwardingSecretString = System.getenv().getOrDefault( String forwardingSecretString = System.getenv().getOrDefault(
"VELOCITY_FORWARDING_SECRET", ""); "VELOCITY_FORWARDING_SECRET", "");
if (forwardingSecretString.isBlank()) { if (forwardingSecretString.isEmpty()) {
final String forwardSecretFile = config.get("forwarding-secret-file"); final String forwardSecretFile = config.get("forwarding-secret-file");
final Path secretPath = forwardSecretFile == null final Path secretPath = forwardSecretFile == null
? defaultForwardingSecretPath ? defaultForwardingSecretPath
@@ -537,11 +528,7 @@ public class VelocityConfiguration implements ProxyConfig {
"The file " + forwardSecretFile + " is not a valid file or it is a directory."); "The file " + forwardSecretFile + " is not a valid file or it is a directory.");
} }
} else { } else {
Files.createFile(secretPath); throw new RuntimeException("The forwarding-secret-file does not exist.");
Files.writeString(secretPath, forwardingSecretString = generateRandomString(12),
StandardCharsets.UTF_8);
logger.info("The forwarding-secret-file does not exist. A new file has been created at {}",
forwardSecretFile);
} }
} }
final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8); final byte[] forwardingSecret = forwardingSecretString.getBytes(StandardCharsets.UTF_8);
@@ -570,7 +557,6 @@ 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.
@@ -598,8 +584,7 @@ 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
); );
} }
} }
@@ -1002,35 +987,4 @@ 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
* @param bytesAfterDecompression the maximum number of decompressed bytes per second allowed
*/
public record PacketLimiterConfig(int interval, int pps, int bytes, int bytesAfterDecompression) {
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, -1, -1, 5242880);
/**
* 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()),
config.getIntOrElse("decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression())
);
} else {
return DEFAULT;
}
}
}
} }
@@ -28,9 +28,7 @@ public sealed interface ConfigurationMigration
permits ForwardingMigration, permits ForwardingMigration,
KeyAuthenticationMigration, KeyAuthenticationMigration,
MotdMigration, MotdMigration,
MiniMessageTranslationsMigration, TransferIntegrationMigration {
TransferIntegrationMigration,
PacketLimiterMigration {
boolean shouldMigrate(CommentedFileConfig config); boolean shouldMigrate(CommentedFileConfig config);
void migrate(CommentedFileConfig config, Logger logger) throws IOException; void migrate(CommentedFileConfig config, Logger logger) throws IOException;
@@ -1,65 +0,0 @@
/*
* 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);
}
}
}
}
@@ -1,62 +0,0 @@
/*
* Copyright (C) 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.config.migration;
import static com.velocitypowered.proxy.config.VelocityConfiguration.PacketLimiterConfig.DEFAULT;
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
import org.apache.logging.log4j.Logger;
/**
* Configuration migration for the new [packet-limiter] section.
* Config version 2.7 may contain this section with only the `interval`, `packets-per-second`
* and `bytes-per-second` attributes. Config version 2.8 enforces these exist, adds the new
* `decompressed-bytes-per-second` attribute, adjusts the new default, and adds comments.
*/
public final class PacketLimiterMigration implements ConfigurationMigration {
@Override
public boolean shouldMigrate(CommentedFileConfig config) {
return configVersion(config) < 2.8;
}
@Override
public void migrate(CommentedFileConfig config, Logger logger) {
config.set("packet-limiter.interval", DEFAULT.interval());
config.set("packet-limiter.packets-per-second", DEFAULT.pps());
config.set("packet-limiter.bytes-per-second", DEFAULT.bytes());
config.set("packet-limiter.decompressed-bytes-per-second", DEFAULT.bytesAfterDecompression());
config.setComment("packet-limiter.interval", """
Size of the moving time window in seconds used to calculate average rates.
A larger window tolerates short bursts while still enforcing the configured limits over time.""");
config.setComment("packet-limiter.packets-per-second", """
Maximum average number of packets per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.bytes-per-second", """
Maximum average number of compressed (on-wire) bytes per second a client may send. -1 disables this check.""");
config.setComment("packet-limiter.decompressed-bytes-per-second", """
Maximum average number of decompressed bytes per second a client may send.
Protects against compression bomb attacks where small packets expand to excessive sizes after decompression.
-1 disables this check.""");
config.set("config-version", "2.8");
}
}
@@ -33,14 +33,11 @@ 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;
import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.network.Connections;
import com.velocitypowered.proxy.network.limiter.SimpleBytesPerSecondLimiter;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent;
import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftCipherDecoder;
@@ -69,7 +66,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.EnumMap; import java.util.HashMap;
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;
@@ -111,7 +108,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
this.server = server; this.server = server;
this.state = StateRegistry.HANDSHAKE; this.state = StateRegistry.HANDSHAKE;
this.sessionHandlers = new EnumMap<>(StateRegistry.class); this.sessionHandlers = new HashMap<>();
} }
@Override @Override
@@ -156,13 +153,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(pkt); activeSessionHandler.handleGeneric((MinecraftPacket) msg);
} }
} 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 buf) { } else if (msg instanceof ByteBuf) {
activeSessionHandler.handleUnknown(buf); activeSessionHandler.handleUnknown((ByteBuf) msg);
} }
} finally { } finally {
ReferenceCountUtil.release(msg); ReferenceCountUtil.release(msg);
@@ -371,7 +368,6 @@ 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);
@@ -392,13 +388,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
if (state == StateRegistry.CONFIG) { if (state == StateRegistry.CONFIG) {
// Activate the play packet queue // Activate the play packet queue
if (previousState == StateRegistry.PLAY addPlayPacketQueueHandler();
&& 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) {
@@ -414,23 +404,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
* Adds the play packet queue handler. * Adds the play packet queue handler.
*/ */
public void addPlayPacketQueueHandler() { public void addPlayPacketQueueHandler() {
addPlayPacketQueueOutboundHandler();
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
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) { if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND, this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
new PlayPacketQueueOutboundHandler(this.protocolVersion, new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
channel.pipeline().get(MinecraftEncoder.class).getDirection())); }
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection()));
} }
} }
@@ -564,23 +544,14 @@ 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, minecraftDecoder.getDirection()); decoder = new MinecraftCompressDecoder(threshold, compressor);
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);
channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder); channel.pipeline().addBefore(MINECRAFT_ENCODER, COMPRESSION_ENCODER, encoder);
var packetLimiterConfig = server.getConfiguration().getPacketLimiterConfig();
if (minecraftDecoder.getDirection() == ProtocolUtils.Direction.SERVERBOUND
&& packetLimiterConfig.interval() > 0
&& packetLimiterConfig.bytesAfterDecompression() > 0) {
decoder.setPacketLimiter(new SimpleBytesPerSecondLimiter(
-1, packetLimiterConfig.bytesAfterDecompression(), packetLimiterConfig.interval()));
}
channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED); channel.pipeline().fireUserEventTriggered(VelocityConnectionEvent.COMPRESSION_ENABLED);
} }
} }
@@ -23,11 +23,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DialogClearPacket;
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
@@ -52,7 +48,6 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -72,8 +67,6 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerComma
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket; import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket; import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
@@ -371,32 +364,4 @@ public interface MinecraftSessionHandler {
default boolean handle(ClientboundServerLinksPacket packet) { default boolean handle(ClientboundServerLinksPacket packet) {
return false; return false;
} }
default boolean handle(DialogClearPacket packet) {
return false;
}
default boolean handle(DialogShowPacket packet) {
return false;
}
default boolean handle(ServerboundCustomClickActionPacket packet) {
return false;
}
default boolean handle(CodeOfConductPacket packet) {
return false;
}
default boolean handle(CodeOfConductAcceptPacket packet) {
return false;
}
default boolean handle(ClientboundSoundEntityPacket packet) {
return false;
}
default boolean handle(ClientboundStopSoundPacket packet) {
return false;
}
} }
@@ -68,7 +68,6 @@ 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;
@@ -92,7 +91,6 @@ 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;
@@ -179,12 +177,10 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(BossBarPacket packet) { public boolean handle(BossBarPacket packet) {
if (serverConn.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) { if (packet.getAction() == BossBarPacket.ADD) {
if (packet.getAction() == BossBarPacket.ADD) { playerSessionHandler.getServerBossBars().add(packet.getUuid());
playerSessionHandler.getServerBossBars().add(packet.getUuid()); } else if (packet.getAction() == BossBarPacket.REMOVE) {
} else if (packet.getAction() == BossBarPacket.REMOVE) { playerSessionHandler.getServerBossBars().remove(packet.getUuid());
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
}
} }
return false; // forward return false; // forward
} }
@@ -363,12 +359,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
// Inject commands from the proxy. // Inject commands from the proxy.
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector(); final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
injector.inject(rootNode, serverConn.getPlayer()); injector.inject(rootNode, serverConn.getPlayer());
rootNode.removeChildByName("velocity:callback");
// In 1.21.6 a confirmation prompt was added when executing a command via `run_command` click
// action if the command is unknown. To prevent this prompt we have to send the command.
if (this.playerConnection.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_6)) {
rootNode.removeChildByName("velocity:callback");
}
} }
server.getEventManager().fire( server.getEventManager().fire(
@@ -454,12 +445,11 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void handleGeneric(MinecraftPacket packet) { public void handleGeneric(MinecraftPacket packet) {
if (packet instanceof PluginMessagePacket pluginMessage) { if (packet instanceof PluginMessagePacket) {
pluginMessage.retain(); ((PluginMessagePacket) packet).retain();
} }
boolean huge = packet instanceof DeferredByteBufHolder def && def.content().readableBytes() > LARGE_PACKET_THRESHOLD;
playerConnection.delayedWrite(packet); playerConnection.delayedWrite(packet);
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush(); playerConnection.flush();
packetsFlushed = 0; packetsFlushed = 0;
} }
@@ -467,9 +457,8 @@ 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 (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) { if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
playerConnection.flush(); playerConnection.flush();
packetsFlushed = 0; packetsFlushed = 0;
} }
@@ -522,4 +511,4 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
playerConnection.setAutoReading(writable); playerConnection.setAutoReading(writable);
} }
} }
@@ -344,30 +344,66 @@ public class BungeeCordMessageResponder {
return false; return false;
} }
final ByteBufDataInput in = new ByteBufDataInput(message.content()); ByteBufDataInput in = new ByteBufDataInput(message.content());
final String subChannel = in.readUTF(); String subChannel = in.readUTF();
switch (subChannel) { switch (subChannel) {
case "GetPlayerServer" -> this.processGetPlayerServer(in); case "GetPlayerServer":
case "ForwardToPlayer" -> this.processForwardToPlayer(in); this.processGetPlayerServer(in);
case "Forward" -> this.processForwardToServer(in); break;
case "Connect" -> this.processConnect(in); case "ForwardToPlayer":
case "ConnectOther" -> this.processConnectOther(in); this.processForwardToPlayer(in);
case "IP" -> this.processIp(in); break;
case "PlayerCount" -> this.processPlayerCount(in); case "Forward":
case "PlayerList" -> this.processPlayerList(in); this.processForwardToServer(in);
case "GetServers" -> this.processGetServers(); break;
case "Message" -> this.processMessage(in); case "Connect":
case "MessageRaw" -> this.processMessageRaw(in); this.processConnect(in);
case "GetServer" -> this.processGetServer(); break;
case "UUID" -> this.processUuid(); case "ConnectOther":
case "UUIDOther" -> this.processUuidOther(in); this.processConnectOther(in);
case "IPOther" -> this.processIpOther(in); break;
case "ServerIP" -> this.processServerIp(in); case "IP":
case "KickPlayer" -> this.processKick(in); this.processIp(in);
case "KickPlayerRaw" -> this.processKickRaw(in); break;
default -> { case "PlayerCount":
// Do nothing, unknown command this.processPlayerCount(in);
} 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;
@@ -52,7 +52,6 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket; import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
@@ -60,7 +59,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 io.netty.channel.Channel; import java.io.IOException;
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,9 +71,6 @@ 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;
@@ -260,13 +256,7 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(DisconnectPacket packet) { public boolean handle(DisconnectPacket packet) {
serverConn.disconnect(); serverConn.disconnect();
// If the player receives a DisconnectPacket without a connection to a server in progress, resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
// it means that the backend server has kicked the player during reconfiguration
if (serverConn.getPlayer().getConnectionInFlight() != null) {
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
} else {
serverConn.getPlayer().handleConnectionException(serverConn.getServer(), packet, true);
}
return true; return true;
} }
@@ -368,16 +358,10 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
@Override
public boolean handle(CodeOfConductPacket packet) {
this.serverConn.getPlayer().getConnection().write(packet.retain());
return true;
}
@Override @Override
public void disconnected() { public void disconnected() {
resultFuture.complete(ConnectionRequestResults.forDisconnect( resultFuture.completeExceptionally(
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer())); new IOException("Unexpectedly disconnected from remote server"));
} }
@Override @Override
@@ -385,22 +369,6 @@ 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);
@@ -414,4 +382,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
} }
} }
@@ -165,7 +165,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
} }
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) { if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
smc.setAutoReading(false); smc.setAutoReading(false);
clientPlaySessionHandler.doSwitch().thenRunAsync(() -> smc.setAutoReading(true), smc.eventLoop()); clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
} else { } else {
// Initial login - the player is already in configuration state. // Initial login - the player is already in configuration state.
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn)); server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
@@ -38,6 +38,7 @@ 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;
@@ -89,7 +90,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override @Override
public boolean handle(JoinGamePacket packet) { public boolean handle(JoinGamePacket packet) {
final MinecraftConnection smc = serverConn.ensureConnected(); MinecraftConnection smc = serverConn.ensureConnected();
final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null); final RegisteredServer previousServer = serverConn.getPreviousServer().orElse(null);
final ConnectedPlayer player = serverConn.getPlayer(); final ConnectedPlayer player = serverConn.getPlayer();
final VelocityServerConnection existingConnection = player.getConnectedServer(); final VelocityServerConnection existingConnection = player.getConnectedServer();
@@ -106,9 +107,6 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
// Reset Tablist header and footer to prevent desync // Reset Tablist header and footer to prevent desync
player.clearPlayerListHeaderAndFooter(); player.clearPlayerListHeaderAndFooter();
// Override online mode
packet.setOnlineMode(player.isOnlineMode());
// The goods are in hand! We got JoinGame. Let's transition completely to the new state. // The goods are in hand! We got JoinGame. Let's transition completely to the new state.
smc.setAutoReading(false); smc.setAutoReading(false);
server.getEventManager() server.getEventManager()
@@ -124,8 +122,9 @@ 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 sessionHandler) { .getActiveSessionHandler() instanceof ClientPlaySessionHandler) {
playHandler = sessionHandler; playHandler =
(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);
@@ -215,7 +214,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
@Override @Override
public void disconnected() { public void disconnected() {
resultFuture.complete(ConnectionRequestResults.forDisconnect( resultFuture
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer())); .completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
} }
} }
@@ -53,7 +53,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -71,7 +70,6 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
private boolean gracefulDisconnect = false; private boolean gracefulDisconnect = false;
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
private final Map<Long, Long> pendingPings = new HashMap<>(); private final Map<Long, Long> pendingPings = new HashMap<>();
private @MonotonicNonNull Integer entityId;
/** /**
* Initializes a new server connection. * Initializes a new server connection.
@@ -180,8 +178,9 @@ 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 forgeConnection) { } else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType) {
handshake.setServerAddress(playerVhost + forgeConnection.getModernToken()); handshake.setServerAddress(playerVhost + ((ModernForgeConnectionType) proxyPlayer
.getConnection().getType()).getModernToken());
} else { } else {
handshake.setServerAddress(playerVhost); handshake.setServerAddress(playerVhost);
} }
@@ -325,14 +324,6 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
return pendingPings; return pendingPings;
} }
public Integer getEntityId() {
return entityId;
}
public void setEntityId(Integer entityId) {
this.entityId = entityId;
}
/** /**
* Ensures that this server connection remains "active": the connection is established and not * Ensures that this server connection remains "active": the connection is established and not
* closed, the player is still connected to the server, and the player still remains online. * closed, the player is still connected to the server, and the player still remains online.
@@ -69,16 +69,14 @@ 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, String serverIdHash) { GameProfile profile, boolean onlineMode) {
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
@@ -215,7 +213,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, serverIdHash)).thenAcceptAsync(event -> { server.getEventManager().fire(new LoginEvent(player)).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,
@@ -236,9 +234,6 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
success.setUsername(player.getUsername()); success.setUsername(player.getUsername());
success.setProperties(player.getGameProfileProperties()); success.setProperties(player.getGameProfileProperties());
success.setUuid(player.getUniqueId()); success.setUuid(player.getUniqueId());
if (inbound.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
success.setSessionId(server.getSessionId());
}
mcConnection.write(success); mcConnection.write(success);
loginState = State.SUCCESS_SENT; loginState = State.SUCCESS_SENT;
@@ -33,7 +33,6 @@ 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;
@@ -41,8 +40,6 @@ import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket;
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket; import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket; import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
@@ -61,8 +58,6 @@ 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;
@@ -210,26 +205,6 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
return true; return true;
} }
@Override
public boolean handle(ServerboundCustomClickActionPacket packet) {
if (player.getConnectionInFlight() != null) {
player.getConnectionInFlight().ensureConnected().write(packet.retain());
return true;
}
return false;
}
@Override
public boolean handle(CodeOfConductAcceptPacket packet) {
if (this.player.getConnectionInFlight() != null) {
this.player.getConnectionInFlight().ensureConnected().write(packet);
return true;
}
return false;
}
@Override @Override
public void handleGeneric(MinecraftPacket packet) { public void handleGeneric(MinecraftPacket packet) {
VelocityServerConnection serverConnection = player.getConnectedServer(); VelocityServerConnection serverConnection = player.getConnectedServer();
@@ -269,36 +244,6 @@ 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);
}
}
} }
/** /**
@@ -21,10 +21,10 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestion;
import com.velocitypowered.api.command.VelocityBrigadierMessage;
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;
@@ -41,7 +41,6 @@ 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;
@@ -86,11 +85,8 @@ import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
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.ComponentLike;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@@ -101,16 +97,6 @@ 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");
// Caps the per-connection queue used while the FML/login phases are not yet "complete". Without
// these caps, a client that never completes its handshake phase can spam plugin messages (each up
// to ~32 KiB serverbound) and grow the queue without bound.
private static final long MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES =
Long.getLong("velocity.max-queued-login-plugin-message-bytes", 4L * 1024 * 1024);
private static final int MAX_QUEUED_LOGIN_PLUGIN_MESSAGES =
Integer.getInteger("velocity.max-queued-login-plugin-messages", 1024);
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class); private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
@@ -118,9 +104,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
private boolean spawned = false; private boolean spawned = false;
private final List<UUID> serverBossBars = new ArrayList<>(); private final List<UUID> serverBossBars = new ArrayList<>();
private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>(); private final Queue<PluginMessagePacket> loginPluginMessages = new ConcurrentLinkedQueue<>();
private final AtomicLong loginPluginMessagesBytes = new AtomicLong();
private final AtomicInteger loginPluginMessagesCount = new AtomicInteger();
private volatile boolean loginPluginMessagesOverflowed;
private final VelocityServer server; private final VelocityServer server;
private @Nullable TabCompleteRequestPacket outstandingTabComplete; private @Nullable TabCompleteRequestPacket outstandingTabComplete;
private final ChatHandler<? extends MinecraftPacket> chatHandler; private final ChatHandler<? extends MinecraftPacket> chatHandler;
@@ -189,38 +172,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
@Override @Override
public void deactivated() { public void deactivated() {
player.discardChatQueue(); player.discardChatQueue();
PluginMessagePacket message; for (PluginMessagePacket message : loginPluginMessages) {
while ((message = loginPluginMessages.poll()) != null) {
ReferenceCountUtil.release(message); ReferenceCountUtil.release(message);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
}
/**
* Adds a retained plugin message to the queue used while the FML/login phases are still in
* progress, enforcing the per-connection byte and count caps. Returns {@code true} if queued,
* {@code false} if the packet was released (and the player disconnected on overflow).
*/
private boolean enqueueLoginPluginMessage(PluginMessagePacket packet) {
if (loginPluginMessagesOverflowed) {
ReferenceCountUtil.release(packet);
return false;
}
int packetSize = packet.content().readableBytes();
long newBytes = loginPluginMessagesBytes.addAndGet(packetSize);
int newCount = loginPluginMessagesCount.incrementAndGet();
if (newBytes > MAX_QUEUED_LOGIN_PLUGIN_MESSAGE_BYTES
|| newCount > MAX_QUEUED_LOGIN_PLUGIN_MESSAGES) {
loginPluginMessagesOverflowed = true;
ReferenceCountUtil.release(packet);
logger.warn("Disconnecting {}: pre-join plugin-message queue exceeded its limits "
+ "({} messages, {} bytes).", player, newCount, newBytes);
player.disconnect(Component.translatable("velocity.error.plugin-message-overflow"));
return false;
}
loginPluginMessages.add(packet);
return true;
} }
@Override @Override
@@ -364,12 +318,8 @@ 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)) {
List<ChannelIdentifier> channels = player.getClientsideChannels()
PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()); .removeAll(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());
@@ -401,7 +351,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// //
// We also need to make sure to retain these packets, so they can be flushed // We also need to make sure to retain these packets, so they can be flushed
// appropriately. // appropriately.
enqueueLoginPluginMessage(packet.retain()); loginPluginMessages.add(packet.retain());
} else { } else {
// The connection is ready, send the packet now. // The connection is ready, send the packet now.
backendConn.write(packet.retain()); backendConn.write(packet.retain());
@@ -416,7 +366,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
if (!player.getPhase().consideredComplete() || !serverConn.getPhase() if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
.consideredComplete()) { .consideredComplete()) {
// We're still processing the connection (see above), enqueue the packet for now. // We're still processing the connection (see above), enqueue the packet for now.
enqueueLoginPluginMessage(message.retain()); loginPluginMessages.add(message.retain());
} else { } else {
backendConn.write(message); backendConn.write(message);
} }
@@ -513,11 +463,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
final boolean stateAllowsForward = smc != null if (smc != null && serverConnection.getPhase().consideredComplete()) {
&& !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();
} }
@@ -534,11 +480,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
MinecraftConnection smc = serverConnection.getConnection(); MinecraftConnection smc = serverConnection.getConnection();
final boolean stateAllowsForward = smc != null if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) {
&& !smc.isClosed()
&& serverConnection.getPhase().consideredComplete()
&& smc.getState() == StateRegistry.PLAY;
if (stateAllowsForward) {
smc.write(buf.retain()); smc.write(buf.retain());
} }
} }
@@ -550,24 +492,14 @@ public class ClientPlaySessionHandler 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(
if (MinecraftDecoder.DEBUG) { Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
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
@@ -603,13 +535,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
// Config state clears everything in the client. No need to clear later. // Config state clears everything in the client. No need to clear later.
spawned = false; spawned = false;
serverBossBars.clear();
player.clearPlayerListHeaderAndFooterSilent(); player.clearPlayerListHeaderAndFooterSilent();
player.getTabList().clearAllSilent(); player.getTabList().clearAllSilent();
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
player.getBossBarManager().dropPackets();
} else {
serverBossBars.clear();
}
} }
player.switchToConfigState(); player.switchToConfigState();
@@ -647,20 +575,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
} }
} }
destination.setEntityId(joinGame.getEntityId()); // used for sound api // Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) { // track them.
player.getBossBarManager().sendBossBars(); for (UUID serverBossBar : serverBossBars) {
} else { BossBarPacket deletePacket = new BossBarPacket();
// Remove previous boss bars. These don't get cleared when sending JoinGame (up until 1.20.2), deletePacket.setUuid(serverBossBar);
// thus the need to track them. deletePacket.setAction(BossBarPacket.REMOVE);
for (UUID serverBossBar : serverBossBars) { player.getConnection().delayedWrite(deletePacket);
BossBarPacket deletePacket = new BossBarPacket();
deletePacket.setUuid(serverBossBar);
deletePacket.setAction(BossBarPacket.REMOVE);
player.getConnection().delayedWrite(deletePacket);
}
serverBossBars.clear();
} }
serverBossBars.clear();
// Tell the server about the proxy's plugin message channels. // Tell the server about the proxy's plugin message channels.
ProtocolVersion serverVersion = serverMc.getProtocolVersion(); ProtocolVersion serverVersion = serverMc.getProtocolVersion();
@@ -679,8 +602,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
while ((pm = loginPluginMessages.poll()) != null) { while ((pm = loginPluginMessages.poll()) != null) {
serverMc.delayedWrite(pm); serverMc.delayedWrite(pm);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
// Clear any title from the previous server. // Clear any title from the previous server.
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) { if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
@@ -773,35 +694,23 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
return; return;
} }
int startPos = -1; List<Offer> offers = new ArrayList<>();
for (var suggestion : suggestions.getList()) { for (Suggestion suggestion : suggestions.getList()) {
if (startPos == -1 || startPos > suggestion.getRange().getStart()) { String offer = suggestion.getText();
startPos = suggestion.getRange().getStart(); ComponentHolder tooltip = null;
if (suggestion.getTooltip() != null
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
tooltip = new ComponentHolder(player.getProtocolVersion(),
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
} }
offers.add(new Offer(offer, tooltip));
} }
int startPos = packet.getCommand().lastIndexOf(' ') + 1;
if (startPos > 0) { if (startPos > 0) {
List<Offer> offers = new ArrayList<>();
for (Suggestion suggestion : suggestions.getList()) {
String offer;
if (suggestion.getRange().getStart() == startPos) {
offer = suggestion.getText();
} else {
offer = command.substring(startPos, suggestion.getRange().getStart()) + suggestion.getText();
}
ComponentHolder tooltip = null;
if (suggestion.getTooltip() instanceof ComponentLike componentLike) {
tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent());
} else if (suggestion.getTooltip() != null) {
tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString()));
}
offers.add(new Offer(offer, tooltip));
}
TabCompleteResponsePacket resp = new TabCompleteResponsePacket(); TabCompleteResponsePacket resp = new TabCompleteResponsePacket();
resp.setTransactionId(packet.getTransactionId()); resp.setTransactionId(packet.getTransactionId());
resp.setStart(startPos + 1); resp.setStart(startPos);
resp.setLength(packet.getCommand().length() - startPos - 1); resp.setLength(packet.getCommand().length() - startPos);
resp.getOffers().addAll(offers); resp.getOffers().addAll(offers);
player.getConnection().write(resp); player.getConnection().write(resp);
} }
@@ -856,10 +765,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
offer = offer.substring(command.length()); offer = offer.substring(command.length());
} }
ComponentHolder tooltip = null; ComponentHolder tooltip = null;
if (suggestion.getTooltip() instanceof ComponentLike componentLike) { if (suggestion.getTooltip() != null
tooltip = new ComponentHolder(player.getProtocolVersion(), componentLike.asComponent()); && suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
} else if (suggestion.getTooltip() != null) { tooltip = new ComponentHolder(player.getProtocolVersion(),
tooltip = new ComponentHolder(player.getProtocolVersion(), Component.text(suggestion.getTooltip().getString())); ((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
} }
response.getOffers().add(new Offer(offer, tooltip)); response.getOffers().add(new Offer(offer, tooltip));
} }
@@ -913,8 +822,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
while ((pm = loginPluginMessages.poll()) != null) { while ((pm = loginPluginMessages.poll()) != null) {
connection.write(pm); connection.write(pm);
} }
loginPluginMessagesBytes.set(0);
loginPluginMessagesCount.set(0);
} }
} }
} }
@@ -62,7 +62,6 @@ import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
import com.velocitypowered.proxy.connection.player.bossbar.BossBarManager;
import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler; import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler; import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
@@ -74,8 +73,6 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
@@ -84,6 +81,7 @@ import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket; import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
import com.velocitypowered.proxy.protocol.packet.TransferPacket; import com.velocitypowered.proxy.protocol.packet.TransferPacket;
import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket; import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
@@ -110,7 +108,6 @@ import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@@ -118,21 +115,21 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.pointer.Pointers;
import net.kyori.adventure.pointer.PointersSupplier; import net.kyori.adventure.pointer.PointersSupplier;
import net.kyori.adventure.resource.ResourcePackInfoLike; import net.kyori.adventure.resource.ResourcePackInfoLike;
import net.kyori.adventure.resource.ResourcePackRequest; import net.kyori.adventure.resource.ResourcePackRequest;
import net.kyori.adventure.resource.ResourcePackRequestLike; import net.kyori.adventure.resource.ResourcePackRequestLike;
import net.kyori.adventure.sound.Sound;
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;
@@ -163,6 +160,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
.resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername())) .resolving(Identity.DISPLAY_NAME, player -> Component.text(player.getUsername()))
.resolving(Identity.LOCALE, Player::getEffectiveLocale) .resolving(Identity.LOCALE, Player::getEffectiveLocale)
.resolving(PermissionChecker.POINTER, Player::getPermissionChecker) .resolving(PermissionChecker.POINTER, Player::getPermissionChecker)
.resolving(FacetPointers.TYPE, player -> Type.PLAYER)
.build(); .build();
/** /**
@@ -199,7 +197,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
private @Nullable ClientSettingsPacket clientSettingsPacket; private @Nullable ClientSettingsPacket clientSettingsPacket;
private volatile ChatQueue chatQueue; private volatile ChatQueue chatQueue;
private final ChatBuilderFactory chatBuilderFactory; private final ChatBuilderFactory chatBuilderFactory;
private final BossBarManager bossBarManager;
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode, @Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
@@ -226,7 +223,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.chatQueue = new ChatQueue(this); this.chatQueue = new ChatQueue(this);
this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion()); this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion());
this.resourcePackHandler = ResourcePackHandler.create(this, server); this.resourcePackHandler = ResourcePackHandler.create(this, server);
this.bossBarManager = new BossBarManager(this);
} }
/** /**
@@ -420,16 +416,29 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void sendMessage(final @NonNull Component message) { public void sendMessage(@NonNull Identity identity, @NonNull Component message) {
Preconditions.checkNotNull(message, "message");
final Component translated = translateMessage(message); final Component translated = translateMessage(message);
connection.write(getChatBuilderFactory().builder() connection.write(getChatBuilderFactory().builder()
.component(translated).toClient()); .component(translated).forIdentity(identity).toClient());
} }
@Override @Override
public void sendActionBar(@NonNull Component message) { public void sendMessage(@NonNull Identity identity, @NonNull Component message,
@NonNull MessageType type) {
Preconditions.checkNotNull(message, "message");
Preconditions.checkNotNull(type, "type");
Component translated = translateMessage(message);
connection.write(getChatBuilderFactory().builder()
.component(translated).forIdentity(identity)
.setType(type == MessageType.CHAT ? ChatType.CHAT : ChatType.SYSTEM)
.toClient());
}
@Override
public void sendActionBar(net.kyori.adventure.text.@NonNull Component message) {
Component translated = translateMessage(message); Component translated = translateMessage(message);
ProtocolVersion playerVersion = getProtocolVersion(); ProtocolVersion playerVersion = getProtocolVersion();
@@ -628,8 +637,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
} }
@Override @Override
public void disconnect(@NotNull Component reason) { public void disconnect(Component reason) {
Objects.requireNonNull(reason, "reason");
if (connection.eventLoop().inEventLoop()) { if (connection.eventLoop().inEventLoop()) {
disconnect0(reason, false); disconnect0(reason, false);
} else { } else {
@@ -698,12 +706,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",
Argument.string("server", server.getServerInfo().getName())); Component.text(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",
Argument.string("server", server.getServerInfo().getName())); Component.text(server.getServerInfo().getName()));
} }
handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe); handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
} }
@@ -725,22 +733,18 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
Component disconnectReason = disconnect.getReason().getComponent(); Component disconnectReason = disconnect.getReason().getComponent();
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason); String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
if (this.server.getConfiguration().isLogPlayerConnections()) { logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(), plainTextReason);
plainTextReason);
}
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,
Argument.string("server", server.getServerInfo().getName()), Component.text(server.getServerInfo().getName()),
disconnectReason), safe); disconnectReason), safe);
} else { } else {
if (this.server.getConfiguration().isLogPlayerConnections()) { logger.error("{}: disconnected while connecting to {}: {}", this,
logger.error("{}: disconnected while connecting to {}: {}", this, server.getServerInfo().getName(), plainTextReason);
server.getServerInfo().getName(), plainTextReason);
}
handleConnectionException(server, disconnectReason, handleConnectionException(server, disconnectReason,
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED, Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
Argument.string("server", server.getServerInfo().getName()), Component.text(server.getServerInfo().getName()),
disconnectReason), safe); disconnectReason), safe);
} }
} }
@@ -796,56 +800,63 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return; return;
} }
switch (event.getResult()) { if (event.getResult() instanceof final DisconnectPlayer res) {
case DisconnectPlayer res -> disconnect(res.getReasonComponent()); disconnect(res.getReasonComponent());
case RedirectPlayer res -> createConnectionRequest(res.getServer(), previousConnection).connect() } else if (event.getResult() instanceof final RedirectPlayer res) {
.whenCompleteAsync((status, throwable) -> { createConnectionRequest(res.getServer(), previousConnection).connect()
if (throwable != null) { .whenCompleteAsync((status, throwable) -> {
handleConnectionException(res.getServer(), throwable, true); if (throwable != null) {
return; handleConnectionException(
} status != null ? status.getAttemptedConnection() : res.getServer(), throwable,
true);
return;
}
switch (status.getStatus()) { switch (status.getStatus()) {
// Impossible/nonsensical cases // Impossible/nonsensical cases
case ALREADY_CONNECTED -> logger.error("{}: already connected to {}", this, case ALREADY_CONNECTED:
status.getAttemptedConnection().getServerInfo().getName()); logger.error("{}: already connected to {}", this,
case CONNECTION_IN_PROGRESS, CONNECTION_CANCELLED -> { status.getAttemptedConnection().getServerInfo().getName());
Component fallbackMsg = res.getMessageComponent(); break;
if (fallbackMsg == null) { case CONNECTION_IN_PROGRESS:
fallbackMsg = friendlyReason; // Fatal case
} case CONNECTION_CANCELLED:
disconnect(status.getReasonComponent().orElse(fallbackMsg)); Component fallbackMsg = res.getMessageComponent();
} if (fallbackMsg == null) {
case SERVER_DISCONNECTED -> { fallbackMsg = friendlyReason;
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!)
}
} }
}, connection.eventLoop()); disconnect(status.getReasonComponent().orElse(fallbackMsg));
case Notify res -> { break;
if (event.kickedDuringServerConnect() && previousConnection != null) { case SERVER_DISCONNECTED:
sendMessage(res.getMessageComponent()); Component reason = status.getReasonComponent()
} else { .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
disconnect(res.getMessageComponent()); handleConnectionException(res.getServer(),
} 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.
default -> disconnect(friendlyReason); disconnect(friendlyReason);
} }
}, connection.eventLoop()); }, connection.eventLoop());
} }
@@ -1027,50 +1038,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
this.clientBrand = clientBrand; this.clientBrand = clientBrand;
} }
@Override
public void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
Preconditions.checkNotNull(sound, "sound");
Preconditions.checkNotNull(emitter, "emitter");
VelocityServerConnection soundTargetServerConn = getConnectedServer();
if (getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)
|| connection.getState() != StateRegistry.PLAY
|| soundTargetServerConn == null
|| (sound.source() == Sound.Source.UI
&& getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_5))) {
return;
}
VelocityServerConnection soundEmitterServerConn;
if (emitter == Sound.Emitter.self()) {
soundEmitterServerConn = soundTargetServerConn;
} else if (emitter instanceof ConnectedPlayer player) {
if ((soundEmitterServerConn = player.getConnectedServer()) == null) {
return;
}
if (!soundEmitterServerConn.getServer().equals(soundTargetServerConn.getServer())) {
return;
}
} else {
return;
}
connection.write(new ClientboundSoundEntityPacket(sound, null, soundEmitterServerConn.getEntityId()));
}
@Override
public void stopSound(@NotNull SoundStop stop) {
Preconditions.checkNotNull(stop, "stop");
if (getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_19_3)
|| connection.getState() != StateRegistry.PLAY
|| (stop.source() == Sound.Source.UI
&& getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_21_5))) {
return;
}
connection.write(new ClientboundStopSoundPacket(stop));
}
@Override @Override
public void transferToHost(final InetSocketAddress address) { public void transferToHost(final InetSocketAddress address) {
Preconditions.checkNotNull(address); Preconditions.checkNotNull(address);
@@ -1333,17 +1300,11 @@ 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();
final StateRegistry clientState = connection.getState(); if (smc != null) {
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;
@@ -1353,8 +1314,7 @@ 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() {
final VelocityServerConnection targetServer = getConnectionInFlightOrConnectedServer(); server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, 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()) {
@@ -1369,7 +1329,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.addPlayPacketQueueOutboundHandler(); connection.addPlayPacketQueueHandler();
}, 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;
@@ -1419,10 +1379,6 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
return handshakeIntent; return handshakeIntent;
} }
public BossBarManager getBossBarManager() {
return bossBarManager;
}
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect; private final RegisteredServer toConnect;
@@ -1482,16 +1438,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
VelocityServerConnection con = VelocityServerConnection con =
new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server); new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server);
connectionInFlight = con; connectionInFlight = con;
return con.connect().whenCompleteAsync((result, exception) -> this.resetIfInFlightIs(con),
return con.connect().whenCompleteAsync((result, exception) -> {
if (result != null && !result.isSuccessful() && !result.isSafe()) {
handleConnectionException(result.getAttemptedConnection(),
// The only way for the reason to be null is if the result is safe
DisconnectPacket.create(result.getReasonComponent().orElseThrow(),
getProtocolVersion(), connection.getState()), false);
}
this.resetIfInFlightIs(con);
},
connection.eventLoop()); connection.eventLoop());
}, connection.eventLoop()); }, connection.eventLoop());
}); });
@@ -1505,14 +1452,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
@Override @Override
public CompletableFuture<Result> connect() { public CompletableFuture<Result> connect() {
return this.internalConnect().thenApply(x -> x); return this.internalConnect().whenCompleteAsync((status, throwable) -> {
if (status != null && !status.isSuccessful()) {
if (!status.isSafe()) {
handleConnectionException(status.getAttemptedConnection(), throwable, false);
}
}
}, connection.eventLoop()).thenApply(x -> x);
} }
@Override @Override
public CompletableFuture<Boolean> connectWithIndication() { public CompletableFuture<Boolean> connectWithIndication() {
return internalConnect().whenCompleteAsync((status, throwable) -> { return internalConnect().whenCompleteAsync((status, throwable) -> {
if (throwable != null) { if (throwable != null) {
handleConnectionException(toConnect, throwable, true); // TODO: The exception handling from this is not very good. Find a better way.
handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect,
throwable, true);
return; return;
} }
@@ -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()
"multiplayer.disconnect.outdated_client", .key("multiplayer.disconnect.outdated_client")
Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING) .arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING))
)); .build());
return; return;
} }
@@ -65,7 +65,8 @@ public class InitialConnectSessionHandler implements MinecraftSessionHandler {
} }
byte[] copy = ByteBufUtil.getBytes(packet.content()); byte[] copy = ByteBufUtil.getBytes(packet.content());
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy); PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id,
copy);
server.getEventManager().fire(event) server.getEventManager().fire(event)
.thenAcceptAsync(pme -> { .thenAcceptAsync(pme -> {
if (pme.getResult().isAllowed() && serverConn.isActive()) { if (pme.getResult().isAllowed() && serverConn.isActive()) {
@@ -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, null)); GameProfile.forOfflinePlayer(login.getUsername()), false));
} }
}); });
}); });
@@ -214,7 +214,6 @@ 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) -> {
@@ -255,7 +254,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, serverId)); new AuthSessionHandler(server, inbound, profile, true));
} 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,8 +267,16 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down")); inbound.disconnect(Component.translatable("multiplayer.disconnect.authservers_down"));
} }
}, mcConnection.eventLoop()) }, mcConnection.eventLoop())
.whenComplete((ignored, throwable) -> { .thenRun(() -> {
httpClient.close(); if (httpClient instanceof final AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
// In Java 21, the HttpClient does not throw any Exception
// when trying to clean its resources, so this should not happen
logger.error("An unknown error occurred while trying to close an HttpClient", e);
}
}
}); });
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
logger.error("Unable to enable encryption", e); logger.error("Unable to enable encryption", e);
@@ -1,79 +0,0 @@
/*
* Copyright (C) 2019-2023 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.connection.player.bossbar;
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import java.util.HashSet;
import java.util.Set;
/**
* Handles dropping and resending boss bar packets on versions 1.20.2 and newer because the client now
* deletes all boss bars during the login phase, and sending update packets would cause the client to be disconnected.
*/
public final class BossBarManager {
private final ConnectedPlayer player;
private final Set<VelocityBossBarImplementation> bossBars = new HashSet<>();
private boolean dropPackets = false;
public BossBarManager(ConnectedPlayer player) {
this.player = player;
}
/**
* Records the specified boss bar to be re-sent when a player changes server, and sends the update packet
* if the client is able to receive it and not be disconnected.
*/
public synchronized void writeUpdate(VelocityBossBarImplementation bar, BossBarPacket packet) {
this.bossBars.add(bar);
if (!this.dropPackets) {
this.player.getConnection().write(packet);
}
}
/**
* Removes the specified boss bar from the player to ensure it is not re-sent.
*/
public synchronized void remove(VelocityBossBarImplementation bar, BossBarPacket packet) {
this.bossBars.remove(bar);
if (!this.dropPackets) {
this.player.getConnection().write(packet);
}
}
/**
* Re-creates the boss bars the player can see with any updates that may have occurred in the meantime,
* and allows update packets for those boss bars to be sent.
*/
public synchronized void sendBossBars() {
for (VelocityBossBarImplementation bossBar : bossBars) {
bossBar.createDirect(player);
}
this.dropPackets = false;
}
/**
* Prevents the player from receiving boss bar update packets while logging in to a new server.
*/
public synchronized void dropPackets() {
this.dropPackets = true;
}
}
@@ -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.getFirst().getId()); tickResourcePackQueue(outstandingResourcePacks.get(0).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.getFirst()); sendResourcePackRequestPacket(outstandingResourcePacks.get(0));
} }
} }
@@ -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.getFirst() : outstandingResourcePacks.removeFirst(); peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0);
server.getEventManager() server.getEventManager()
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued)) .fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
@@ -111,7 +111,7 @@ public abstract sealed class ResourcePackHandler
} }
request.setRequired(queued.getShouldForce()); request.setRequired(queued.getShouldForce());
request.setPrompt(queued.getPrompt() == null ? null : request.setPrompt(queued.getPrompt() == null ? null :
new ComponentHolder(player.getProtocolVersion(), player.translateMessage(queued.getPrompt()))); new ComponentHolder(player.getProtocolVersion(), queued.getPrompt()));
player.getConnection().write(request); player.getConnection().write(request);
} }
@@ -36,7 +36,6 @@ import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import net.kyori.adventure.text.Component;
/** /**
* Common utilities for handling server list ping results. * Common utilities for handling server list ping results.
@@ -57,16 +56,16 @@ public class ServerListPingHandler {
List<ServerPing.SamplePlayer> samplePlayers; List<ServerPing.SamplePlayer> samplePlayers;
if (configuration.getSamplePlayersInPing()) { if (configuration.getSamplePlayersInPing()) {
List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream() List<ServerPing.SamplePlayer> unshuffledPlayers = server.getAllPlayers().stream()
.map(p -> { .map(p -> {
if (p.getPlayerSettings().isClientListingAllowed()) { if (p.getPlayerSettings().isClientListingAllowed()) {
return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId()); return new ServerPing.SamplePlayer(p.getUsername(), p.getUniqueId());
} else { } else {
return ServerPing.SamplePlayer.ANONYMOUS; return ServerPing.SamplePlayer.ANONYMOUS;
} }
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.shuffle(unshuffledPlayers); Collections.shuffle(unshuffledPlayers);
samplePlayers = unshuffledPlayers.subList(0, Math.min(12, unshuffledPlayers.size())); samplePlayers = unshuffledPlayers.subList(0, Math.min(12, server.getPlayerCount()));
} else { } else {
samplePlayers = ImmutableList.of(); samplePlayers = ImmutableList.of();
} }
@@ -100,60 +99,58 @@ public class ServerListPingHandler {
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings, CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
(ex) -> fallback); (ex) -> fallback);
return switch (mode) { switch (mode) {
case ALL -> pingResponses.thenApply(responses -> { case ALL:
// Find the first non-fallback return pingResponses.thenApply(responses -> {
for (ServerPing response : responses) { // Find the first non-fallback
if (response == fallback) { for (ServerPing response : responses) {
continue; if (response == fallback) {
continue;
}
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) {
return response.asBuilder() continue;
.description(Component.empty()) }
.build();
}
return response; return new ServerPing(
} fallback.getVersion(),
return fallback; fallback.getPlayers().orElse(null),
}); response.getDescriptionComponent(),
case MODS -> pingResponses.thenApply(responses -> { fallback.getFavicon().orElse(null),
// Find the first non-fallback that contains a mod list response.getModinfo().orElse(null)
for (ServerPing response : responses) { );
if (response == fallback) {
continue;
} }
Optional<ModInfo> modInfo = response.getModinfo(); return fallback;
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 -> CompletableFuture.completedFuture(fallback); default:
}; return CompletableFuture.completedFuture(fallback);
}
} }
/** /**
@@ -25,13 +25,14 @@ import com.velocitypowered.api.permission.Tristate;
import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.util.ClosestLocaleMatcher; import com.velocitypowered.proxy.util.ClosestLocaleMatcher;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity; import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.permission.PermissionChecker; import net.kyori.adventure.permission.PermissionChecker;
import net.kyori.adventure.platform.facet.FacetPointers;
import net.kyori.adventure.platform.facet.FacetPointers.Type;
import net.kyori.adventure.pointer.Pointers; import net.kyori.adventure.pointer.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;
@@ -58,10 +59,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 static final @NotNull PointersSupplier<VelocityConsole> POINTERS = PointersSupplier.<VelocityConsole>builder() private final @NotNull Pointers pointers = ConsoleCommandSource.super.pointers().toBuilder()
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker) .withDynamic(PermissionChecker.POINTER, this::getPermissionChecker)
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE .withDynamic(Identity.LOCALE, () -> ClosestLocaleMatcher.INSTANCE
.lookupClosest(Locale.getDefault())) .lookupClosest(Locale.getDefault()))
.withStatic(FacetPointers.TYPE, Type.CONSOLE)
.build(); .build();
public VelocityConsole(VelocityServer server) { public VelocityConsole(VelocityServer server) {
@@ -69,7 +71,8 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
} }
@Override @Override
public void sendMessage(@NonNull Component message) { public void sendMessage(@NonNull Identity identity, @NonNull Component message,
@NonNull MessageType messageType) {
componentLogger.info(message); componentLogger.info(message);
} }
@@ -107,7 +110,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
protected LineReader buildReader(LineReaderBuilder builder) { protected LineReader buildReader(LineReaderBuilder builder) {
return super.buildReader(builder return super.buildReader(builder
.appName("Velocity") .appName("Velocity")
.variable(LineReader.HISTORY_FILE, Path.of(".console_history"))
.completer((reader, parsedLine, list) -> { .completer((reader, parsedLine, list) -> {
try { try {
List<String> offers = this.server.getCommandManager() List<String> offers = this.server.getCommandManager()
@@ -134,10 +136,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
if (!this.server.getCommandManager().executeAsync(this, command).join()) { if (!this.server.getCommandManager().executeAsync(this, command).join()) {
sendMessage(Component.translatable("velocity.command.command-does-not-exist", sendMessage(Component.translatable("velocity.command.command-does-not-exist",
NamedTextColor.RED)); NamedTextColor.RED));
return;
}
if (server.getConfiguration().isLogCommandExecutions()) {
logger.info("CONSOLE -> executed command /{}", command);
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("An error occurred while running this command.", e); logger.error("An error occurred while running this command.", e);
@@ -151,6 +149,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
@Override @Override
public @NotNull Pointers pointers() { public @NotNull Pointers pointers() {
return POINTERS.view(this); return pointers;
} }
} }
@@ -26,10 +26,8 @@ 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;
@@ -74,17 +72,6 @@ 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());
} }
@@ -116,7 +116,7 @@ public enum TransportType {
return NIO; return NIO;
} }
if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) { if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) {
return IO_URING; return IO_URING;
} }
@@ -1,32 +0,0 @@
/*
* 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);
}
@@ -1,77 +0,0 @@
/*
* 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;
}
}
@@ -32,20 +32,17 @@ import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
/** /**
* An implementation of {@code InetNameResolver} that performs blocking DNS name lookups * An implementation of {@code InetNameResolver} that performs blocking DNS name lookups
* on a small bounded pool of separate threads, avoiding blocking the Netty threads for an * in a separate thread, avoiding blocking the Netty threads for an extended period of time
* extended period of time and without the downsides of Netty's native DNS resolver. * and without the downsides of Netty's native DNS resolver.
*/ */
public final class SeparatePoolInetNameResolver extends InetNameResolver { public final class SeparatePoolInetNameResolver extends InetNameResolver {
private static final int MAX_RESOLVE_THREADS = 8;
private final ExecutorService resolveExecutor; private final ExecutorService resolveExecutor;
private final InetNameResolver delegate; private final InetNameResolver delegate;
private final Cache<String, List<InetAddress>> cache; private final Cache<String, List<InetAddress>> cache;
@@ -59,15 +56,11 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
*/ */
public SeparatePoolInetNameResolver(EventExecutor executor) { public SeparatePoolInetNameResolver(EventExecutor executor) {
super(executor); super(executor);
ThreadPoolExecutor resolveExecutor = new ThreadPoolExecutor( this.resolveExecutor = Executors.newSingleThreadExecutor(
MAX_RESOLVE_THREADS, MAX_RESOLVE_THREADS,
60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder() new ThreadFactoryBuilder()
.setNameFormat("Velocity DNS Resolver #%d") .setNameFormat("Velocity DNS Resolver")
.setDaemon(true) .setDaemon(true)
.build()); .build());
resolveExecutor.allowCoreThreadTimeOut(true);
this.resolveExecutor = resolveExecutor;
this.delegate = new DefaultNameResolver(executor); this.delegate = new DefaultNameResolver(executor);
this.cache = Caffeine.newBuilder() this.cache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS) .expireAfterWrite(30, TimeUnit.SECONDS)
@@ -78,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.getFirst()); promise.trySuccess(addresses.get(0));
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 candidateInst)) { if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate)) {
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,6 +93,8 @@ 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);
} }
@@ -100,10 +102,11 @@ 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 javaDescription)) { if (!(description instanceof JavaVelocityPluginDescription)) {
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()) {
@@ -115,23 +118,24 @@ 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 pluginContainer)) { if (!(container instanceof VelocityPluginContainer)) {
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 = pluginContainer.getDescription(); PluginDescription description = container.getDescription();
if (!(description instanceof JavaVelocityPluginDescription javaPluginDescription)) { if (!(description instanceof JavaVelocityPluginDescription)) {
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.getInstance(javaPluginDescription.getMainClass()); Object instance = injector
.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());
} }
pluginContainer.setInstance(instance); ((VelocityPluginContainer) container).setInstance(instance);
} }
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source) private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source)
@@ -141,23 +145,22 @@ 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) {
switch (entry.getName()) { if (entry.getName().equals("velocity-plugin.json")) {
case "velocity-plugin.json" -> { try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) { return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader, SerializedPluginDescription.class));
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 Paper, Bukkit or BungeeCord plugin. Velocity does not support plugins from these " + "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord "
+ "platforms."); + "plugins.");
} }
return Optional.empty(); return Optional.empty();
@@ -32,18 +32,13 @@ public interface MinecraftPacket {
boolean handle(MinecraftSessionHandler handler); boolean handle(MinecraftSessionHandler handler);
default int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, default int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) { ProtocolVersion version) {
return -1; return -1;
} }
default int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, default int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) { ProtocolVersion version) {
return 0; return 0;
} }
default int encodeSizeHint(ProtocolUtils.Direction direction,
ProtocolVersion version) {
return -1;
}
} }
@@ -36,9 +36,7 @@ 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;
@@ -46,7 +44,6 @@ import net.kyori.adventure.nbt.BinaryTagIO;
import net.kyori.adventure.nbt.BinaryTagType; import net.kyori.adventure.nbt.BinaryTagType;
import net.kyori.adventure.nbt.BinaryTagTypes; import net.kyori.adventure.nbt.BinaryTagTypes;
import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.json.JSONOptions; import net.kyori.adventure.text.serializer.json.JSONOptions;
import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer; import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer;
@@ -60,11 +57,10 @@ public enum ProtocolUtils {
private static final GsonComponentSerializer PRE_1_16_SERIALIZER = private static final GsonComponentSerializer PRE_1_16_SERIALIZER =
GsonComponentSerializer.builder() GsonComponentSerializer.builder()
.downsampleColors()
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionSchema.globalSchema().stateBuilder() OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// before 1.16 // before 1.16
.value(JSONOptions.EMIT_RGB, Boolean.FALSE) .value(JSONOptions.EMIT_RGB, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.VALUE_FIELD) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.VALUE_FIELD)
@@ -73,8 +69,6 @@ public enum ProtocolUtils {
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build() .build()
) )
.build(); .build();
@@ -83,8 +77,6 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionSchema.globalSchema().stateBuilder() OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// after 1.16 // after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE) .value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
@@ -94,8 +86,6 @@ public enum ProtocolUtils {
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE) .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build() .build()
) )
.build(); .build();
@@ -104,8 +94,6 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionSchema.globalSchema().stateBuilder() OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// after 1.16 // after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE) .value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
@@ -115,8 +103,6 @@ public enum ProtocolUtils {
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE) .value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
// before 1.21.5
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
.build() .build()
) )
.build(); .build();
@@ -125,8 +111,6 @@ public enum ProtocolUtils {
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
.options( .options(
OptionSchema.globalSchema().stateBuilder() OptionSchema.globalSchema().stateBuilder()
// general options
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
// after 1.16 // after 1.16
.value(JSONOptions.EMIT_RGB, Boolean.TRUE) .value(JSONOptions.EMIT_RGB, Boolean.TRUE)
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.SNAKE_CASE) .value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.SNAKE_CASE)
@@ -137,7 +121,6 @@ public enum ProtocolUtils {
// after 1.21.5 // after 1.21.5
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, Boolean.FALSE) .value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, Boolean.FALSE)
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE) .value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.FALSE)
.build() .build()
) )
.build(); .build();
@@ -151,7 +134,7 @@ public enum ProtocolUtils {
BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY}; BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY};
private static final QuietDecoderException BAD_VARINT_CACHED = private static final QuietDecoderException BAD_VARINT_CACHED =
new QuietDecoderException("Bad VarInt decoded"); new QuietDecoderException("Bad VarInt decoded");
private static final int[] VAR_INT_LENGTHS = new int[33]; private static final int[] VAR_INT_LENGTHS = new int[65];
static { static {
for (int i = 0; i <= 32; ++i) { for (int i = 0; i <= 32; ++i) {
@@ -160,9 +143,6 @@ 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;
@@ -254,15 +234,16 @@ public enum ProtocolUtils {
} }
/** /**
* Directly encodes a 21-bit Minecraft VarInt, ready to be written with {@link ByteBuf#writeMedium(int)}. * Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}.
* The upper 11 bits will be discarded. * The upper 11 bits will be discarded.
* *
* @param value the value to encode * @param buf the buffer to read from
* @return the encoded value * @param value the integer to write
*/ */
public static int encode21BitVarInt(int value) { public static void write21BitVarInt(ByteBuf buf, int value) {
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
return (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14); int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
buf.writeMedium(w);
} }
public static String readString(ByteBuf buf) { public static String readString(ByteBuf buf) {
@@ -291,22 +272,12 @@ public enum ProtocolUtils {
checkFrame(buf.isReadable(length), checkFrame(buf.isReadable(length),
"Trying to read a string that is too long (wanted %s, only have %s)", length, "Trying to read a string that is too long (wanted %s, only have %s)", length,
buf.readableBytes()); buf.readableBytes());
String str = buf.readString(length, StandardCharsets.UTF_8); String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
buf.skipBytes(length);
checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap); checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap);
return str; return str;
} }
/**
* Determines the size of the written {@code str} if encoded as a VarInt-prefixed UTF-8 string.
*
* @param str the string to write
* @return the encoded size
*/
public static int stringSizeHint(CharSequence str) {
int size = ByteBufUtil.utf8Bytes(str);
return varIntBytes(size) + size;
}
/** /**
* Writes the specified {@code str} to the {@code buf} with a VarInt prefix. * Writes the specified {@code str} to the {@code buf} with a VarInt prefix.
* *
@@ -339,16 +310,6 @@ public enum ProtocolUtils {
writeString(buf, key.asString()); writeString(buf, key.asString());
} }
/**
* Writes the key to the buffer, dropping the "minecraft:" namespace when present.
*
* @param buf the buffer to write to
* @param key the key to write
*/
public static void writeMinimalKey(ByteBuf buf, Key key) {
writeString(buf, key.asMinimalString());
}
/** /**
* Reads a standard Mojang Text namespaced:key array from the buffer. * Reads a standard Mojang Text namespaced:key array from the buffer.
* *
@@ -419,10 +380,7 @@ public enum ProtocolUtils {
*/ */
public static int[] readIntegerArray(ByteBuf buf) { public static int[] readIntegerArray(ByteBuf buf) {
int len = readVarInt(buf); int len = readVarInt(buf);
checkFrame(len >= 0, "Got a negative-length integer array (%s)", len); checkArgument(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);
@@ -542,10 +500,6 @@ 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);
@@ -657,9 +611,6 @@ 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);
@@ -824,63 +775,6 @@ public enum ProtocolUtils {
return new IdentifiedKeyImpl(revision, key, expiry, signature); return new IdentifiedKeyImpl(revision, key, expiry, signature);
} }
/**
* Reads a {@link Sound.Source} from the buffer.
*
* @param buf the buffer
* @param version the protocol version
* @return the sound source
*/
public static Sound.Source readSoundSource(ByteBuf buf, ProtocolVersion version) {
int ordinal = readVarInt(buf);
if (version.lessThan(ProtocolVersion.MINECRAFT_1_21_5)
&& ordinal == Sound.Source.UI.ordinal()) {
throw new UnsupportedOperationException("UI sound-source is only supported in 1.21.5+");
}
return Sound.Source.values()[ordinal];
}
/**
* Writes a {@link Sound.Source} to the buffer.
*
* @param buf the buffer
* @param version the protocol version
* @param source the sound source to write
*/
public static void writeSoundSource(ByteBuf buf, ProtocolVersion version, Sound.Source source) {
if (version.lessThan(ProtocolVersion.MINECRAFT_1_21_5)
&& source == Sound.Source.UI) {
throw new UnsupportedOperationException("UI sound-source is only supported in 1.21.5+");
}
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.
*/ */
@@ -888,4 +782,4 @@ public enum ProtocolUtils {
SERVERBOUND, SERVERBOUND,
CLIENTBOUND CLIENTBOUND
} }
} }
@@ -41,12 +41,10 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_9;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
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;
@@ -60,11 +58,7 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket;
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
import com.velocitypowered.proxy.protocol.packet.DialogClearPacket;
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket; import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket; import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
@@ -87,7 +81,6 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket; import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket;
import com.velocitypowered.proxy.protocol.packet.ServerboundCustomClickActionPacket;
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket; import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
import com.velocitypowered.proxy.protocol.packet.StatusPingPacket; import com.velocitypowered.proxy.protocol.packet.StatusPingPacket;
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket; import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
@@ -108,8 +101,6 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.UnsignedPlayerComm
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket; import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket; import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductAcceptPacket;
import com.velocitypowered.proxy.protocol.packet.config.CodeOfConductPacket;
import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdatePacket;
import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket; import com.velocitypowered.proxy.protocol.packet.config.KnownPacksPacket;
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket; import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
@@ -192,12 +183,6 @@ public enum StateRegistry {
KnownPacksPacket.class, KnownPacksPacket.class,
KnownPacksPacket::new, KnownPacksPacket::new,
map(0x07, MINECRAFT_1_20_5, false)); map(0x07, MINECRAFT_1_20_5, false));
serverbound.register(ServerboundCustomClickActionPacket.class, ServerboundCustomClickActionPacket::new,
map(0x08, MINECRAFT_1_21_6, false));
serverbound.register(
CodeOfConductAcceptPacket.class,
() -> CodeOfConductAcceptPacket.INSTANCE,
map(0x09, MINECRAFT_1_21_9, false));
clientbound.register( clientbound.register(
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
@@ -252,12 +237,6 @@ public enum StateRegistry {
map(0x0F, MINECRAFT_1_21, false)); map(0x0F, MINECRAFT_1_21, false));
clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new, clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new,
map(0x10, MINECRAFT_1_21, false)); map(0x10, MINECRAFT_1_21, false));
clientbound.register(DialogClearPacket.class, () -> DialogClearPacket.INSTANCE,
map(0x11, MINECRAFT_1_21_6, false));
clientbound.register(DialogShowPacket.class, () -> new DialogShowPacket(this),
map(0x12, MINECRAFT_1_21_6, false));
clientbound.register(CodeOfConductPacket.class, CodeOfConductPacket::new,
map(0x13, MINECRAFT_1_21_9, false));
} }
}, },
PLAY { PLAY {
@@ -279,8 +258,7 @@ 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,
@@ -294,8 +272,7 @@ 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));
@@ -306,21 +283,18 @@ 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,
@@ -336,14 +310,12 @@ 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,
@@ -362,8 +334,7 @@ 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,
@@ -383,8 +354,7 @@ 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,
@@ -402,15 +372,13 @@ 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,
@@ -462,28 +430,6 @@ public enum StateRegistry {
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new, ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
map(0x16, MINECRAFT_1_20_5, false), map(0x16, MINECRAFT_1_20_5, false),
map(0x15, MINECRAFT_1_21_5, false)); map(0x15, MINECRAFT_1_21_5, false));
clientbound.register(
ClientboundSoundEntityPacket.class, ClientboundSoundEntityPacket::new,
map(0x5D, MINECRAFT_1_19_3, true),
map(0x61, MINECRAFT_1_19_4, true),
map(0x63, MINECRAFT_1_20_2, true),
map(0x65, MINECRAFT_1_20_3, true),
map(0x67, MINECRAFT_1_20_5, true),
map(0x6E, MINECRAFT_1_21_2, true),
map(0x6D, MINECRAFT_1_21_5, true),
map(0x72, MINECRAFT_1_21_9, true),
map(0x74, MINECRAFT_26_1, true));
clientbound.register(
ClientboundStopSoundPacket.class, ClientboundStopSoundPacket::new,
map(0x5F, MINECRAFT_1_19_3, true),
map(0x63, MINECRAFT_1_19_4, true),
map(0x66, MINECRAFT_1_20_2, true),
map(0x68, MINECRAFT_1_20_3, true),
map(0x6A, MINECRAFT_1_20_5, true),
map(0x71, MINECRAFT_1_21_2, true),
map(0x70, MINECRAFT_1_21_5, 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,
@@ -519,8 +465,7 @@ public enum StateRegistry {
map(0x1A, MINECRAFT_1_19_4, false), map(0x1A, MINECRAFT_1_19_4, false),
map(0x1B, MINECRAFT_1_20_2, false), map(0x1B, MINECRAFT_1_20_2, false),
map(0x1D, MINECRAFT_1_20_5, false), map(0x1D, MINECRAFT_1_20_5, false),
map(0x1C, MINECRAFT_1_21_5, false), map(0x1C, MINECRAFT_1_21_5, false));
map(0x20, MINECRAFT_1_21_9, false));
clientbound.register( clientbound.register(
KeepAlivePacket.class, KeepAlivePacket.class,
KeepAlivePacket::new, KeepAlivePacket::new,
@@ -539,9 +484,7 @@ public enum StateRegistry {
map(0x24, MINECRAFT_1_20_2, false), map(0x24, MINECRAFT_1_20_2, false),
map(0x26, MINECRAFT_1_20_5, false), map(0x26, MINECRAFT_1_20_5, false),
map(0x27, MINECRAFT_1_21_2, false), map(0x27, MINECRAFT_1_21_2, false),
map(0x26, MINECRAFT_1_21_5, false), map(0x26, MINECRAFT_1_21_5, 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,
@@ -560,9 +503,7 @@ public enum StateRegistry {
map(0x29, MINECRAFT_1_20_2, false), map(0x29, MINECRAFT_1_20_2, false),
map(0x2B, MINECRAFT_1_20_5, false), map(0x2B, MINECRAFT_1_20_5, false),
map(0x2C, MINECRAFT_1_21_2, false), map(0x2C, MINECRAFT_1_21_2, false),
map(0x2B, MINECRAFT_1_21_5, false), map(0x2B, MINECRAFT_1_21_5, 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,
@@ -584,18 +525,14 @@ public enum StateRegistry {
map(0x45, MINECRAFT_1_20_3, true), map(0x45, MINECRAFT_1_20_3, true),
map(0x47, MINECRAFT_1_20_5, true), map(0x47, MINECRAFT_1_20_5, true),
map(0x4C, MINECRAFT_1_21_2, true), map(0x4C, MINECRAFT_1_21_2, true),
map(0x4B, MINECRAFT_1_21_5, true), map(0x4B, MINECRAFT_1_21_5, 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,
map(0x43, MINECRAFT_1_20_3, false), map(0x43, MINECRAFT_1_20_3, false),
map(0x45, MINECRAFT_1_20_5, false), map(0x45, MINECRAFT_1_20_5, false),
map(0x4A, MINECRAFT_1_21_2, false), map(0x4A, MINECRAFT_1_21_2, false),
map(0x49, MINECRAFT_1_21_5, false), map(0x49, MINECRAFT_1_21_5, 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,
@@ -617,9 +554,7 @@ public enum StateRegistry {
map(0x44, MINECRAFT_1_20_3, false), map(0x44, MINECRAFT_1_20_3, false),
map(0x46, MINECRAFT_1_20_5, false), map(0x46, MINECRAFT_1_20_5, false),
map(0x4B, MINECRAFT_1_21_2, false), map(0x4B, MINECRAFT_1_21_2, false),
map(0x4A, MINECRAFT_1_21_5, false), map(0x4A, MINECRAFT_1_21_5, 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,
@@ -642,9 +577,7 @@ public enum StateRegistry {
map(0x6A, MINECRAFT_1_20_3, true), map(0x6A, MINECRAFT_1_20_3, true),
map(0x6D, MINECRAFT_1_20_5, true), map(0x6D, MINECRAFT_1_20_5, true),
map(0x74, MINECRAFT_1_21_2, true), map(0x74, MINECRAFT_1_21_2, true),
map(0x73, MINECRAFT_1_21_5, true), map(0x73, MINECRAFT_1_21_5, 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,
@@ -666,9 +599,7 @@ public enum StateRegistry {
map(0x61, MINECRAFT_1_20_3, true), map(0x61, MINECRAFT_1_20_3, true),
map(0x63, MINECRAFT_1_20_5, true), map(0x63, MINECRAFT_1_20_5, true),
map(0x6A, MINECRAFT_1_21_2, true), map(0x6A, MINECRAFT_1_21_2, true),
map(0x69, MINECRAFT_1_21_5, true), map(0x69, MINECRAFT_1_21_5, 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,
@@ -681,9 +612,7 @@ public enum StateRegistry {
map(0x63, MINECRAFT_1_20_3, true), map(0x63, MINECRAFT_1_20_3, true),
map(0x65, MINECRAFT_1_20_5, true), map(0x65, MINECRAFT_1_20_5, true),
map(0x6C, MINECRAFT_1_21_2, true), map(0x6C, MINECRAFT_1_21_2, true),
map(0x6B, MINECRAFT_1_21_5, true), map(0x6B, MINECRAFT_1_21_5, 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,
@@ -696,9 +625,7 @@ public enum StateRegistry {
map(0x4A, MINECRAFT_1_20_3, true), map(0x4A, MINECRAFT_1_20_3, true),
map(0x4C, MINECRAFT_1_20_5, true), map(0x4C, MINECRAFT_1_20_5, true),
map(0x51, MINECRAFT_1_21_2, true), map(0x51, MINECRAFT_1_21_2, true),
map(0x50, MINECRAFT_1_21_5, true), map(0x50, MINECRAFT_1_21_5, 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,
@@ -711,9 +638,7 @@ public enum StateRegistry {
map(0x64, MINECRAFT_1_20_3, true), map(0x64, MINECRAFT_1_20_3, true),
map(0x66, MINECRAFT_1_20_5, true), map(0x66, MINECRAFT_1_20_5, true),
map(0x6D, MINECRAFT_1_21_2, true), map(0x6D, MINECRAFT_1_21_2, true),
map(0x6C, MINECRAFT_1_21_5, true), map(0x6C, MINECRAFT_1_21_5, 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,
@@ -743,9 +668,7 @@ public enum StateRegistry {
map(0x3B, MINECRAFT_1_20_2, false), map(0x3B, MINECRAFT_1_20_2, false),
map(0x3D, MINECRAFT_1_20_5, false), map(0x3D, MINECRAFT_1_20_5, false),
map(0x3F, MINECRAFT_1_21_2, false), map(0x3F, MINECRAFT_1_21_2, false),
map(0x3E, MINECRAFT_1_21_5, false), map(0x3E, MINECRAFT_1_21_5, 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,
@@ -754,16 +677,12 @@ public enum StateRegistry {
map(0x3C, MINECRAFT_1_20_2, false), map(0x3C, MINECRAFT_1_20_2, false),
map(0x3E, MINECRAFT_1_20_5, false), map(0x3E, MINECRAFT_1_20_5, false),
map(0x40, MINECRAFT_1_21_2, false), map(0x40, MINECRAFT_1_21_2, false),
map(0x3F, MINECRAFT_1_21_5, false), map(0x3F, MINECRAFT_1_21_5, 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(0x78, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
SystemChatPacket.class, SystemChatPacket.class,
SystemChatPacket::new, SystemChatPacket::new,
@@ -775,9 +694,7 @@ public enum StateRegistry {
map(0x69, MINECRAFT_1_20_3, true), map(0x69, MINECRAFT_1_20_3, true),
map(0x6C, MINECRAFT_1_20_5, true), map(0x6C, MINECRAFT_1_20_5, true),
map(0x73, MINECRAFT_1_21_2, true), map(0x73, MINECRAFT_1_21_2, true),
map(0x72, MINECRAFT_1_21_5, true), map(0x72, MINECRAFT_1_21_5, 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,
@@ -798,9 +715,7 @@ public enum StateRegistry {
map(0x49, MINECRAFT_1_20_3, false), map(0x49, MINECRAFT_1_20_3, false),
map(0x4B, MINECRAFT_1_20_5, false), map(0x4B, MINECRAFT_1_20_5, false),
map(0x50, MINECRAFT_1_21_2, false), map(0x50, MINECRAFT_1_21_2, false),
map(0x4F, MINECRAFT_1_21_5, false), map(0x4F, MINECRAFT_1_21_5, 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,
@@ -808,9 +723,7 @@ public enum StateRegistry {
map(0x67, MINECRAFT_1_20_3, false), map(0x67, MINECRAFT_1_20_3, false),
map(0x69, MINECRAFT_1_20_5, false), map(0x69, MINECRAFT_1_20_5, false),
map(0x70, MINECRAFT_1_21_2, false), map(0x70, MINECRAFT_1_21_2, false),
map(0x6F, MINECRAFT_1_21_5, false), map(0x6F, MINECRAFT_1_21_5, 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,
@@ -819,23 +732,17 @@ public enum StateRegistry {
TransferPacket.class, TransferPacket.class,
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(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(0x88, MINECRAFT_26_1, false));
clientbound.register( clientbound.register(
ClientboundServerLinksPacket.class, ClientboundServerLinksPacket.class,
ClientboundServerLinksPacket::new, ClientboundServerLinksPacket::new,
map(0x7B, MINECRAFT_1_21, false), map(0x7B, MINECRAFT_1_21, false),
map(0x82, MINECRAFT_1_21_2, false), map(0x82, MINECRAFT_1_21_2, false));
map(0x87, MINECRAFT_1_21_9, false),
map(0x89, MINECRAFT_26_1, false));
} }
}, },
LOGIN { LOGIN {
@@ -864,7 +771,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,9 +132,10 @@ 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);
@@ -189,10 +190,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
}
} }
} }
@@ -22,49 +22,31 @@ import static com.velocitypowered.natives.util.MoreByteBufUtils.preferredBuffer;
import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame; import static com.velocitypowered.proxy.protocol.util.NettyPreconditions.checkFrame;
import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressor;
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.util.except.QuietDecoderException;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List; import java.util.List;
import org.jspecify.annotations.Nullable;
/** /**
* Decompresses a Minecraft packet. * Decompresses a Minecraft packet.
*/ */
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 CLIENTBOUND_UNCOMPRESSED_CAP = private static final int UNCOMPRESSED_CAP =
Boolean.getBoolean("velocity.increased-compression-cap") Boolean.getBoolean("velocity.increased-compression-cap")
? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE; ? HARD_MAXIMUM_UNCOMPRESSED_SIZE : VANILLA_MAXIMUM_UNCOMPRESSED_SIZE;
private static final 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 final ProtocolUtils.Direction direction;
private int threshold; private int threshold;
private final VelocityCompressor compressor; private final VelocityCompressor compressor;
@Nullable
private PacketLimiter packetLimiter;
/** 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
@@ -77,35 +59,20 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
+ " threshold %s", actualUncompressedSize, threshold); + " threshold %s", actualUncompressedSize, threshold);
} }
// This message is not compressed. // This message is not compressed.
if (packetLimiter != null && !packetLimiter.account(in.readableBytes())) {
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
.formatted(ctx.channel().remoteAddress()));
}
out.add(in.retain()); out.add(in.retain());
return; return;
} }
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);
if (direction == ProtocolUtils.Direction.CLIENTBOUND) { checkFrame(claimedUncompressedSize <= UNCOMPRESSED_CAP,
checkFrame(claimedUncompressedSize <= CLIENTBOUND_UNCOMPRESSED_CAP, "Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize, UNCOMPRESSED_CAP);
CLIENTBOUND_UNCOMPRESSED_CAP);
} else {
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
SERVERBOUND_UNCOMPRESSED_CAP);
}
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);
if (packetLimiter != null && !packetLimiter.account(claimedUncompressedSize)) {
throw new QuietDecoderException("Rate limit exceeded while processing packets for %s"
.formatted(ctx.channel().remoteAddress()));
}
out.add(uncompressed); out.add(uncompressed);
} catch (Exception e) { } catch (Exception e) {
uncompressed.release(); uncompressed.release();
@@ -123,8 +90,4 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
public void setThreshold(int threshold) { public void setThreshold(int threshold) {
this.threshold = threshold; this.threshold = threshold;
} }
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
this.packetLimiter = packetLimiter;
}
} }
@@ -46,7 +46,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
if (uncompressed < threshold) { if (uncompressed < threshold) {
// Under the threshold, there is nothing to do. // Under the threshold, there is nothing to do.
ProtocolUtils.writeVarInt(out, uncompressed + 1); ProtocolUtils.writeVarInt(out, uncompressed + 1);
out.writeByte(0); ProtocolUtils.writeVarInt(out, 0);
out.writeBytes(msg); out.writeBytes(msg);
} else { } else {
handleCompressed(ctx, msg, out); handleCompressed(ctx, msg, out);
@@ -57,7 +57,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
throws DataFormatException { throws DataFormatException {
int uncompressed = msg.readableBytes(); int uncompressed = msg.readableBytes();
out.writeMedium(0); // Reserve the packet length ProtocolUtils.write21BitVarInt(out, 0); // Dummy packet length
ProtocolUtils.writeVarInt(out, uncompressed); ProtocolUtils.writeVarInt(out, uncompressed);
ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg); ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
@@ -72,8 +72,11 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet."); throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
} }
int writerIndex = out.writerIndex();
int packetLength = out.readableBytes() - 3; int packetLength = out.readableBytes() - 3;
out.setMedium(0, ProtocolUtils.encode21BitVarInt(packetLength)); // Rewrite packet length out.writerIndex(0);
ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length
out.writerIndex(writerIndex);
} }
@Override @Override
@@ -57,11 +57,7 @@ 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) {
try { tryDecode(ctx, buf);
tryDecode(ctx, buf);
} finally {
buf.release();
}
} else { } else {
ctx.fireChannelRead(msg); ctx.fireChannelRead(msg);
} }
@@ -69,6 +65,7 @@ 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;
} }
@@ -77,34 +74,35 @@ 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);
if (this.direction == ProtocolUtils.Direction.SERVERBOUND && this.state != StateRegistry.PLAY) { ctx.fireChannelRead(buf);
throw this.handleInvalidPacketId(packetId);
}
ctx.fireChannelRead(buf.retain());
} else { } else {
doLengthSanityChecks(buf, packet);
try { try {
packet.decode(buf, direction, registry.version); doLengthSanityChecks(buf, packet);
} catch (Exception e) {
throw handleDecodeFailure(e, packet, packetId);
}
if (buf.isReadable()) { try {
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex()); 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(packet);
} }
} }
private void doLengthSanityChecks(ByteBuf buf, MinecraftPacket packet) throws Exception { private void doLengthSanityChecks(ByteBuf buf, MinecraftPacket packet) throws Exception {
int expectedMinLen = packet.decodeExpectedMinLength(buf, direction, registry.version); int expectedMinLen = packet.expectedMinLength(buf, direction, registry.version);
int expectedMaxLen = packet.decodeExpectedMaxLength(buf, direction, registry.version); int expectedMaxLen = packet.expectedMaxLength(buf, direction, registry.version);
if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) { if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes()); throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
} }
if (buf.readableBytes() < expectedMinLen) { if (buf.readableBytes() < expectedMinLen) {
throw handleUnderflow(packet, expectedMinLen, buf.readableBytes()); throw handleUnderflow(packet, expectedMaxLen, buf.readableBytes());
} }
} }
@@ -135,14 +133,6 @@ 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);
@@ -54,19 +54,6 @@ public class MinecraftEncoder extends MessageToByteEncoder<MinecraftPacket> {
msg.encode(out, direction, registry.version); msg.encode(out, direction, registry.version);
} }
@Override
protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, MinecraftPacket msg,
boolean preferDirect) throws Exception {
int hint = msg.encodeSizeHint(direction, registry.version);
if (hint < 0) {
return super.allocateBuffer(ctx, msg, preferDirect);
}
int packetId = this.registry.getPacketId(msg);
int totalHint = ProtocolUtils.varIntBytes(packetId) + hint;
return preferDirect ? ctx.alloc().ioBuffer(totalHint) : ctx.alloc().heapBuffer(totalHint);
}
public void setProtocolVersion(final ProtocolVersion protocolVersion) { public void setProtocolVersion(final ProtocolVersion protocolVersion) {
this.registry = state.getProtocolRegistry(direction, protocolVersion); this.registry = state.getProtocolRegistry(direction, protocolVersion);
} }
@@ -20,7 +20,6 @@ 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;
@@ -33,7 +32,6 @@ 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.
@@ -46,8 +44,6 @@ 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 =
@@ -56,8 +52,6 @@ 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}.
@@ -80,93 +74,69 @@ 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
try { in.markReaderIndex();
int length = readRawVarInt21(in); int preIndex = in.readerIndex();
if (packetStart == in.readerIndex()) { int length = readRawVarInt21(in);
return; if (preIndex == in.readerIndex()) {
} return;
if (length < 0) { }
throw BAD_PACKET_LENGTH; if (length < 0) {
} 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)) { StateRegistry.PacketRegistry.ProtocolRegistry registry =
in.readerIndex(packetStart); state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION);
return;
} final int index = in.readerIndex();
final int packetId = readRawVarInt21(in);
// Index hasn't changed, we've read nothing
if (index == in.readerIndex()) {
in.resetReaderIndex();
return;
} }
} final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
// note that zero-length packets are ignored MinecraftPacket packet = registry.createPacket(packetId);
if (length > 0) {
if (in.readableBytes() < length) { // We handle every packet in this phase, if you said something we don't know, something is really wrong
in.readerIndex(packetStart); if (packet == null) {
} else { throw UNKNOWN_PACKET;
// 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));
} }
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse
// the packet if needed, so, we'll take advantage of the existing methods
int expectedMinLen = packet.expectedMinLength(in, direction, registry.version);
int expectedMaxLen = packet.expectedMaxLength(in, direction, registry.version);
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
}
if (payloadLength < expectedMinLen) {
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes());
}
in.readerIndex(index);
} }
} catch (Exception e) {
// Reset buffer to consistent state before propagating exception to prevent memory leaks
in.readerIndex(packetStart);
throw e;
}
}
private boolean validateServerboundHandshakePacket(ByteBuf in, int length) throws Exception {
StateRegistry.PacketRegistry.ProtocolRegistry registry =
state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION);
final int index = in.readerIndex();
final int packetId = readRawVarInt21(in);
// Index hasn't changed, we've read nothing
if (index == in.readerIndex()) {
return true;
}
final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
MinecraftPacket packet = registry.createPacket(packetId);
// We handle every packet in this phase, if you said something we don't know, something is really wrong
if (packet == null) {
throw UNKNOWN_PACKET;
} }
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse // note that zero-length packets are ignored
// the packet if needed, so, we'll take advantage of the existing methods if (length > 0) {
int expectedMinLen = packet.decodeExpectedMinLength(in, direction, registry.version); if (in.readableBytes() < length) {
int expectedMaxLen = packet.decodeExpectedMaxLength(in, direction, registry.version); in.resetReaderIndex();
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) { } else {
throw handleOverflow(packet, expectedMaxLen, payloadLength); out.add(in.readRetainedSlice(length));
}
} }
if (payloadLength < expectedMinLen) {
throw handleUnderflow(packet, expectedMinLen, payloadLength);
}
in.readerIndex(index);
return false;
} }
@Override @Override
@@ -270,8 +240,4 @@ 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,9 +21,6 @@ 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;
@@ -44,13 +41,8 @@ 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.
@@ -72,20 +64,6 @@ 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);
} }
@@ -112,6 +90,5 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
ReferenceCountUtil.release(msg); ReferenceCountUtil.release(msg);
} }
} }
this.queueSize = 0;
} }
} }
@@ -25,6 +25,7 @@ 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;
@@ -44,9 +45,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;
@@ -85,14 +86,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);
List<WireNode> wireNodes = ProtocolUtils.newList(commands); WireNode[] wireNodes = new WireNode[commands];
for (int i = 0; i < commands; i++) { for (int i = 0; i < commands; i++) {
wireNodes.add(deserializeNode(buf, i, protocolVersion)); wireNodes[i] = 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<>(wireNodes); Queue<WireNode> nodeQueue = new ArrayDeque<>(Arrays.asList(wireNodes));
while (!nodeQueue.isEmpty()) { while (!nodeQueue.isEmpty()) {
boolean cycling = false; boolean cycling = false;
@@ -111,7 +112,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
} }
int rootIdx = ProtocolUtils.readVarInt(buf); int rootIdx = ProtocolUtils.readVarInt(buf);
rootNode = (RootCommandNode<CommandSource>) wireNodes.get(rootIdx).built; rootNode = (RootCommandNode<CommandSource>) wireNodes[rootIdx].built;
} }
@Override @Override
@@ -245,17 +246,17 @@ public class AvailableCommandsPacket implements MinecraftPacket {
this.validated = false; this.validated = false;
} }
void validate(List<WireNode> wireNodes) { void validate(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.size()) { if (child < 0 || child >= wireNodes.length) {
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.size()) { if (redirectTo < 0 || redirectTo >= wireNodes.length) {
throw new IllegalStateException("Redirect node points to non-existent index " throw new IllegalStateException("Redirect node points to non-existent index "
+ redirectTo); + redirectTo);
} }
@@ -264,7 +265,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
this.validated = true; this.validated = true;
} }
boolean toNode(List<WireNode> wireNodes) { boolean toNode(WireNode[] wireNodes) {
if (!this.validated) { if (!this.validated) {
this.validate(wireNodes); this.validate(wireNodes);
} }
@@ -280,7 +281,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
// Add any redirects // Add any redirects
if (redirectTo != -1) { if (redirectTo != -1) {
WireNode redirect = wireNodes.get(redirectTo); WireNode redirect = wireNodes[redirectTo];
if (redirect.built != null) { if (redirect.built != null) {
args.redirect(redirect.built); args.redirect(redirect.built);
} else { } else {
@@ -304,7 +305,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
} }
for (int child : children) { for (int child : children) {
if (wireNodes.get(child).built == null) { if (wireNodes[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;
} }
@@ -312,7 +313,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.get(child).built; CommandNode<CommandSource> childNode = wireNodes[child].built;
if (!(childNode instanceof RootCommandNode)) { if (!(childNode instanceof RootCommandNode)) {
built.addChild(childNode); built.addChild(childNode);
} }
@@ -330,10 +331,12 @@ public class AvailableCommandsPacket implements MinecraftPacket {
.add("redirectTo", redirectTo); .add("redirectTo", redirectTo);
if (args != null) { if (args != null) {
if (args instanceof LiteralArgumentBuilder literal) { if (args instanceof LiteralArgumentBuilder) {
helper.add("argsLabel", literal.getLiteral()); helper.add("argsLabel",
} else if (args instanceof RequiredArgumentBuilder required) { ((LiteralArgumentBuilder<CommandSource>) args).getLiteral());
helper.add("argsName", required.getName()); } else if (args instanceof RequiredArgumentBuilder) {
helper.add("argsName",
((RequiredArgumentBuilder<CommandSource, ?>) args).getName());
} }
} }
@@ -345,20 +348,18 @@ 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 record ProtocolSuggestionProvider(String name) implements SuggestionProvider<CommandSource> { public static class ProtocolSuggestionProvider 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) { SuggestionsBuilder builder) throws CommandSyntaxException {
return builder.buildFuture(); return builder.buildFuture();
} }
} }
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// This is a very complex packet to encode. Paper 1.21.10 + Velocity with Spark has a size of
// 30,334, but this is likely on the lower side. We'll use 128KiB as a more realistically-sized
// amount.
return 128 * 1024;
}
} }
@@ -207,22 +207,30 @@ 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:
case UPDATE_PERCENT -> this.percent = buf.readFloat(); break;
case UPDATE_NAME -> this.name = ComponentHolder.read(buf, version); case UPDATE_PERCENT:
case UPDATE_STYLE -> { this.percent = buf.readFloat();
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 -> this.flags = buf.readUnsignedByte(); case UPDATE_PROPERTIES:
default -> throw new UnsupportedOperationException("Unknown action " + action); this.flags = buf.readUnsignedByte();
break;
default:
throw new UnsupportedOperationException("Unknown action " + action);
} }
} }
@@ -234,30 +242,36 @@ 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);
case UPDATE_PROPERTIES -> buf.writeByte(flags); break;
default -> throw new UnsupportedOperationException("Unknown action " + action); case REMOVE:
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,16 +206,6 @@ 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) {
@@ -247,7 +237,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
difficulty, difficulty,
skinParts, skinParts,
mainHand, mainHand,
textFilteringEnabled, textFilteringEnabled,
clientListingAllowed, clientListingAllowed,
particleStatus); particleStatus);
} }
@@ -1,101 +0,0 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.sound.Sound;
import org.jetbrains.annotations.Nullable;
import java.util.Random;
public class ClientboundSoundEntityPacket implements MinecraftPacket {
private static final Random SEEDS_RANDOM = new Random();
private Sound sound;
private @Nullable Float fixedRange;
private int emitterEntityId;
public ClientboundSoundEntityPacket() {}
public ClientboundSoundEntityPacket(Sound sound, @Nullable Float fixedRange, int emitterEntityId) {
this.sound = sound;
this.fixedRange = fixedRange;
this.emitterEntityId = emitterEntityId;
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
throw new UnsupportedOperationException("Decode is not implemented");
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
ProtocolUtils.writeVarInt(buf, 0); // version-dependent, hardcoded sound ID
ProtocolUtils.writeMinimalKey(buf, sound.name());
buf.writeBoolean(fixedRange != null);
if (fixedRange != null)
buf.writeFloat(fixedRange);
ProtocolUtils.writeSoundSource(buf, protocolVersion, sound.source());
ProtocolUtils.writeVarInt(buf, emitterEntityId);
buf.writeFloat(sound.volume());
buf.writeFloat(sound.pitch());
buf.writeLong(sound.seed().orElse(SEEDS_RANDOM.nextLong()));
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
public Sound getSound() {
return sound;
}
public void setSound(Sound sound) {
this.sound = sound;
}
public @Nullable Float getFixedRange() {
return fixedRange;
}
public void setFixedRange(@Nullable Float fixedRange) {
this.fixedRange = fixedRange;
}
public int getEmitterEntityId() {
return emitterEntityId;
}
public void setEmitterEntityId(int emitterEntityId) {
this.emitterEntityId = emitterEntityId;
}
}
@@ -1,109 +0,0 @@
/*
* Copyright (C) 2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.sound.Sound;
import net.kyori.adventure.sound.SoundStop;
import javax.annotation.Nullable;
public class ClientboundStopSoundPacket implements MinecraftPacket {
private @Nullable Sound.Source source;
private @Nullable Key soundName;
public ClientboundStopSoundPacket() {}
public ClientboundStopSoundPacket(SoundStop soundStop) {
this(soundStop.source(), soundStop.sound());
}
public ClientboundStopSoundPacket(@Nullable Sound.Source source, @Nullable Key soundName) {
this.source = source;
this.soundName = soundName;
}
@Override
public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int flagsBitmask = buf.readByte();
if ((flagsBitmask & 1) != 0) {
source = ProtocolUtils.readSoundSource(buf, protocolVersion);
} else {
source = null;
}
if ((flagsBitmask & 2) != 0) {
soundName = ProtocolUtils.readKey(buf);
} else {
soundName = null;
}
}
@Override
public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) {
int flagsBitmask = 0;
if (source != null && soundName == null) {
flagsBitmask |= 1;
} else if (soundName != null && source == null) {
flagsBitmask |= 2;
} else if (source != null /*&& sound != null*/) {
flagsBitmask |= 3;
}
buf.writeByte(flagsBitmask);
if (source != null) {
ProtocolUtils.writeSoundSource(buf, protocolVersion, source);
}
if (soundName != null) {
ProtocolUtils.writeMinimalKey(buf, soundName);
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
@Nullable
public Sound.Source getSource() {
return source;
}
public void setSource(@Nullable Sound.Source source) {
this.source = source;
}
@Nullable
public Key getSoundName() {
return soundName;
}
public void setSoundName(@Nullable Key soundName) {
this.soundName = soundName;
}
}
@@ -1,45 +0,0 @@
/*
* Copyright (C) 2018-2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf;
public class DialogClearPacket implements MinecraftPacket {
public static final DialogClearPacket INSTANCE = new DialogClearPacket();
private DialogClearPacket() {
}
@Override
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
}
@Override
public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -1,64 +0,0 @@
/*
* Copyright (C) 2018-2025 Velocity Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.velocitypowered.proxy.protocol.packet;
import com.velocitypowered.api.network.ProtocolVersion;
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.StateRegistry;
import io.netty.buffer.ByteBuf;
import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.BinaryTagIO;
public class DialogShowPacket implements MinecraftPacket {
private final StateRegistry state;
private int id;
private BinaryTag nbt;
public DialogShowPacket(final StateRegistry state) {
this.state = state;
}
@Override
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
this.id = this.state == StateRegistry.CONFIG ? 0 : ProtocolUtils.readVarInt(buf);
if (this.id == 0) {
this.nbt = ProtocolUtils.readBinaryTag(buf, protocolVersion, BinaryTagIO.reader());
}
}
@Override
public void encode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
if (this.state == StateRegistry.CONFIG) {
ProtocolUtils.writeBinaryTag(buf, protocolVersion, this.nbt);
} else {
ProtocolUtils.writeVarInt(buf, this.id);
if (this.id == 0) {
ProtocolUtils.writeBinaryTag(buf, protocolVersion, this.nbt);
}
}
}
@Override
public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this);
}
}
@@ -107,7 +107,7 @@ public class EncryptionResponsePacket implements MinecraftPacket {
} }
@Override @Override
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) { public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
// It turns out these come out to the same length, whether we're talking >=1.8 or not. // It turns out these come out to the same length, whether we're talking >=1.8 or not.
// The length prefix always winds up being 2 bytes. // The length prefix always winds up being 2 bytes.
int base = 256 + 2 + 2; int base = 256 + 2 + 2;
@@ -123,8 +123,8 @@ public class EncryptionResponsePacket implements MinecraftPacket {
} }
@Override @Override
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) { public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
int base = decodeExpectedMaxLength(buf, direction, version); int base = expectedMaxLength(buf, direction, version);
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
// These are "optional" // These are "optional"
base -= 128 + 8; base -= 128 + 8;
@@ -24,7 +24,6 @@ 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;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
public class HandshakePacket implements MinecraftPacket { public class HandshakePacket implements MinecraftPacket {
@@ -109,21 +108,14 @@ public class HandshakePacket implements MinecraftPacket {
} }
@Override @Override
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, public int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) { ProtocolVersion version) {
return 7; return 7;
} }
@Override @Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) { ProtocolVersion version) {
return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3); return 9 + (MAXIMUM_HOSTNAME_LENGTH * 3);
} }
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
// We could compute an exact size, but 4KiB ought to be enough to encode all reasonable
// sizes of this packet.
return 4 * 1024;
}
} }
@@ -52,7 +52,6 @@ public class JoinGamePacket implements MinecraftPacket {
private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+ private @Nullable Pair<String, Long> lastDeathPosition; // 1.19+
private int portalCooldown; // 1.20+ private int portalCooldown; // 1.20+
private int seaLevel; // 1.21.2+ private int seaLevel; // 1.21.2+
private boolean onlineMode; // 26.2+
private boolean enforcesSecureChat; // 1.20.5+ private boolean enforcesSecureChat; // 1.20.5+
public int getEntityId() { public int getEntityId() {
@@ -191,10 +190,6 @@ public class JoinGamePacket implements MinecraftPacket {
this.seaLevel = seaLevel; this.seaLevel = seaLevel;
} }
public void setOnlineMode(boolean onlineMode) {
this.onlineMode = onlineMode;
}
public boolean getEnforcesSecureChat() { public boolean getEnforcesSecureChat() {
return this.enforcesSecureChat; return this.enforcesSecureChat;
} }
@@ -218,7 +213,7 @@ public class JoinGamePacket implements MinecraftPacket {
dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' + dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' +
", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance +
", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown + ", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown +
", seaLevel=" + seaLevel + ", onlineMode=" + this.onlineMode + ", seaLevel=" + seaLevel +
'}'; '}';
} }
@@ -363,10 +358,6 @@ public class JoinGamePacket implements MinecraftPacket {
this.seaLevel = ProtocolUtils.readVarInt(buf); this.seaLevel = ProtocolUtils.readVarInt(buf);
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
this.onlineMode = buf.readBoolean();
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
this.enforcesSecureChat = buf.readBoolean(); this.enforcesSecureChat = buf.readBoolean();
} }
@@ -519,10 +510,6 @@ public class JoinGamePacket implements MinecraftPacket {
ProtocolUtils.writeVarInt(buf, seaLevel); ProtocolUtils.writeVarInt(buf, seaLevel);
} }
if (version.noLessThan(ProtocolVersion.MINECRAFT_26_2)) {
buf.writeBoolean(this.onlineMode);
}
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
buf.writeBoolean(this.enforcesSecureChat); buf.writeBoolean(this.enforcesSecureChat);
} }
@@ -64,28 +64,6 @@ 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,25 +69,33 @@ 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 -> item.setGameMode(ProtocolUtils.readVarInt(buf)); case UPDATE_GAMEMODE:
case UPDATE_LATENCY -> item.setLatency(ProtocolUtils.readVarInt(buf)); item.setGameMode(ProtocolUtils.readVarInt(buf));
case UPDATE_DISPLAY_NAME -> item.setDisplayName(readOptionalComponent(buf, version)); break;
case REMOVE_PLAYER -> { case UPDATE_LATENCY:
//Do nothing, all that is needed is the uuid item.setLatency(ProtocolUtils.readVarInt(buf));
} break;
default -> throw new UnsupportedOperationException("Unknown action " + action); case UPDATE_DISPLAY_NAME:
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 {
@@ -118,32 +126,39 @@ 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 -> ProtocolUtils.writeVarInt(buf, item.getGameMode()); case UPDATE_GAMEMODE:
case UPDATE_LATENCY -> ProtocolUtils.writeVarInt(buf, item.getLatency()); ProtocolUtils.writeVarInt(buf, item.getGameMode());
case UPDATE_DISPLAY_NAME -> writeDisplayName(buf, item.getDisplayName(), version); break;
case REMOVE_PLAYER -> { case UPDATE_LATENCY:
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 -> throw new UnsupportedOperationException("Unknown action " + action); default:
throw new UnsupportedOperationException("Unknown action " + action);
} }
} }
} else { } else {
Item item = items.getFirst(); Item item = items.get(0);
Component displayNameComponent = item.getDisplayName(); Component displayNameComponent = item.getDisplayName();
if (displayNameComponent != null) { if (displayNameComponent != null) {
String displayName = LegacyComponentSerializer.legacySection() String displayName = LegacyComponentSerializer.legacySection()
@@ -254,7 +269,7 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
return this; return this;
} }
public @Nullable IdentifiedKey getPlayerKey() { public IdentifiedKey getPlayerKey() {
return playerKey; return playerKey;
} }
} }
@@ -36,7 +36,7 @@ public class LoginAcknowledgedPacket implements MinecraftPacket {
} }
@Override @Override
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
ProtocolVersion version) { ProtocolVersion version) {
return 0; return 0;
} }
@@ -21,7 +21,6 @@ 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;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
@@ -87,9 +86,4 @@ public class LoginPluginMessagePacket extends DeferredByteBufHolder implements M
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
} }
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
} }
@@ -21,7 +21,6 @@ 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;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
@@ -89,9 +88,4 @@ public class LoginPluginResponsePacket extends DeferredByteBufHolder implements
public boolean handle(MinecraftSessionHandler handler) { public boolean handle(MinecraftSessionHandler handler) {
return handler.handle(this); return handler.handle(this);
} }
@Override
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
return content().readableBytes();
}
} }
@@ -42,16 +42,6 @@ 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);

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