Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cdeabe91d0 | |||
| 9c0c9b0218 | |||
| ad8de4361c | |||
| f712997dd7 | |||
| a6d97e28ad | |||
| 339a4c1887 | |||
| 1a41b77ccb | |||
| e834af9cf1 | |||
| affc1d6e08 | |||
| 6ce432e4ae | |||
| b1a1b8bda3 | |||
| 6f01587318 | |||
| 308ce6d992 | |||
| 9890c429c6 | |||
| 0219993c8a | |||
| f6d48c90f9 | |||
| ab99bde9d6 | |||
| 2cf181df0c | |||
| d11511c184 | |||
| 99bd030996 | |||
| 5017f8c9f2 | |||
| e8b64aa6c0 | |||
| 470cd7a9d2 | |||
| 0783b1d4e4 | |||
| 747cc8984f | |||
| e0db25664f | |||
| 6aff78728c | |||
| c2fd3c07ac | |||
| 2535751cd9 | |||
| 7e01491e2f | |||
| 14160e1988 | |||
| 9bfe19f795 | |||
| 7d0c002f89 | |||
| 5320aae5d9 | |||
| fe8dc5eaa2 | |||
| eb2bea79ba | |||
| 75ecb64159 | |||
| d69431a08e | |||
| 7e4f37b5f5 | |||
| 1df79a403b | |||
| 6b1ea78ff7 | |||
| 3022793418 | |||
| 21671daebe | |||
| 372a3b28bd | |||
| a03bd884aa | |||
| 4bc3f00424 | |||
| 8f65a81420 | |||
| 6cc1be7746 | |||
| a046f70075 | |||
| 75d68115ef | |||
|
babb22bf88
|
|||
|
468127996c
|
|||
| b6b6b20fe9 | |||
| f75b512837 | |||
| 7412aca81c | |||
| 02cf349075 | |||
| 67b988e6d2 | |||
| d2c13c2a4c | |||
| 498a38cf74 | |||
| 13a1c93ea6 | |||
| 38a0a7ed27 | |||
| 70c3eabdb1 | |||
| 4cd3b68697 | |||
| 1140fc65ba | |||
| 5753548b44 | |||
| 806b386cdb | |||
| d266059abe | |||
| b1dd26fbc4 | |||
| c8c27af7c3 | |||
| ba01492790 | |||
| 94368d5021 | |||
| ec793a9fdb | |||
| 37f622f226 | |||
| 87f74eaeda | |||
| 8406979e71 | |||
| 6e80f57739 | |||
| 1532fb360b | |||
| 180af8c844 | |||
| 311e2bc18d | |||
| bfd15e1a81 | |||
| d2d333a958 | |||
| 60a22ff330 | |||
| 946e5c47d4 | |||
| 5d450ab3c7 | |||
| a509a878e9 | |||
| 49e2988e37 | |||
| db8d16fd6e | |||
| d47848cb93 | |||
| 873fca763d | |||
| e99407132f | |||
| 67d63faeca | |||
| 871b053561 | |||
| 81deb1fff8 | |||
| 59560ebad1 | |||
| 67a6600c05 | |||
| f3e30558e4 | |||
| e46ab6ad7d | |||
| b6fd48f282 | |||
| c2edc26d8e | |||
| 76417b13d4 | |||
| 91a61643bd | |||
| b6e05cb0b9 | |||
| 1507b91463 | |||
| b06af3718c | |||
| a20a896582 | |||
| e1a3421212 | |||
| 19e51a2b12 | |||
| b89a5c5ce9 | |||
| 65d3277319 | |||
| a22bfa10f9 | |||
| d9d1319a3a | |||
| 15ecbf4345 | |||
| 5e3bbcd427 | |||
| a6c79db07b | |||
| 6e33bc6c17 | |||
| 01208bb359 | |||
| fa88aaae52 | |||
| 2da400a267 | |||
| 8103135dfb | |||
| cfabff7288 | |||
| 2f5a27a708 | |||
| fdfe8bcc4b | |||
| a19fd8db74 | |||
| e63d71423d | |||
| a7afe35fab | |||
| 56d6339313 | |||
| 2475572573 |
@@ -6,18 +6,18 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
distribution: 'zulu'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
|
||||
@@ -34,3 +34,9 @@ and you can configure it from there.
|
||||
|
||||
Alternatively, you can get the proxy JAR from the [downloads](https://papermc.io/downloads/velocity)
|
||||
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.
|
||||
|
||||
@@ -59,18 +59,18 @@ tasks {
|
||||
|
||||
val o = options as StandardJavadocDocletOptions
|
||||
o.encoding = "UTF-8"
|
||||
o.source = "17"
|
||||
o.source = "21"
|
||||
|
||||
o.use()
|
||||
o.links(
|
||||
"https://www.slf4j.org/apidocs/",
|
||||
"https://www.javadocs.dev/org.slf4j/slf4j-api/${libs.slf4j.get().version}/",
|
||||
"https://guava.dev/releases/${libs.guava.get().version}/api/docs/",
|
||||
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jd.advntr.dev/api/${libs.adventure.bom.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}/",
|
||||
"https://www.javadocs.dev/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
|
||||
)
|
||||
|
||||
o.tags(
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.velocitypowered.api.plugin.Plugin;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import javax.annotation.processing.AbstractProcessor;
|
||||
import javax.annotation.processing.ProcessingEnvironment;
|
||||
@@ -68,8 +67,8 @@ public class PluginAnnotationProcessor extends AbstractProcessor {
|
||||
|
||||
Name qualifiedName = ((TypeElement) element).getQualifiedName();
|
||||
|
||||
if (Objects.equals(pluginClassFound, qualifiedName.toString())) {
|
||||
if (!warnedAboutMultiplePlugins) {
|
||||
if (pluginClassFound != null) {
|
||||
if (!pluginClassFound.equals(qualifiedName.toString()) && !warnedAboutMultiplePlugins) {
|
||||
environment.getMessager()
|
||||
.printMessage(Diagnostic.Kind.WARNING, "Velocity does not yet currently support "
|
||||
+ "multiple plugins. We are using " + pluginClassFound
|
||||
|
||||
@@ -24,7 +24,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
*/
|
||||
public final class SerializedPluginDescription {
|
||||
|
||||
public static final Pattern ID_PATTERN = Pattern.compile("[a-z][a-z0-9-_]{0,63}");
|
||||
public static final String ID_PATTERN_STRING = "[a-z][a-z0-9-_]{0,63}";
|
||||
public static final Pattern ID_PATTERN = Pattern.compile(ID_PATTERN_STRING);
|
||||
|
||||
// @Nullable is used here to make GSON skip these in the serialized file
|
||||
private final String id;
|
||||
|
||||
@@ -23,7 +23,7 @@ public interface CommandSource extends Audience, PermissionSubject {
|
||||
* Sends a message with the MiniMessage format to this source.
|
||||
*
|
||||
* @param message MiniMessage content
|
||||
* @see <a href="https://docs.advntr.dev/minimessage/format.html">MiniMessage docs</a>
|
||||
* @see <a href="https://docs.papermc.io/adventure/minimessage/format/">MiniMessage docs</a>
|
||||
* for more information on the format.
|
||||
**/
|
||||
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.
|
||||
*
|
||||
* @param message MiniMessage content
|
||||
* @param resolvers resolvers to use
|
||||
* @see <a href="https://docs.advntr.dev/minimessage/">MiniMessage docs</a>
|
||||
* and <a href="https://docs.advntr.dev/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
|
||||
* for more information on the format.
|
||||
**/
|
||||
* Sends a message with the MiniMessage format to this source.
|
||||
*
|
||||
* @param message MiniMessage content
|
||||
* @param resolvers resolvers to use
|
||||
* @see <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a>
|
||||
* and <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements">MiniMessage Placeholders docs</a>
|
||||
* for more information on the format.
|
||||
*/
|
||||
default void sendRichMessage(
|
||||
final @NotNull String message,
|
||||
final @NotNull TagResolver @NotNull... resolvers
|
||||
|
||||
@@ -60,7 +60,8 @@ public interface EventManager {
|
||||
*
|
||||
* @param plugin the plugin to associate with the handler
|
||||
* @param eventClass the class for the event handler to register
|
||||
* @param postOrder the relative order in which events should be posted to the handler
|
||||
* @param postOrder the relative order in which events should be posted to the handler. The higher
|
||||
* the priority, the earlier the event handler will be called
|
||||
* @param handler the handler to register
|
||||
* @param <E> the event type to handle
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.event.ResultedEvent;
|
||||
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||
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.
|
||||
@@ -22,10 +23,24 @@ import com.velocitypowered.api.proxy.Player;
|
||||
public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentResult> {
|
||||
|
||||
private final Player player;
|
||||
private final String serverIdHash;
|
||||
private ComponentResult result;
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
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.serverIdHash = serverIdHash;
|
||||
this.result = ComponentResult.allowed();
|
||||
}
|
||||
|
||||
@@ -33,6 +48,16 @@ public final class LoginEvent implements ResultedEvent<ResultedEvent.ComponentRe
|
||||
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
|
||||
public ComponentResult getResult() {
|
||||
return result;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||
* reference the LICENSE file in the api top-level directory.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.api.event.player;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is fired when a client ({@link Player}) sends a plugin message through the
|
||||
* unregister channel. Velocity will not wait on this event to finish firing.
|
||||
*/
|
||||
public final class PlayerChannelUnregisterEvent {
|
||||
|
||||
private final Player player;
|
||||
private final List<ChannelIdentifier> channels;
|
||||
|
||||
public PlayerChannelUnregisterEvent(Player player, List<ChannelIdentifier> channels) {
|
||||
this.player = Preconditions.checkNotNull(player, "player");
|
||||
this.channels = Preconditions.checkNotNull(channels, "channels");
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
public List<ChannelIdentifier> getChannels() {
|
||||
return channels;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PlayerChannelUnregisterEvent{"
|
||||
+ "player=" + player
|
||||
+ ", channels=" + channels
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public final class ServerPreConnectEvent implements
|
||||
* is used, then {@link ConnectionRequestBuilder#connect()}'s result will have the status
|
||||
* {@link Status#CONNECTION_CANCELLED}.
|
||||
*
|
||||
* @return a result to deny conneections
|
||||
* @return a result to deny connections
|
||||
*/
|
||||
public static ServerResult denied() {
|
||||
return DENIED;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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";
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,11 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
|
||||
MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"),
|
||||
MINECRAFT_1_21_4(769, "1.21.4"),
|
||||
MINECRAFT_1_21_5(770, "1.21.5"),
|
||||
MINECRAFT_1_21_6(771, "1.21.6");
|
||||
MINECRAFT_1_21_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");
|
||||
|
||||
private static final int SNAPSHOT_BIT = 30;
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.intellij.lang.annotations.Pattern;
|
||||
|
||||
/**
|
||||
* Indicates that the {@link Plugin} depends on another plugin in order to enable.
|
||||
@@ -24,6 +26,7 @@ public @interface Dependency {
|
||||
* @return The dependency plugin ID
|
||||
* @see Plugin#id()
|
||||
*/
|
||||
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
|
||||
String id();
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
|
||||
package com.velocitypowered.api.plugin;
|
||||
|
||||
import com.velocitypowered.api.plugin.ap.SerializedPluginDescription;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.intellij.lang.annotations.Pattern;
|
||||
|
||||
/**
|
||||
* Annotation used to describe a Velocity plugin.
|
||||
@@ -26,6 +28,7 @@ public @interface Plugin {
|
||||
*
|
||||
* @return the ID for this plugin
|
||||
*/
|
||||
@Pattern(SerializedPluginDescription.ID_PATTERN_STRING)
|
||||
String id();
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.UnaryOperator;
|
||||
import net.kyori.adventure.dialog.DialogLike;
|
||||
import net.kyori.adventure.identity.Identified;
|
||||
import net.kyori.adventure.inventory.Book;
|
||||
import net.kyori.adventure.key.Key;
|
||||
@@ -38,6 +39,7 @@ import net.kyori.adventure.sound.SoundStop;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.event.HoverEventSource;
|
||||
import net.kyori.adventure.text.object.PlayerHeadObjectContents;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -48,7 +50,8 @@ public interface Player extends
|
||||
/* Fundamental Velocity interfaces */
|
||||
CommandSource, InboundConnection, ChannelMessageSource, ChannelMessageSink,
|
||||
/* Adventure-specific interfaces */
|
||||
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable {
|
||||
Identified, HoverEventSource<HoverEvent.ShowEntity>, Keyed, KeyIdentifiable, Sound.Emitter,
|
||||
PlayerHeadObjectContents.SkinSource {
|
||||
|
||||
/**
|
||||
* Returns the player's current username.
|
||||
@@ -335,6 +338,15 @@ public interface Player extends
|
||||
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.
|
||||
@@ -383,8 +395,12 @@ public interface Player extends
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <b>This method is not currently implemented in Velocity
|
||||
* and will not perform any actions.</b>
|
||||
*
|
||||
* @apiNote <b>This method is not currently implemented in Velocity
|
||||
* 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
|
||||
default void playSound(@NotNull Sound sound) {
|
||||
@@ -393,8 +409,11 @@ public interface Player extends
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <b>This method is not currently implemented in Velocity
|
||||
* and will not perform any actions.</b>
|
||||
* @apiNote <b>This method is not currently implemented in Velocity
|
||||
* 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
|
||||
default void playSound(@NotNull Sound sound, double x, double y, double z) {
|
||||
@@ -403,18 +422,28 @@ public interface Player extends
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <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-146721">MC-146721</a>, stereo sounds are always played globally in 1.14+.
|
||||
*
|
||||
* <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
|
||||
default void playSound(@NotNull Sound sound, Sound.Emitter emitter) {
|
||||
default void playSound(@NotNull Sound sound, @NotNull Sound.Emitter emitter) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <b>This method is not currently implemented in Velocity
|
||||
* and will not perform any actions.</b>
|
||||
* @param stop the sound and/or a sound source, to stop
|
||||
* @since 3.4.0
|
||||
* @sinceMinecraft 1.19.3
|
||||
* @apiNote This method is currently only implemented for players on 1.19.3+.
|
||||
*/
|
||||
@Override
|
||||
default void stopSound(@NotNull SoundStop stop) {
|
||||
@@ -425,11 +454,40 @@ public interface Player extends
|
||||
*
|
||||
* <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 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.
|
||||
*
|
||||
|
||||
@@ -19,7 +19,9 @@ import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a 1.7 and above server list ping response. This class is immutable.
|
||||
@@ -28,9 +30,10 @@ public final class ServerPing {
|
||||
|
||||
private final Version version;
|
||||
private final @Nullable Players players;
|
||||
private final net.kyori.adventure.text.Component description;
|
||||
private final @Nullable Component description;
|
||||
private final @Nullable Favicon favicon;
|
||||
private final @Nullable ModInfo modinfo;
|
||||
private final boolean preventsChatReports = true;
|
||||
|
||||
public ServerPing(Version version, @Nullable Players players,
|
||||
net.kyori.adventure.text.Component description, @Nullable Favicon favicon) {
|
||||
@@ -47,8 +50,8 @@ public final class ServerPing {
|
||||
* @param modinfo the mods this server runs
|
||||
*/
|
||||
public ServerPing(Version version, @Nullable Players players,
|
||||
net.kyori.adventure.text.Component description, @Nullable Favicon favicon,
|
||||
@Nullable ModInfo modinfo) {
|
||||
Component description, @Nullable Favicon favicon,
|
||||
@Nullable ModInfo modinfo) {
|
||||
this.version = Preconditions.checkNotNull(version, "version");
|
||||
this.players = players;
|
||||
this.description = Preconditions.checkNotNull(description, "description");
|
||||
@@ -64,7 +67,8 @@ public final class ServerPing {
|
||||
return Optional.ofNullable(players);
|
||||
}
|
||||
|
||||
public net.kyori.adventure.text.Component getDescriptionComponent() {
|
||||
@Nullable
|
||||
public Component getDescriptionComponent() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -151,7 +155,7 @@ public final class ServerPing {
|
||||
private final List<SamplePlayer> samplePlayers = new ArrayList<>();
|
||||
private String modType = "FML";
|
||||
private final List<ModInfo.Mod> mods = new ArrayList<>();
|
||||
private net.kyori.adventure.text.Component description;
|
||||
private Component description;
|
||||
private @Nullable Favicon favicon;
|
||||
private boolean nullOutPlayers;
|
||||
private boolean nullOutModinfo;
|
||||
@@ -299,7 +303,7 @@ public final class ServerPing {
|
||||
* @param description Component to use as the description.
|
||||
* @return this builder, for chaining
|
||||
*/
|
||||
public Builder description(net.kyori.adventure.text.Component description) {
|
||||
public Builder description(Component description) {
|
||||
this.description = Preconditions.checkNotNull(description, "description");
|
||||
return this;
|
||||
}
|
||||
@@ -359,7 +363,7 @@ public final class ServerPing {
|
||||
return samplePlayers;
|
||||
}
|
||||
|
||||
public Optional<net.kyori.adventure.text.Component> getDescriptionComponent() {
|
||||
public Optional<Component> getDescriptionComponent() {
|
||||
return Optional.ofNullable(description);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,11 +11,14 @@ import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.List;
|
||||
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.
|
||||
*/
|
||||
public final class GameProfile {
|
||||
public final class GameProfile implements PlayerHeadObjectContents.SkinSource {
|
||||
|
||||
private final UUID id;
|
||||
private final String undashedId;
|
||||
@@ -169,6 +172,23 @@ public final class GameProfile {
|
||||
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
|
||||
public String toString() {
|
||||
return "GameProfile{"
|
||||
|
||||
@@ -2,8 +2,15 @@ import org.gradle.jvm.tasks.Jar
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
// This interface is needed as a workaround to get an instance of ExecOperations
|
||||
interface Injected {
|
||||
@get:Inject
|
||||
val execOps: ExecOperations
|
||||
}
|
||||
|
||||
val currentShortRevision = ByteArrayOutputStream().use {
|
||||
exec {
|
||||
val execOps = objects.newInstance<Injected>().execOps
|
||||
execOps.exec {
|
||||
executable = "git"
|
||||
args = listOf("rev-parse", "HEAD")
|
||||
standardOutput = it
|
||||
|
||||
@@ -8,10 +8,10 @@ extensions.configure<PublishingExtension> {
|
||||
maven {
|
||||
credentials(PasswordCredentials::class.java)
|
||||
|
||||
name = "paper"
|
||||
val base = "https://repo.papermc.io/repository/maven"
|
||||
val releasesRepoUrl = "$base-releases/"
|
||||
val snapshotsRepoUrl = "$base-snapshots/"
|
||||
name = if (version.toString().endsWith("SNAPSHOT")) "paperSnapshots" else "paper" // "paper" is seemingly not defined
|
||||
val base = "https://artifactory.papermc.io/artifactory"
|
||||
val releasesRepoUrl = "$base/releases/"
|
||||
val snapshotsRepoUrl = "$base/snapshots/"
|
||||
setUrl(if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl else releasesRepoUrl)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -12,7 +12,7 @@ subprojects {
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
languageVersion.set(JavaLanguageVersion.of(21))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ subprojects {
|
||||
testImplementation(rootProject.libs.junit)
|
||||
}
|
||||
|
||||
tasks {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
reports {
|
||||
junitXml.required.set(true)
|
||||
testing.suites.named<JvmTestSuite>("test") {
|
||||
useJUnitJupiter()
|
||||
targets.all {
|
||||
testTask.configure {
|
||||
reports.junitXml.required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
group=com.velocitypowered
|
||||
version=3.4.0-SNAPSHOT
|
||||
version=3.5.0-SNAPSHOT
|
||||
|
||||
+25
-25
@@ -1,26 +1,26 @@
|
||||
[versions]
|
||||
configurate3 = "3.7.3"
|
||||
configurate4 = "4.1.2"
|
||||
configurate4 = "4.2.0"
|
||||
flare = "2.0.1"
|
||||
log4j = "2.24.3"
|
||||
netty = "4.2.1.Final"
|
||||
log4j = "2.25.3"
|
||||
netty = "4.2.10.Final"
|
||||
|
||||
[plugins]
|
||||
indra-publishing = "net.kyori.indra.publishing:2.0.6"
|
||||
shadow = "io.github.goooler.shadow:8.1.5"
|
||||
spotless = "com.diffplug.spotless:6.25.0"
|
||||
fill = "io.papermc.fill.gradle:1.0.10"
|
||||
shadow = "com.gradleup.shadow:9.3.1"
|
||||
spotless = "com.diffplug.spotless:8.2.0"
|
||||
|
||||
[libraries]
|
||||
adventure-bom = "net.kyori:adventure-bom:4.21.0"
|
||||
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.21.0"
|
||||
adventure-facet = "net.kyori:adventure-platform-facet:4.3.4"
|
||||
asm = "org.ow2.asm:asm:9.8"
|
||||
auto-service = "com.google.auto.service:auto-service:1.0.1"
|
||||
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1"
|
||||
adventure-bom = "net.kyori:adventure-bom:4.26.1"
|
||||
adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.26.1"
|
||||
adventure-facet = "net.kyori:adventure-platform-facet:4.4.1"
|
||||
asm = "org.ow2.asm:asm:9.9.1"
|
||||
auto-service = "com.google.auto.service:auto-service:1.1.1"
|
||||
auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
|
||||
brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT"
|
||||
bstats = "org.bstats:bstats-base:3.0.3"
|
||||
caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8"
|
||||
checker-qual = "org.checkerframework:checker-qual:3.42.0"
|
||||
bstats = "org.bstats:bstats-base:3.1.0"
|
||||
caffeine = "com.github.ben-manes.caffeine:caffeine:3.2.3"
|
||||
checker-qual = "org.checkerframework:checker-qual:3.53.0"
|
||||
checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3"
|
||||
completablefutures = "com.spotify:completable-futures:0.3.6"
|
||||
configurate3-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate3" }
|
||||
@@ -33,21 +33,21 @@ disruptor = "com.lmax:disruptor:4.0.0"
|
||||
fastutil = "it.unimi.dsi:fastutil:8.5.15"
|
||||
flare-core = { module = "space.vectrix.flare:flare", version.ref = "flare" }
|
||||
flare-fastutil = { module = "space.vectrix.flare:flare-fastutil", version.ref = "flare" }
|
||||
jline = "org.jline:jline-terminal-jansi:3.30.2"
|
||||
jline = "org.jline:jline-terminal-jansi:3.30.6"
|
||||
jopt = "net.sf.jopt-simple:jopt-simple:5.0.4"
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.10.2"
|
||||
jspecify = "org.jspecify:jspecify:0.3.0"
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.14.2"
|
||||
jspecify = "org.jspecify:jspecify:1.0.0"
|
||||
kyori-ansi = "net.kyori:ansi:1.1.1"
|
||||
guava = "com.google.guava:guava:25.1-jre"
|
||||
gson = "com.google.code.gson:gson:2.10.1"
|
||||
guice = "com.google.inject:guice:6.0.0"
|
||||
guava = "com.google.guava:guava:33.5.0-jre"
|
||||
gson = "com.google.code.gson:gson:2.13.2"
|
||||
guice = "com.google.inject:guice:7.0.0"
|
||||
lmbda = "org.lanternpowered:lmbda:2.0.0"
|
||||
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-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-jul = { module = "org.apache.logging.log4j:log4j-jul", version.ref = "log4j" }
|
||||
mockito = "org.mockito:mockito-core:5.10.0"
|
||||
mockito = "org.mockito:mockito-core:5.21.0"
|
||||
netty-codec = { module = "io.netty:netty-codec", version.ref = "netty" }
|
||||
netty-codec-haproxy = { module = "io.netty:netty-codec-haproxy", version.ref = "netty" }
|
||||
netty-codec-http = { module = "io.netty:netty-codec-http", version.ref = "netty" }
|
||||
@@ -55,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-kqueue = { module = "io.netty:netty-transport-native-kqueue", version.ref = "netty" }
|
||||
netty-transport-native-iouring = { module = "io.netty:netty-transport-native-io_uring", version.ref = "netty" }
|
||||
nightconfig = "com.electronwill.night-config:toml:3.6.7"
|
||||
nightconfig = "com.electronwill.night-config:toml:3.8.3"
|
||||
slf4j = "org.slf4j:slf4j-api:2.0.17"
|
||||
snakeyaml = "org.yaml:snakeyaml:1.33"
|
||||
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.7.3"
|
||||
snakeyaml = "org.yaml:snakeyaml:2.5"
|
||||
spotbugs-annotations = "com.github.spotbugs:spotbugs-annotations:4.9.8"
|
||||
terminalconsoleappender = "net.minecrell:terminalconsoleappender:1.3.0"
|
||||
|
||||
[bundles]
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# 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
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -115,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -173,7 +171,6 @@ fi
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
@@ -203,18 +200,17 @@ fi
|
||||
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * 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.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
@@ -249,4 +245,4 @@ eval "set -- $(
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
Vendored
+3
-4
@@ -36,7 +36,7 @@ set APP_HOME=%DIRNAME%
|
||||
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.
|
||||
set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
@@ -70,11 +70,10 @@ goto fail
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
@@ -91,4 +90,4 @@ exit /b %EXIT_CODE%
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
:omega
|
||||
|
||||
+26
-2
@@ -1,14 +1,16 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.transformers.Log4j2PluginsCacheFileTransformer
|
||||
import io.papermc.fill.model.BuildChannel
|
||||
|
||||
plugins {
|
||||
application
|
||||
id("velocity-init-manifest")
|
||||
alias(libs.plugins.shadow)
|
||||
alias(libs.plugins.fill)
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("com.velocitypowered.proxy.Velocity")
|
||||
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true");
|
||||
applicationDefaultJvmArgs += listOf("-Dvelocity.packet-decode-logging=true")
|
||||
}
|
||||
|
||||
tasks {
|
||||
@@ -25,6 +27,10 @@ tasks {
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
filesMatching("META-INF/org/apache/logging/log4j/core/config/plugins/**") {
|
||||
duplicatesStrategy = DuplicatesStrategy.INCLUDE
|
||||
}
|
||||
|
||||
transform(Log4j2PluginsCacheFileTransformer::class.java)
|
||||
|
||||
// Exclude all the collection types we don"t intend to use
|
||||
@@ -108,10 +114,27 @@ tasks {
|
||||
}
|
||||
}
|
||||
|
||||
val projectVersion = version as String
|
||||
fill {
|
||||
project("velocity")
|
||||
|
||||
build {
|
||||
channel = BuildChannel.STABLE
|
||||
versionFamily("3.0.0")
|
||||
version(projectVersion)
|
||||
|
||||
downloads {
|
||||
register("server:default") {
|
||||
file = tasks.shadowJar.flatMap { it.archiveFile }
|
||||
nameResolver.set { project, _, version, build -> "$project-$version-$build.jar" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":velocity-api"))
|
||||
implementation(project(":velocity-native"))
|
||||
implementation(project(":velocity-proxy-log4j2-plugin"))
|
||||
|
||||
implementation(libs.bundles.log4j)
|
||||
implementation(libs.kyori.ansi)
|
||||
@@ -148,4 +171,5 @@ dependencies {
|
||||
testImplementation(libs.mockito)
|
||||
|
||||
annotationProcessor(libs.auto.service)
|
||||
annotationProcessor(libs.log4j.core)
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
dependencies {
|
||||
implementation(libs.bundles.log4j)
|
||||
annotationProcessor(libs.log4j.core)
|
||||
}
|
||||
@@ -47,11 +47,6 @@ public class Velocity {
|
||||
System.setProperty("io.netty.native.workdir", System.getProperty("velocity.natives-tmpdir"));
|
||||
}
|
||||
|
||||
// Restore allocator used before Netty 4.2 due to oom issues with the adaptive allocator
|
||||
if (System.getProperty("io.netty.allocator.type") == null) {
|
||||
System.setProperty("io.netty.allocator.type", "pooled");
|
||||
}
|
||||
|
||||
// Disable the resource leak detector by default as it reduces performance. Allow the user to
|
||||
// override this if desired.
|
||||
if (!VelocityProperties.hasProperty("io.netty.leakDetection.level")) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.velocitypowered.api.command.BrigadierCommand;
|
||||
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.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
@@ -81,7 +82,6 @@ import java.net.http.HttpClient;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
@@ -104,8 +104,8 @@ import net.kyori.adventure.audience.Audience;
|
||||
import net.kyori.adventure.audience.ForwardingAudience;
|
||||
import net.kyori.adventure.key.Key;
|
||||
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.TranslationStore;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bstats.MetricsBase;
|
||||
@@ -119,7 +119,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
*/
|
||||
public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
|
||||
public static final String VELOCITY_URL = "https://velocitypowered.com";
|
||||
public static final String VELOCITY_URL = "https://papermc.io/software/velocity";
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(VelocityServer.class);
|
||||
public static final Gson GENERAL_GSON = new GsonBuilder()
|
||||
@@ -150,6 +150,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
)
|
||||
.registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE)
|
||||
.create();
|
||||
private static final int PRE_SHUTDOWN_TIMEOUT =
|
||||
Integer.getInteger("velocity.pre-shutdown-timeout", 10);
|
||||
|
||||
private final ConnectionManager cm;
|
||||
private final ProxyOptions options;
|
||||
@@ -216,7 +218,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
ProxyVersion version = getVersion();
|
||||
PluginDescription description = new VelocityPluginDescription(
|
||||
"velocity", version.getName(), version.getVersion(), "The Velocity proxy",
|
||||
VELOCITY_URL, ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
|
||||
version.getName().equals("Velocity") ? VELOCITY_URL : null,
|
||||
ImmutableList.of(version.getVendor()), Collections.emptyList(), null);
|
||||
VelocityPluginContainer container = new VelocityPluginContainer(description);
|
||||
container.setInstance(VelocityVirtualPlugin.INSTANCE);
|
||||
return container;
|
||||
@@ -238,8 +241,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
console.setupStreams();
|
||||
pluginManager.registerPlugin(this.createVirtualPlugin());
|
||||
|
||||
registerTranslations();
|
||||
|
||||
// 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...
|
||||
//
|
||||
@@ -288,6 +289,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
|
||||
this.doStartupConfigLoad();
|
||||
|
||||
registerTranslations();
|
||||
|
||||
for (ServerInfo cliServer : options.getServers()) {
|
||||
servers.register(cliServer);
|
||||
}
|
||||
@@ -338,8 +341,8 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
}
|
||||
|
||||
private void registerTranslations() {
|
||||
final TranslationStore.StringBased<MessageFormat> translationRegistry =
|
||||
TranslationStore.messageFormat(Key.key("velocity", "translations"));
|
||||
final MiniMessageTranslationStore translationRegistry =
|
||||
MiniMessageTranslationStore.create(Key.key("velocity", "translations"));
|
||||
translationRegistry.defaultLocale(Locale.US);
|
||||
try {
|
||||
ResourceUtils.visitResources(VelocityServer.class, path -> {
|
||||
@@ -578,6 +581,20 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
// done first to refuse new connections
|
||||
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());
|
||||
for (ConnectedPlayer player : players) {
|
||||
player.disconnect(reason);
|
||||
@@ -817,7 +834,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience {
|
||||
public VelocityChannelRegistrar getChannelRegistrar() {
|
||||
return channelRegistrar;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isShuttingDown() {
|
||||
return shutdownInProgress.get();
|
||||
|
||||
+15
-7
@@ -53,15 +53,23 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
||||
viewer.getProtocolVersion(),
|
||||
viewer.translateMessage(this.bar.name())
|
||||
);
|
||||
viewer.getConnection().write(BossBarPacket.createAddPacket(this.id, this.bar, name));
|
||||
viewer.getBossBarManager().writeUpdate(this, BossBarPacket.createAddPacket(this.id, this.bar, name));
|
||||
return true;
|
||||
}
|
||||
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) {
|
||||
if (this.viewers.remove(viewer)) {
|
||||
viewer.getConnection().write(BossBarPacket.createRemovePacket(this.id, this.bar));
|
||||
viewer.getBossBarManager().remove(this, BossBarPacket.createRemovePacket(this.id, this.bar));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -84,7 +92,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
||||
this.bar,
|
||||
new ComponentHolder(viewer.getProtocolVersion(), translated)
|
||||
);
|
||||
viewer.getConnection().write(packet);
|
||||
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +104,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
||||
) {
|
||||
final BossBarPacket packet = BossBarPacket.createUpdateProgressPacket(this.id, this.bar);
|
||||
for (final ConnectedPlayer viewer : this.viewers) {
|
||||
viewer.getConnection().write(packet);
|
||||
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +116,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
||||
) {
|
||||
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
|
||||
for (final ConnectedPlayer viewer : this.viewers) {
|
||||
viewer.getConnection().write(packet);
|
||||
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +128,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
||||
) {
|
||||
final BossBarPacket packet = BossBarPacket.createUpdateStylePacket(this.id, this.bar);
|
||||
for (final ConnectedPlayer viewer : this.viewers) {
|
||||
viewer.getConnection().write(packet);
|
||||
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +140,7 @@ public final class VelocityBossBarImplementation implements BossBar.Listener,
|
||||
) {
|
||||
final BossBarPacket packet = BossBarPacket.createUpdatePropertiesPacket(this.id, this.bar);
|
||||
for (final ConnectedPlayer viewer : this.viewers) {
|
||||
viewer.getConnection().write(packet);
|
||||
viewer.getBossBarManager().writeUpdate(this, packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +344,7 @@ final class SuggestionsProvider<S> {
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
return potentials.get(0);
|
||||
return potentials.getFirst();
|
||||
}
|
||||
return new ParseResults<>(contextSoFar, originalReader, Collections.emptyMap());
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.command.CommandMeta;
|
||||
import com.velocitypowered.api.command.CommandResult;
|
||||
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.PostCommandInvocationEvent;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
@@ -59,6 +58,7 @@ import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.stream.Collectors;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.checkerframework.checker.lock.qual.GuardedBy;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
@@ -140,7 +140,7 @@ public class VelocityCommandManager implements CommandManager {
|
||||
command + " implements multiple registrable Command subinterfaces: "
|
||||
+ implementedInterfaces);
|
||||
} else {
|
||||
this.internalRegister(commandRegistrars.get(0), command, meta);
|
||||
this.internalRegister(commandRegistrars.getFirst(), command, meta);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,8 +242,8 @@ public class VelocityCommandManager implements CommandManager {
|
||||
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
|
||||
if (isSyntaxError) {
|
||||
final Message message = e.getRawMessage();
|
||||
if (message instanceof VelocityBrigadierMessage velocityMessage) {
|
||||
source.sendMessage(velocityMessage.asComponent().applyFallbackStyle(NamedTextColor.RED));
|
||||
if (message instanceof ComponentLike componentLike) {
|
||||
source.sendMessage(componentLike.asComponent().applyFallbackStyle(NamedTextColor.RED));
|
||||
} else {
|
||||
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED));
|
||||
}
|
||||
@@ -256,7 +256,7 @@ public class VelocityCommandManager implements CommandManager {
|
||||
}
|
||||
} catch (final Throwable e) {
|
||||
// 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 {
|
||||
eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result));
|
||||
}
|
||||
@@ -400,4 +400,4 @@ public class VelocityCommandManager implements CommandManager {
|
||||
return MoreExecutors.directExecutor();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,33 +70,34 @@ public final class VelocityCommands {
|
||||
maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant);
|
||||
}
|
||||
|
||||
if (delegate instanceof LiteralCommandNode<CommandSource> lcn) {
|
||||
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
|
||||
literalBuilder.executes(maybeCommand);
|
||||
// we also need to wrap any children
|
||||
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
|
||||
literalBuilder.then(wrap(child, registrant));
|
||||
return switch (delegate) {
|
||||
case LiteralCommandNode<CommandSource> lcn -> {
|
||||
var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true);
|
||||
literalBuilder.executes(maybeCommand);
|
||||
// we also need to wrap any children
|
||||
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
|
||||
literalBuilder.then(wrap(child, registrant));
|
||||
}
|
||||
if (delegate.getRedirect() != null) {
|
||||
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
||||
}
|
||||
yield literalBuilder.build();
|
||||
}
|
||||
if (delegate.getRedirect() != null) {
|
||||
literalBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
||||
case VelocityArgumentCommandNode<CommandSource, ?> vacn -> vacn.withCommand(maybeCommand)
|
||||
.withRedirect(delegate.getRedirect() != null ? wrap(delegate.getRedirect(), registrant) : null);
|
||||
case ArgumentCommandNode<CommandSource, ?> node -> {
|
||||
var argBuilder = node.createBuilder().executes(maybeCommand);
|
||||
// we also need to wrap any children
|
||||
for (final CommandNode<CommandSource> child : delegate.getChildren()) {
|
||||
argBuilder.then(wrap(child, registrant));
|
||||
}
|
||||
if (delegate.getRedirect() != null) {
|
||||
argBuilder.redirect(wrap(delegate.getRedirect(), registrant));
|
||||
}
|
||||
yield argBuilder.build();
|
||||
}
|
||||
return literalBuilder.build();
|
||||
} 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());
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass());
|
||||
};
|
||||
}
|
||||
|
||||
// Normalization
|
||||
@@ -133,7 +134,7 @@ public final class VelocityCommands {
|
||||
if (nodes.isEmpty()) {
|
||||
throw new IllegalArgumentException("Cannot read alias from empty node list");
|
||||
}
|
||||
return nodes.get(0).getNode().getName();
|
||||
return nodes.getFirst().getNode().getName();
|
||||
}
|
||||
|
||||
public static final String ARGS_NODE_NAME = "arguments";
|
||||
|
||||
+2
-4
@@ -118,14 +118,12 @@ public class VelocityArgumentCommandNode<S, T> extends ArgumentCommandNode<S, St
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof VelocityArgumentCommandNode)) {
|
||||
if (!(o instanceof VelocityArgumentCommandNode that)) {
|
||||
return false;
|
||||
}
|
||||
if (!super.equals(o)) {
|
||||
if (!super.equals(that)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final VelocityArgumentCommandNode<?, ?> that = (VelocityArgumentCommandNode<?, ?>) o;
|
||||
return this.type.equals(that.type);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||
|
||||
/**
|
||||
* Implements the Velocity default {@code /glist} command.
|
||||
@@ -111,7 +112,7 @@ public class GlistCommand {
|
||||
if (registeredServer.isEmpty()) {
|
||||
source.sendMessage(
|
||||
CommandMessages.SERVER_DOES_NOT_EXIST
|
||||
.arguments(Component.text(serverName)));
|
||||
.arguments(Argument.string("server", serverName)));
|
||||
return -1;
|
||||
}
|
||||
sendServerPlayers(source, registeredServer.get(), false);
|
||||
@@ -126,7 +127,8 @@ public class GlistCommand {
|
||||
? "velocity.command.glist-player-singular"
|
||||
: "velocity.command.glist-player-plural"
|
||||
).color(NamedTextColor.YELLOW)
|
||||
.arguments(Component.text(Integer.toString(online), NamedTextColor.GREEN));
|
||||
.arguments(Argument.component(
|
||||
"players", Component.text(Integer.toString(online), NamedTextColor.GREEN)));
|
||||
target.sendMessage(msg.build());
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||
|
||||
/**
|
||||
* Implements the Velocity default {@code /send} command.
|
||||
@@ -121,7 +122,7 @@ public class SendCommand {
|
||||
|
||||
if (maybeServer.isEmpty()) {
|
||||
context.getSource().sendMessage(
|
||||
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Component.text(serverName))
|
||||
CommandMessages.SERVER_DOES_NOT_EXIST.arguments(Argument.string("server", serverName))
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
@@ -133,7 +134,7 @@ public class SendCommand {
|
||||
&& !Objects.equals(player, "all")
|
||||
&& !Objects.equals(player, "current")) {
|
||||
context.getSource().sendMessage(
|
||||
CommandMessages.PLAYER_NOT_FOUND.arguments(Component.text(player))
|
||||
CommandMessages.PLAYER_NOT_FOUND.arguments(Argument.string("player", player))
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import net.kyori.adventure.text.TextComponent;
|
||||
import net.kyori.adventure.text.TranslatableComponent;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.minimessage.translation.Argument;
|
||||
|
||||
/**
|
||||
* Implements Velocity's {@code /server} command.
|
||||
@@ -76,7 +77,7 @@ public final class ServerCommand {
|
||||
final Optional<RegisteredServer> toConnect = server.getServer(serverName);
|
||||
if (toConnect.isEmpty()) {
|
||||
player.sendMessage(CommandMessages.SERVER_DOES_NOT_EXIST
|
||||
.arguments(Component.text(serverName)));
|
||||
.arguments(Argument.string("server", serverName)));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -135,7 +136,7 @@ public final class ServerCommand {
|
||||
} else {
|
||||
playersTextComponent.key("velocity.command.server-tooltip-players-online");
|
||||
}
|
||||
playersTextComponent.arguments(Component.text(connectedPlayers));
|
||||
playersTextComponent.arguments(Argument.component("players", Component.text(connectedPlayers)));
|
||||
if (serverInfo.getName().equals(currentPlayerServer)) {
|
||||
serverTextComponent.color(NamedTextColor.GREEN)
|
||||
.hoverEvent(
|
||||
|
||||
@@ -62,6 +62,7 @@ import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
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.Logger;
|
||||
|
||||
@@ -165,9 +166,9 @@ public final class VelocityCommand {
|
||||
.build();
|
||||
final Component copyright = Component
|
||||
.translatable("velocity.command.version-copyright",
|
||||
Component.text(version.getVendor()),
|
||||
Component.text(version.getName()),
|
||||
Component.text(LocalDate.now().getYear()));
|
||||
Argument.string("vendor", version.getVendor()),
|
||||
Argument.string("name", version.getName()),
|
||||
Argument.component("year", Component.text(LocalDate.now().getYear())));
|
||||
source.sendMessage(velocity);
|
||||
source.sendMessage(copyright);
|
||||
|
||||
@@ -176,8 +177,7 @@ public final class VelocityCommand {
|
||||
.append(Component.text()
|
||||
.content("PaperMC")
|
||||
.color(NamedTextColor.GREEN)
|
||||
.clickEvent(
|
||||
ClickEvent.openUrl("https://papermc.io/software/velocity"))
|
||||
.clickEvent(ClickEvent.openUrl(VelocityServer.VELOCITY_URL))
|
||||
.build())
|
||||
.append(Component.text(" - "))
|
||||
.append(Component.text()
|
||||
@@ -221,7 +221,7 @@ public final class VelocityCommand {
|
||||
final TranslatableComponent output = Component.translatable()
|
||||
.key("velocity.command.plugins-list")
|
||||
.color(NamedTextColor.YELLOW)
|
||||
.arguments(listBuilder.build())
|
||||
.arguments(Argument.component("plugins", listBuilder.build()))
|
||||
.build();
|
||||
source.sendMessage(output);
|
||||
return Command.SINGLE_SUCCESS;
|
||||
@@ -237,17 +237,17 @@ public final class VelocityCommand {
|
||||
hoverText.append(Component.newline());
|
||||
hoverText.append(Component.translatable(
|
||||
"velocity.command.plugin-tooltip-website",
|
||||
Component.text(url)));
|
||||
Argument.component("url", Component.text(url))));
|
||||
});
|
||||
if (!description.getAuthors().isEmpty()) {
|
||||
hoverText.append(Component.newline());
|
||||
if (description.getAuthors().size() == 1) {
|
||||
hoverText.append(Component.translatable("velocity.command.plugin-tooltip-author",
|
||||
Component.text(description.getAuthors().get(0))));
|
||||
Component.text(description.getAuthors().getFirst())));
|
||||
} else {
|
||||
hoverText.append(
|
||||
Component.translatable("velocity.command.plugin-tooltip-author",
|
||||
Component.text(String.join(", ", description.getAuthors()))
|
||||
Argument.string("authors", String.join(", ", description.getAuthors()))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
+6
-2
@@ -103,13 +103,17 @@ abstract class InvocableCommandRegistrar<T extends InvocableCommand<I>,
|
||||
.requiresWithContext((context, reader) -> requirement.test(context))
|
||||
.executes(callback)
|
||||
.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);
|
||||
return command.suggestAsync(invocation).thenApply(suggestions -> {
|
||||
for (String value : suggestions) {
|
||||
Preconditions.checkNotNull(value, "suggestion");
|
||||
builder.suggest(value);
|
||||
offsetBuilder.suggest(value);
|
||||
}
|
||||
return builder.build();
|
||||
return offsetBuilder.build();
|
||||
});
|
||||
})
|
||||
.build();
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.velocitypowered.api.util.Favicon;
|
||||
import com.velocitypowered.proxy.config.migration.ConfigurationMigration;
|
||||
import com.velocitypowered.proxy.config.migration.ForwardingMigration;
|
||||
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.TransferIntegrationMigration;
|
||||
import com.velocitypowered.proxy.util.AddressUtil;
|
||||
@@ -93,6 +94,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
private @Nullable Favicon favicon;
|
||||
@Expose
|
||||
private boolean forceKeyAuthentication = true; // Added in 1.19
|
||||
@Expose
|
||||
private PacketLimiterConfig packetLimiterConfig = PacketLimiterConfig.DEFAULT;
|
||||
|
||||
private VelocityConfiguration(Servers servers, ForcedHosts forcedHosts, Advanced advanced,
|
||||
Query query, Metrics metrics) {
|
||||
@@ -109,7 +112,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough,
|
||||
boolean samplePlayersInPing, boolean enablePlayerAddressLogging, Servers servers,
|
||||
ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics,
|
||||
boolean forceKeyAuthentication) {
|
||||
boolean forceKeyAuthentication, PacketLimiterConfig packetLimiterConfig) {
|
||||
this.bind = bind;
|
||||
this.motd = motd;
|
||||
this.showMaxPlayers = showMaxPlayers;
|
||||
@@ -128,6 +131,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
this.query = query;
|
||||
this.metrics = metrics;
|
||||
this.forceKeyAuthentication = forceKeyAuthentication;
|
||||
this.packetLimiterConfig = packetLimiterConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,19 +160,16 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
}
|
||||
|
||||
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.");
|
||||
break;
|
||||
case MODERN:
|
||||
case BUNGEEGUARD:
|
||||
case MODERN, BUNGEEGUARD -> {
|
||||
if (forwardingSecret == null || forwardingSecret.length == 0) {
|
||||
logger.error("You don't have a forwarding secret set. This is required for security.");
|
||||
valid = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (servers.getServers().isEmpty()) {
|
||||
@@ -449,6 +450,10 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
return advanced.isEnableReusePort();
|
||||
}
|
||||
|
||||
public PacketLimiterConfig getPacketLimiterConfig() {
|
||||
return packetLimiterConfig;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return MoreObjects.toStringHelper(this)
|
||||
@@ -466,6 +471,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
.add("favicon", favicon)
|
||||
.add("enablePlayerAddressLogging", enablePlayerAddressLogging)
|
||||
.add("forceKeyAuthentication", forceKeyAuthentication)
|
||||
.add("packetLimiterConfig", packetLimiterConfig)
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -504,6 +510,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
new ForwardingMigration(),
|
||||
new KeyAuthenticationMigration(),
|
||||
new MotdMigration(),
|
||||
new MiniMessageTranslationsMigration(),
|
||||
new TransferIntegrationMigration()
|
||||
};
|
||||
|
||||
@@ -515,7 +522,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
|
||||
String forwardingSecretString = System.getenv().getOrDefault(
|
||||
"VELOCITY_FORWARDING_SECRET", "");
|
||||
if (forwardingSecretString.isEmpty()) {
|
||||
if (forwardingSecretString.isBlank()) {
|
||||
final String forwardSecretFile = config.get("forwarding-secret-file");
|
||||
final Path secretPath = forwardSecretFile == null
|
||||
? defaultForwardingSecretPath
|
||||
@@ -528,7 +535,11 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
"The file " + forwardSecretFile + " is not a valid file or it is a directory.");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("The forwarding-secret-file does not exist.");
|
||||
Files.createFile(secretPath);
|
||||
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);
|
||||
@@ -557,6 +568,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
final boolean kickExisting = config.getOrElse("kick-existing-players", false);
|
||||
final boolean enablePlayerAddressLogging = config.getOrElse(
|
||||
"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
|
||||
// forwarding mode that requires it.
|
||||
@@ -584,7 +596,8 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
new Advanced(advancedConfig),
|
||||
new Query(queryConfig),
|
||||
new Metrics(metricsConfig),
|
||||
forceKeyAuthentication
|
||||
forceKeyAuthentication,
|
||||
packetLimiterConfig
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -987,4 +1000,33 @@ public class VelocityConfiguration implements ProxyConfig {
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for packet limiting.
|
||||
*
|
||||
* @param interval the interval in seconds to measure packets over
|
||||
* @param pps the maximum number of packets per second allowed
|
||||
* @param bytes the maximum number of bytes per second allowed
|
||||
*/
|
||||
public record PacketLimiterConfig(int interval, int pps, int bytes) {
|
||||
public static PacketLimiterConfig DEFAULT = new PacketLimiterConfig(7, 500, -1);
|
||||
|
||||
/**
|
||||
* returns a PacketLimiterConfig from a config section, or the default if the section is null.
|
||||
*
|
||||
* @param config the configuration object to parse
|
||||
* @return the packet limiter config, or the default if {@code config} is null
|
||||
*/
|
||||
public static PacketLimiterConfig fromConfig(CommentedConfig config) {
|
||||
if (config != null) {
|
||||
return new PacketLimiterConfig(
|
||||
config.getIntOrElse("interval", DEFAULT.interval()),
|
||||
config.getIntOrElse("packets-per-second", DEFAULT.pps()),
|
||||
config.getIntOrElse("bytes-per-second", DEFAULT.bytes())
|
||||
);
|
||||
} else {
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ public sealed interface ConfigurationMigration
|
||||
permits ForwardingMigration,
|
||||
KeyAuthenticationMigration,
|
||||
MotdMigration,
|
||||
MiniMessageTranslationsMigration,
|
||||
TransferIntegrationMigration {
|
||||
boolean shouldMigrate(CommentedFileConfig config);
|
||||
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.config.migration;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Pattern;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
/**
|
||||
* Migration from old to modern language argument format with MiniMessage.
|
||||
* Also migrates possible use of legacy colors to MiniMessage format.
|
||||
*/
|
||||
public final class MiniMessageTranslationsMigration implements ConfigurationMigration {
|
||||
@Override
|
||||
public boolean shouldMigrate(final CommentedFileConfig config) {
|
||||
// Checking whether translations should be migrated would be just as costly as attempting to migrate them directly.
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void migrate(final CommentedFileConfig config, final Logger logger) throws IOException {
|
||||
final Path langFolder = Path.of("lang");
|
||||
if (Files.notExists(langFolder)) {
|
||||
return;
|
||||
}
|
||||
final Pattern oldPlaceholderPattern = Pattern.compile("\\{(\\d+)}");
|
||||
try (final DirectoryStream<Path> stream
|
||||
= Files.newDirectoryStream(langFolder, Files::isRegularFile)) {
|
||||
for (final Path path : stream) {
|
||||
String content = Files.readString(path, StandardCharsets.UTF_8);
|
||||
if (content.indexOf('{') == -1) {
|
||||
continue;
|
||||
}
|
||||
// Migrate old arguments
|
||||
content = oldPlaceholderPattern.matcher(content).replaceAll("<arg:$1>");
|
||||
// Some setups use legacy color codes, this format is migrated to MiniMessage
|
||||
content = MiniMessage.miniMessage().serialize(
|
||||
LegacyComponentSerializer.legacySection().deserialize(content));
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ import com.velocitypowered.natives.encryption.VelocityCipher;
|
||||
import com.velocitypowered.natives.encryption.VelocityCipherFactory;
|
||||
import com.velocitypowered.natives.util.Natives;
|
||||
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.InitialLoginSessionHandler;
|
||||
import com.velocitypowered.proxy.connection.client.StatusSessionHandler;
|
||||
@@ -66,7 +67,7 @@ import io.netty.util.ReferenceCountUtil;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.HashMap;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -108,7 +109,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
this.server = server;
|
||||
this.state = StateRegistry.HANDSHAKE;
|
||||
|
||||
this.sessionHandlers = new HashMap<>();
|
||||
this.sessionHandlers = new EnumMap<>(StateRegistry.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,13 +154,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
|
||||
if (msg instanceof MinecraftPacket pkt) {
|
||||
if (!pkt.handle(activeSessionHandler)) {
|
||||
activeSessionHandler.handleGeneric((MinecraftPacket) msg);
|
||||
activeSessionHandler.handleGeneric(pkt);
|
||||
}
|
||||
} else if (msg instanceof HAProxyMessage proxyMessage) {
|
||||
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
|
||||
proxyMessage.sourcePort());
|
||||
} else if (msg instanceof ByteBuf) {
|
||||
activeSessionHandler.handleUnknown((ByteBuf) msg);
|
||||
} else if (msg instanceof ByteBuf buf) {
|
||||
activeSessionHandler.handleUnknown(buf);
|
||||
}
|
||||
} finally {
|
||||
ReferenceCountUtil.release(msg);
|
||||
@@ -368,6 +369,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
public void setState(StateRegistry state) {
|
||||
ensureInEventLoop();
|
||||
|
||||
final StateRegistry previousState = this.state;
|
||||
this.state = state;
|
||||
final MinecraftVarintFrameDecoder frameDecoder = this.channel.pipeline()
|
||||
.get(MinecraftVarintFrameDecoder.class);
|
||||
@@ -388,7 +390,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
|
||||
if (state == StateRegistry.CONFIG) {
|
||||
// Activate the play packet queue
|
||||
addPlayPacketQueueHandler();
|
||||
if (previousState == StateRegistry.PLAY
|
||||
&& this.pendingConfigurationSwitch
|
||||
&& this.association instanceof ConnectedPlayer) {
|
||||
addPlayPacketQueueOutboundHandler();
|
||||
} else {
|
||||
addPlayPacketQueueHandler();
|
||||
}
|
||||
} else {
|
||||
// Remove the queue
|
||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
|
||||
@@ -404,13 +412,23 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
* Adds the play packet queue handler.
|
||||
*/
|
||||
public void addPlayPacketQueueHandler() {
|
||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
|
||||
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
|
||||
new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
|
||||
}
|
||||
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()));
|
||||
new PlayPacketQueueInboundHandler(this.protocolVersion,
|
||||
channel.pipeline().get(MinecraftDecoder.class).getDirection()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds only the outbound play packet queue handler.
|
||||
*/
|
||||
public void addPlayPacketQueueOutboundHandler() {
|
||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
|
||||
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
|
||||
new PlayPacketQueueOutboundHandler(this.protocolVersion,
|
||||
channel.pipeline().get(MinecraftEncoder.class).getDirection()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,9 +562,10 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
||||
} else {
|
||||
int level = server.getConfiguration().getCompressionLevel();
|
||||
VelocityCompressor compressor = Natives.compress.get().create(level);
|
||||
final MinecraftDecoder minecraftDecoder = (MinecraftDecoder) channel.pipeline().get(MINECRAFT_DECODER);
|
||||
|
||||
encoder = new MinecraftCompressorAndLengthEncoder(threshold, compressor);
|
||||
decoder = new MinecraftCompressDecoder(threshold, compressor);
|
||||
decoder = new MinecraftCompressDecoder(threshold, compressor, minecraftDecoder.getDirection());
|
||||
|
||||
channel.pipeline().remove(FRAME_ENCODER);
|
||||
channel.pipeline().addBefore(MINECRAFT_DECODER, COMPRESSION_DECODER, decoder);
|
||||
|
||||
@@ -23,7 +23,11 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||
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.DialogClearPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
|
||||
@@ -48,6 +52,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||
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.StatusPingPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||
@@ -67,6 +72,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerComma
|
||||
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
||||
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.KnownPacksPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
|
||||
@@ -364,4 +371,32 @@ public interface MinecraftSessionHandler {
|
||||
default boolean handle(ClientboundServerLinksPacket packet) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+25
-29
@@ -34,6 +34,8 @@ import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.command.CommandGraphInjector;
|
||||
@@ -68,6 +70,7 @@ import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
||||
import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
@@ -91,6 +94,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||
private static final int MAXIMUM_PACKETS_TO_FLUSH =
|
||||
Integer.getInteger("velocity.max-packets-per-flush", 8192);
|
||||
private static final int LARGE_PACKET_THRESHOLD = 1024 * 128;
|
||||
|
||||
private final VelocityServer server;
|
||||
private final VelocityServerConnection serverConn;
|
||||
@@ -177,10 +181,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public boolean handle(BossBarPacket packet) {
|
||||
if (packet.getAction() == BossBarPacket.ADD) {
|
||||
playerSessionHandler.getServerBossBars().add(packet.getUuid());
|
||||
} else if (packet.getAction() == BossBarPacket.REMOVE) {
|
||||
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
|
||||
if (serverConn.getPlayer().getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||
if (packet.getAction() == BossBarPacket.ADD) {
|
||||
playerSessionHandler.getServerBossBars().add(packet.getUuid());
|
||||
} else if (packet.getAction() == BossBarPacket.REMOVE) {
|
||||
playerSessionHandler.getServerBossBars().remove(packet.getUuid());
|
||||
}
|
||||
}
|
||||
return false; // forward
|
||||
}
|
||||
@@ -290,31 +296,14 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register and unregister packets are simply forwarded to the server as-is.
|
||||
if (PluginMessageUtil.isRegister(packet) || PluginMessageUtil.isUnregister(packet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PluginMessageUtil.isMcBrand(packet)) {
|
||||
PluginMessagePacket rewritten = PluginMessageUtil
|
||||
.rewriteMinecraftBrand(packet,
|
||||
server.getVersion(), playerConnection.getProtocolVersion());
|
||||
playerConnection.write(rewritten);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (serverConn.getPhase().handle(serverConn, serverConn.getPlayer(), packet)) {
|
||||
// Handled.
|
||||
return true;
|
||||
}
|
||||
|
||||
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy);
|
||||
String channel = packet.getChannel();
|
||||
PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
|
||||
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
|
||||
if (pme.getResult().isAllowed() && !playerConnection.isClosed()) {
|
||||
PluginMessagePacket copied = new PluginMessagePacket(
|
||||
@@ -359,7 +348,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
// Inject commands from the proxy.
|
||||
final CommandGraphInjector<CommandSource> injector = server.getCommandManager().getInjector();
|
||||
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(
|
||||
@@ -445,11 +439,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void handleGeneric(MinecraftPacket packet) {
|
||||
if (packet instanceof PluginMessagePacket) {
|
||||
((PluginMessagePacket) packet).retain();
|
||||
if (packet instanceof PluginMessagePacket pluginMessage) {
|
||||
pluginMessage.retain();
|
||||
}
|
||||
boolean huge = packet instanceof DeferredByteBufHolder def && def.content().readableBytes() > LARGE_PACKET_THRESHOLD;
|
||||
playerConnection.delayedWrite(packet);
|
||||
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||
playerConnection.flush();
|
||||
packetsFlushed = 0;
|
||||
}
|
||||
@@ -457,8 +452,9 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void handleUnknown(ByteBuf buf) {
|
||||
boolean huge = buf.readableBytes() > LARGE_PACKET_THRESHOLD;
|
||||
playerConnection.delayedWrite(buf.retain());
|
||||
if (++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||
if (huge || ++packetsFlushed >= MAXIMUM_PACKETS_TO_FLUSH) {
|
||||
playerConnection.flush();
|
||||
packetsFlushed = 0;
|
||||
}
|
||||
@@ -511,4 +507,4 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
playerConnection.setAutoReading(writable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-59
@@ -344,66 +344,30 @@ public class BungeeCordMessageResponder {
|
||||
return false;
|
||||
}
|
||||
|
||||
ByteBufDataInput in = new ByteBufDataInput(message.content());
|
||||
String subChannel = in.readUTF();
|
||||
final ByteBufDataInput in = new ByteBufDataInput(message.content());
|
||||
final String subChannel = in.readUTF();
|
||||
switch (subChannel) {
|
||||
case "GetPlayerServer":
|
||||
this.processGetPlayerServer(in);
|
||||
break;
|
||||
case "ForwardToPlayer":
|
||||
this.processForwardToPlayer(in);
|
||||
break;
|
||||
case "Forward":
|
||||
this.processForwardToServer(in);
|
||||
break;
|
||||
case "Connect":
|
||||
this.processConnect(in);
|
||||
break;
|
||||
case "ConnectOther":
|
||||
this.processConnectOther(in);
|
||||
break;
|
||||
case "IP":
|
||||
this.processIp(in);
|
||||
break;
|
||||
case "PlayerCount":
|
||||
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;
|
||||
case "GetPlayerServer" -> this.processGetPlayerServer(in);
|
||||
case "ForwardToPlayer" -> this.processForwardToPlayer(in);
|
||||
case "Forward" -> this.processForwardToServer(in);
|
||||
case "Connect" -> this.processConnect(in);
|
||||
case "ConnectOther" -> this.processConnectOther(in);
|
||||
case "IP" -> this.processIp(in);
|
||||
case "PlayerCount" -> this.processPlayerCount(in);
|
||||
case "PlayerList" -> this.processPlayerList(in);
|
||||
case "GetServers" -> this.processGetServers();
|
||||
case "Message" -> this.processMessage(in);
|
||||
case "MessageRaw" -> this.processMessageRaw(in);
|
||||
case "GetServer" -> this.processGetServer();
|
||||
case "UUID" -> this.processUuid();
|
||||
case "UUIDOther" -> this.processUuidOther(in);
|
||||
case "IPOther" -> this.processIpOther(in);
|
||||
case "ServerIP" -> this.processServerIp(in);
|
||||
case "KickPlayer" -> this.processKick(in);
|
||||
case "KickPlayerRaw" -> this.processKickRaw(in);
|
||||
default -> {
|
||||
// Do nothing, unknown command
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
+37
-5
@@ -52,6 +52,7 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
||||
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.RegistrySyncPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
||||
@@ -59,7 +60,7 @@ import com.velocitypowered.proxy.protocol.packet.config.TagsUpdatePacket;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import java.io.IOException;
|
||||
import io.netty.channel.Channel;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.kyori.adventure.key.Key;
|
||||
@@ -71,6 +72,9 @@ import org.apache.logging.log4j.Logger;
|
||||
* 1.20.2+ switching. Yes, some of this is exceptionally stupid.
|
||||
*/
|
||||
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 final VelocityServer server;
|
||||
private final VelocityServerConnection serverConn;
|
||||
@@ -256,7 +260,13 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public boolean handle(DisconnectPacket packet) {
|
||||
serverConn.disconnect();
|
||||
resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer()));
|
||||
// If the player receives a DisconnectPacket without a connection to a server in progress,
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -358,10 +368,16 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(CodeOfConductPacket packet) {
|
||||
this.serverConn.getPlayer().getConnection().write(packet.retain());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnected() {
|
||||
resultFuture.completeExceptionally(
|
||||
new IOException("Unexpectedly disconnected from remote server"));
|
||||
resultFuture.complete(ConnectionRequestResults.forDisconnect(
|
||||
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -369,6 +385,22 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||
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) {
|
||||
logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(),
|
||||
serverConn.getPlayer().getUsername(), cause);
|
||||
@@ -382,4 +414,4 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
||||
public enum State {
|
||||
START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -165,7 +165,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
|
||||
smc.setAutoReading(false);
|
||||
clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
|
||||
clientPlaySessionHandler.doSwitch().thenRunAsync(() -> smc.setAutoReading(true), smc.eventLoop());
|
||||
} else {
|
||||
// Initial login - the player is already in configuration state.
|
||||
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
|
||||
|
||||
+4
-6
@@ -38,7 +38,6 @@ import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -122,9 +121,8 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
// Change the client to use the ClientPlaySessionHandler if required.
|
||||
ClientPlaySessionHandler playHandler;
|
||||
if (player.getConnection()
|
||||
.getActiveSessionHandler() instanceof ClientPlaySessionHandler) {
|
||||
playHandler =
|
||||
(ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler();
|
||||
.getActiveSessionHandler() instanceof ClientPlaySessionHandler sessionHandler) {
|
||||
playHandler = sessionHandler;
|
||||
} else {
|
||||
playHandler = new ClientPlaySessionHandler(server, player);
|
||||
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler);
|
||||
@@ -214,7 +212,7 @@ public class TransitionSessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
public void disconnected() {
|
||||
resultFuture
|
||||
.completeExceptionally(new IOException("Unexpectedly disconnected from remote server"));
|
||||
resultFuture.complete(ConnectionRequestResults.forDisconnect(
|
||||
ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR, serverConn.getServer()));
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -53,6 +53,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -70,6 +71,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
private boolean gracefulDisconnect = false;
|
||||
private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN;
|
||||
private final Map<Long, Long> pendingPings = new HashMap<>();
|
||||
private @MonotonicNonNull Integer entityId;
|
||||
|
||||
/**
|
||||
* Initializes a new server connection.
|
||||
@@ -178,9 +180,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
handshake.setServerAddress(createBungeeGuardForwardingAddress(secret));
|
||||
} else if (proxyPlayer.getConnection().getType() == ConnectionTypes.LEGACY_FORGE) {
|
||||
handshake.setServerAddress(playerVhost + HANDSHAKE_HOSTNAME_TOKEN);
|
||||
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType) {
|
||||
handshake.setServerAddress(playerVhost + ((ModernForgeConnectionType) proxyPlayer
|
||||
.getConnection().getType()).getModernToken());
|
||||
} else if (proxyPlayer.getConnection().getType() instanceof ModernForgeConnectionType forgeConnection) {
|
||||
handshake.setServerAddress(playerVhost + forgeConnection.getModernToken());
|
||||
} else {
|
||||
handshake.setServerAddress(playerVhost);
|
||||
}
|
||||
@@ -324,6 +325,14 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation,
|
||||
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
|
||||
* closed, the player is still connected to the server, and the player still remains online.
|
||||
|
||||
+4
-2
@@ -69,14 +69,16 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
||||
private @MonotonicNonNull ConnectedPlayer connectedPlayer;
|
||||
private final boolean onlineMode;
|
||||
private State loginState = State.START; // 1.20.2+
|
||||
private final String serverIdHash;
|
||||
|
||||
AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound,
|
||||
GameProfile profile, boolean onlineMode) {
|
||||
GameProfile profile, boolean onlineMode, String serverIdHash) {
|
||||
this.server = Preconditions.checkNotNull(server, "server");
|
||||
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
|
||||
this.profile = Preconditions.checkNotNull(profile, "profile");
|
||||
this.onlineMode = onlineMode;
|
||||
this.mcConnection = inbound.delegatedConnection();
|
||||
this.serverIdHash = serverIdHash;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,7 +215,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
||||
private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) {
|
||||
mcConnection.setAssociation(player);
|
||||
|
||||
server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> {
|
||||
server.getEventManager().fire(new LoginEvent(player, serverIdHash)).thenAcceptAsync(event -> {
|
||||
if (mcConnection.isClosed()) {
|
||||
// The player was disconnected
|
||||
server.getEventManager().fireAndForget(new DisconnectEvent(player,
|
||||
|
||||
+55
@@ -33,6 +33,7 @@ import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResp
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
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.packet.ClientSettingsPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
||||
@@ -40,6 +41,8 @@ import com.velocitypowered.proxy.protocol.packet.PingIdentifyPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||
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.KnownPacksPacket;
|
||||
import com.velocitypowered.proxy.protocol.util.PluginMessageUtil;
|
||||
@@ -58,6 +61,8 @@ import org.apache.logging.log4j.Logger;
|
||||
* Handles the client config stage.
|
||||
*/
|
||||
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 final VelocityServer server;
|
||||
@@ -205,6 +210,26 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||
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
|
||||
public void handleGeneric(MinecraftPacket packet) {
|
||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||
@@ -244,6 +269,36 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
||||
@Override
|
||||
public void exception(Throwable throwable) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+99
-68
@@ -21,15 +21,17 @@ import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.construc
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import com.velocitypowered.api.command.VelocityBrigadierMessage;
|
||||
import com.velocitypowered.api.event.connection.PluginMessageEvent;
|
||||
import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
||||
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.TabCompleteEvent;
|
||||
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.connection.ConnectionTypes;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
@@ -41,6 +43,7 @@ import com.velocitypowered.proxy.connection.forge.legacy.LegacyForgeConstants;
|
||||
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
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.ClientSettingsPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.JoinGamePacket;
|
||||
@@ -87,6 +90,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.ComponentLike;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -97,6 +101,8 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
* center that joins backend servers with players.
|
||||
*/
|
||||
public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
private static final boolean BACKPRESSURE_LOG =
|
||||
Boolean.getBoolean("velocity.log-server-backpressure");
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(ClientPlaySessionHandler.class);
|
||||
|
||||
@@ -318,8 +324,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
new PlayerChannelRegisterEvent(player, ImmutableList.copyOf(channels)));
|
||||
backendConn.write(packet.retain());
|
||||
} else if (PluginMessageUtil.isUnregister(packet)) {
|
||||
player.getClientsideChannels()
|
||||
.removeAll(PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion()));
|
||||
List<ChannelIdentifier> channels =
|
||||
PluginMessageUtil.getChannels(0, packet, this.player.getProtocolVersion());
|
||||
player.getClientsideChannels().removeAll(channels);
|
||||
server.getEventManager()
|
||||
.fireAndForget(
|
||||
new PlayerChannelUnregisterEvent(player, ImmutableList.copyOf(channels)));
|
||||
backendConn.write(packet.retain());
|
||||
} else if (PluginMessageUtil.isMcBrand(packet)) {
|
||||
String brand = PluginMessageUtil.readBrandMessage(packet.content());
|
||||
@@ -339,43 +349,25 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
if (!player.getPhase().handle(player, packet, serverConn)) {
|
||||
ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel());
|
||||
if (id == null) {
|
||||
// We don't have any plugins listening on this channel, process the packet now.
|
||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||
.consideredComplete()) {
|
||||
// The client is trying to send messages too early. This is primarily caused by mods,
|
||||
// but further aggravated by Velocity. To work around these issues, we will queue any
|
||||
// non-FML handshake messages to be sent once the FML handshake has completed or the
|
||||
// JoinGame packet has been received by the proxy, whichever comes first.
|
||||
//
|
||||
// We also need to make sure to retain these packets, so they can be flushed
|
||||
// appropriately.
|
||||
loginPluginMessages.add(packet.retain());
|
||||
} else {
|
||||
// The connection is ready, send the packet now.
|
||||
backendConn.write(packet.retain());
|
||||
}
|
||||
} else {
|
||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, id, copy);
|
||||
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
|
||||
if (pme.getResult().isAllowed()) {
|
||||
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
|
||||
Unpooled.wrappedBuffer(copy));
|
||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||
.consideredComplete()) {
|
||||
// We're still processing the connection (see above), enqueue the packet for now.
|
||||
loginPluginMessages.add(message.retain());
|
||||
} else {
|
||||
backendConn.write(message);
|
||||
}
|
||||
byte[] copy = ByteBufUtil.getBytes(packet.content());
|
||||
String channel = packet.getChannel();
|
||||
PluginMessageEvent event = new PluginMessageEvent(player, serverConn, channel.indexOf(':') == -1 ? new LegacyChannelIdentifier(channel) : MinecraftChannelIdentifier.from(channel), copy);
|
||||
server.getEventManager().fire(event).thenAcceptAsync(pme -> {
|
||||
if (pme.getResult().isAllowed()) {
|
||||
PluginMessagePacket message = new PluginMessagePacket(packet.getChannel(),
|
||||
Unpooled.wrappedBuffer(copy));
|
||||
if (!player.getPhase().consideredComplete() || !serverConn.getPhase()
|
||||
.consideredComplete()) {
|
||||
// We're still processing the connection (see above), enqueue the packet for now.
|
||||
loginPluginMessages.add(message.retain());
|
||||
} else {
|
||||
backendConn.write(message);
|
||||
}
|
||||
}, backendConn.eventLoop()).exceptionally((ex) -> {
|
||||
logger.error("Exception while handling plugin message packet for {}", player, ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}, backendConn.eventLoop()).exceptionally((ex) -> {
|
||||
logger.error("Exception while handling plugin message packet for {}", player, ex);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,7 +455,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && serverConnection.getPhase().consideredComplete()) {
|
||||
final boolean stateAllowsForward = smc != null
|
||||
&& !smc.isClosed()
|
||||
&& serverConnection.getPhase().consideredComplete()
|
||||
&& smc.getState() == StateRegistry.PLAY;
|
||||
if (stateAllowsForward) {
|
||||
if (packet instanceof PluginMessagePacket) {
|
||||
((PluginMessagePacket) packet).retain();
|
||||
}
|
||||
@@ -480,7 +476,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
|
||||
MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) {
|
||||
final boolean stateAllowsForward = smc != null
|
||||
&& !smc.isClosed()
|
||||
&& serverConnection.getPhase().consideredComplete()
|
||||
&& smc.getState() == StateRegistry.PLAY;
|
||||
if (stateAllowsForward) {
|
||||
smc.write(buf.retain());
|
||||
}
|
||||
}
|
||||
@@ -492,14 +492,24 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
@Override
|
||||
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() {
|
||||
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) {
|
||||
// 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
|
||||
@@ -535,9 +545,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
|
||||
// Config state clears everything in the client. No need to clear later.
|
||||
spawned = false;
|
||||
serverBossBars.clear();
|
||||
player.clearPlayerListHeaderAndFooterSilent();
|
||||
player.getTabList().clearAllSilent();
|
||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||
player.getBossBarManager().dropPackets();
|
||||
} else {
|
||||
serverBossBars.clear();
|
||||
}
|
||||
}
|
||||
|
||||
player.switchToConfigState();
|
||||
@@ -575,15 +589,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to
|
||||
// track them.
|
||||
for (UUID serverBossBar : serverBossBars) {
|
||||
BossBarPacket deletePacket = new BossBarPacket();
|
||||
deletePacket.setUuid(serverBossBar);
|
||||
deletePacket.setAction(BossBarPacket.REMOVE);
|
||||
player.getConnection().delayedWrite(deletePacket);
|
||||
destination.setEntityId(joinGame.getEntityId()); // used for sound api
|
||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||
player.getBossBarManager().sendBossBars();
|
||||
} else {
|
||||
// Remove previous boss bars. These don't get cleared when sending JoinGame (up until 1.20.2),
|
||||
// thus the need to track them.
|
||||
for (UUID serverBossBar : serverBossBars) {
|
||||
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.
|
||||
ProtocolVersion serverVersion = serverMc.getProtocolVersion();
|
||||
@@ -694,23 +713,35 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
List<Offer> offers = new ArrayList<>();
|
||||
for (Suggestion suggestion : suggestions.getList()) {
|
||||
String offer = suggestion.getText();
|
||||
ComponentHolder tooltip = null;
|
||||
if (suggestion.getTooltip() != null
|
||||
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
|
||||
tooltip = new ComponentHolder(player.getProtocolVersion(),
|
||||
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
|
||||
int startPos = -1;
|
||||
for (var suggestion : suggestions.getList()) {
|
||||
if (startPos == -1 || startPos > suggestion.getRange().getStart()) {
|
||||
startPos = suggestion.getRange().getStart();
|
||||
}
|
||||
offers.add(new Offer(offer, tooltip));
|
||||
}
|
||||
int startPos = packet.getCommand().lastIndexOf(' ') + 1;
|
||||
|
||||
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();
|
||||
resp.setTransactionId(packet.getTransactionId());
|
||||
resp.setStart(startPos);
|
||||
resp.setLength(packet.getCommand().length() - startPos);
|
||||
resp.setStart(startPos + 1);
|
||||
resp.setLength(packet.getCommand().length() - startPos - 1);
|
||||
resp.getOffers().addAll(offers);
|
||||
player.getConnection().write(resp);
|
||||
}
|
||||
@@ -765,10 +796,10 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
||||
offer = offer.substring(command.length());
|
||||
}
|
||||
ComponentHolder tooltip = null;
|
||||
if (suggestion.getTooltip() != null
|
||||
&& suggestion.getTooltip() instanceof VelocityBrigadierMessage) {
|
||||
tooltip = new ComponentHolder(player.getProtocolVersion(),
|
||||
((VelocityBrigadierMessage) suggestion.getTooltip()).asComponent());
|
||||
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()));
|
||||
}
|
||||
response.getOffers().add(new Offer(offer, tooltip));
|
||||
}
|
||||
|
||||
+137
-76
@@ -62,6 +62,7 @@ import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||
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.resourcepack.VelocityResourcePackInfo;
|
||||
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
|
||||
@@ -73,6 +74,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||
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.DisconnectPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.HeaderAndFooterPacket;
|
||||
@@ -127,9 +130,12 @@ import net.kyori.adventure.pointer.PointersSupplier;
|
||||
import net.kyori.adventure.resource.ResourcePackInfoLike;
|
||||
import net.kyori.adventure.resource.ResourcePackRequest;
|
||||
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.format.NamedTextColor;
|
||||
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.plain.PlainTextComponentSerializer;
|
||||
import net.kyori.adventure.title.Title.Times;
|
||||
@@ -197,6 +203,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
private @Nullable ClientSettingsPacket clientSettingsPacket;
|
||||
private volatile ChatQueue chatQueue;
|
||||
private final ChatBuilderFactory chatBuilderFactory;
|
||||
private final BossBarManager bossBarManager;
|
||||
|
||||
ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection,
|
||||
@Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode,
|
||||
@@ -223,6 +230,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
this.chatQueue = new ChatQueue(this);
|
||||
this.chatBuilderFactory = new ChatBuilderFactory(this.getProtocolVersion());
|
||||
this.resourcePackHandler = ResourcePackHandler.create(this, server);
|
||||
this.bossBarManager = new BossBarManager(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -706,12 +714,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
Component friendlyError;
|
||||
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
|
||||
friendlyError = Component.translatable("velocity.error.connected-server-error",
|
||||
Component.text(server.getServerInfo().getName()));
|
||||
Argument.string("server", server.getServerInfo().getName()));
|
||||
} else {
|
||||
logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(),
|
||||
wrapped);
|
||||
friendlyError = Component.translatable("velocity.error.connecting-server-error",
|
||||
Component.text(server.getServerInfo().getName()));
|
||||
Argument.string("server", server.getServerInfo().getName()));
|
||||
}
|
||||
handleConnectionException(server, null, friendlyError.color(NamedTextColor.RED), safe);
|
||||
}
|
||||
@@ -733,18 +741,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
Component disconnectReason = disconnect.getReason().getComponent();
|
||||
String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason);
|
||||
if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) {
|
||||
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
||||
plainTextReason);
|
||||
if (this.server.getConfiguration().isLogPlayerConnections()) {
|
||||
logger.info("{}: kicked from server {}: {}", this, server.getServerInfo().getName(),
|
||||
plainTextReason);
|
||||
}
|
||||
handleConnectionException(server, disconnectReason,
|
||||
Component.translatable("velocity.error.moved-to-new-server", NamedTextColor.RED,
|
||||
Component.text(server.getServerInfo().getName()),
|
||||
Argument.string("server", server.getServerInfo().getName()),
|
||||
disconnectReason), safe);
|
||||
} else {
|
||||
logger.error("{}: disconnected while connecting to {}: {}", this,
|
||||
server.getServerInfo().getName(), plainTextReason);
|
||||
if (this.server.getConfiguration().isLogPlayerConnections()) {
|
||||
logger.error("{}: disconnected while connecting to {}: {}", this,
|
||||
server.getServerInfo().getName(), plainTextReason);
|
||||
}
|
||||
handleConnectionException(server, disconnectReason,
|
||||
Component.translatable("velocity.error.cant-connect", NamedTextColor.RED,
|
||||
Component.text(server.getServerInfo().getName()),
|
||||
Argument.string("server", server.getServerInfo().getName()),
|
||||
disconnectReason), safe);
|
||||
}
|
||||
}
|
||||
@@ -800,63 +812,56 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.getResult() instanceof final DisconnectPlayer res) {
|
||||
disconnect(res.getReasonComponent());
|
||||
} else if (event.getResult() instanceof final RedirectPlayer res) {
|
||||
createConnectionRequest(res.getServer(), previousConnection).connect()
|
||||
.whenCompleteAsync((status, throwable) -> {
|
||||
if (throwable != null) {
|
||||
handleConnectionException(
|
||||
status != null ? status.getAttemptedConnection() : res.getServer(), throwable,
|
||||
true);
|
||||
return;
|
||||
}
|
||||
switch (event.getResult()) {
|
||||
case DisconnectPlayer res -> disconnect(res.getReasonComponent());
|
||||
case RedirectPlayer res -> createConnectionRequest(res.getServer(), previousConnection).connect()
|
||||
.whenCompleteAsync((status, throwable) -> {
|
||||
if (throwable != null) {
|
||||
handleConnectionException(res.getServer(), throwable, true);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (status.getStatus()) {
|
||||
// Impossible/nonsensical cases
|
||||
case ALREADY_CONNECTED:
|
||||
logger.error("{}: already connected to {}", this,
|
||||
status.getAttemptedConnection().getServerInfo().getName());
|
||||
break;
|
||||
case CONNECTION_IN_PROGRESS:
|
||||
// Fatal case
|
||||
case CONNECTION_CANCELLED:
|
||||
Component fallbackMsg = res.getMessageComponent();
|
||||
if (fallbackMsg == null) {
|
||||
fallbackMsg = friendlyReason;
|
||||
switch (status.getStatus()) {
|
||||
// Impossible/nonsensical cases
|
||||
case ALREADY_CONNECTED -> logger.error("{}: already connected to {}", this,
|
||||
status.getAttemptedConnection().getServerInfo().getName());
|
||||
case CONNECTION_IN_PROGRESS, CONNECTION_CANCELLED -> {
|
||||
Component fallbackMsg = res.getMessageComponent();
|
||||
if (fallbackMsg == null) {
|
||||
fallbackMsg = friendlyReason;
|
||||
}
|
||||
disconnect(status.getReasonComponent().orElse(fallbackMsg));
|
||||
}
|
||||
case SERVER_DISCONNECTED -> {
|
||||
Component reason = status.getReasonComponent()
|
||||
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
||||
handleConnectionException(res.getServer(),
|
||||
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
|
||||
((Impl) status).isSafe());
|
||||
}
|
||||
case SUCCESS -> {
|
||||
Component requestedMessage = res.getMessageComponent();
|
||||
if (requestedMessage == null) {
|
||||
requestedMessage = friendlyReason;
|
||||
}
|
||||
if (requestedMessage != Component.empty()) {
|
||||
sendMessage(requestedMessage);
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
// The only remaining value is successful (no need to do anything!)
|
||||
}
|
||||
}
|
||||
disconnect(status.getReasonComponent().orElse(fallbackMsg));
|
||||
break;
|
||||
case SERVER_DISCONNECTED:
|
||||
Component reason = status.getReasonComponent()
|
||||
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
||||
handleConnectionException(res.getServer(),
|
||||
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()),
|
||||
((Impl) status).isSafe());
|
||||
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());
|
||||
}, connection.eventLoop());
|
||||
case 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.
|
||||
disconnect(friendlyReason);
|
||||
default -> disconnect(friendlyReason);
|
||||
}
|
||||
}, connection.eventLoop());
|
||||
}
|
||||
@@ -1038,6 +1043,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
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
|
||||
public void transferToHost(final InetSocketAddress address) {
|
||||
Preconditions.checkNotNull(address);
|
||||
@@ -1300,11 +1349,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
||||
if (sentTime != null) {
|
||||
final MinecraftConnection smc = serverConnection.getConnection();
|
||||
if (smc != null) {
|
||||
final StateRegistry clientState = connection.getState();
|
||||
final boolean stateAllowsForward = smc != null
|
||||
&& !smc.isClosed()
|
||||
&& clientState == smc.getState()
|
||||
&& (clientState == StateRegistry.CONFIG || clientState == StateRegistry.PLAY);
|
||||
if (stateAllowsForward) {
|
||||
setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
|
||||
smc.write(packet);
|
||||
return true;
|
||||
}
|
||||
// We removed this, and so this is ours
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -1314,7 +1369,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
* Switches the connection to the client into config state.
|
||||
*/
|
||||
public void switchToConfigState() {
|
||||
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
|
||||
final VelocityServerConnection targetServer = getConnectionInFlightOrConnectedServer();
|
||||
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, targetServer))
|
||||
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
|
||||
// if the connection was closed earlier, there is a risk that the player is no longer connected
|
||||
if (!connection.getChannel().isActive()) {
|
||||
@@ -1329,7 +1385,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
connection.pendingConfigurationSwitch = true;
|
||||
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
||||
// Make sure we don't send any play packets to the player after update start
|
||||
connection.addPlayPacketQueueHandler();
|
||||
connection.addPlayPacketQueueOutboundHandler();
|
||||
}, connection.eventLoop()).exceptionally((ex) -> {
|
||||
logger.error("Error switching player connection to config state", ex);
|
||||
return null;
|
||||
@@ -1379,6 +1435,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
return handshakeIntent;
|
||||
}
|
||||
|
||||
public BossBarManager getBossBarManager() {
|
||||
return bossBarManager;
|
||||
}
|
||||
|
||||
private final class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
|
||||
|
||||
private final RegisteredServer toConnect;
|
||||
@@ -1438,7 +1498,16 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
VelocityServerConnection con =
|
||||
new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server);
|
||||
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());
|
||||
});
|
||||
@@ -1452,22 +1521,14 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Result> connect() {
|
||||
return this.internalConnect().whenCompleteAsync((status, throwable) -> {
|
||||
if (status != null && !status.isSuccessful()) {
|
||||
if (!status.isSafe()) {
|
||||
handleConnectionException(status.getAttemptedConnection(), throwable, false);
|
||||
}
|
||||
}
|
||||
}, connection.eventLoop()).thenApply(x -> x);
|
||||
return this.internalConnect().thenApply(x -> x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> connectWithIndication() {
|
||||
return internalConnect().whenCompleteAsync((status, throwable) -> {
|
||||
if (throwable != null) {
|
||||
// TODO: The exception handling from this is not very good. Find a better way.
|
||||
handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect,
|
||||
throwable, true);
|
||||
handleConnectionException(toConnect, throwable, true);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -127,10 +127,10 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler {
|
||||
if (!handshake.getProtocolVersion().isSupported()) {
|
||||
// Bump connection into correct protocol state so that we can send the disconnect packet.
|
||||
connection.setState(StateRegistry.LOGIN);
|
||||
ic.disconnectQuietly(Component.translatable()
|
||||
.key("multiplayer.disconnect.outdated_client")
|
||||
.arguments(Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING))
|
||||
.build());
|
||||
ic.disconnectQuietly(Component.translatable(
|
||||
"multiplayer.disconnect.outdated_client",
|
||||
Component.text(ProtocolVersion.SUPPORTED_VERSION_STRING)
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+9
-10
@@ -152,7 +152,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
} else {
|
||||
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
|
||||
new AuthSessionHandler(server, inbound,
|
||||
GameProfile.forOfflinePlayer(login.getUsername()), false));
|
||||
GameProfile.forOfflinePlayer(login.getUsername()), false, null));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -214,6 +214,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
server.getVersion().getName() + "/" + server.getVersion().getVersion())
|
||||
.uri(URI.create(url))
|
||||
.build();
|
||||
//noinspection resource
|
||||
final HttpClient httpClient = server.createHttpClient();
|
||||
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString())
|
||||
.whenCompleteAsync((response, throwable) -> {
|
||||
@@ -254,7 +255,7 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
// All went well, initialize the session.
|
||||
mcConnection.setActiveSessionHandler(StateRegistry.LOGIN,
|
||||
new AuthSessionHandler(server, inbound, profile, true));
|
||||
new AuthSessionHandler(server, inbound, profile, true, serverId));
|
||||
} else if (response.statusCode() == 204) {
|
||||
// Apparently an offline-mode user logged onto this online-mode proxy.
|
||||
inbound.disconnect(
|
||||
@@ -268,14 +269,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler {
|
||||
}
|
||||
}, mcConnection.eventLoop())
|
||||
.thenRun(() -> {
|
||||
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);
|
||||
}
|
||||
try {
|
||||
httpClient.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) {
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -94,7 +94,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
|
||||
this.outstandingResourcePacks.get(info.getId());
|
||||
outstandingResourcePacks.add(info);
|
||||
if (outstandingResourcePacks.size() == 1) {
|
||||
tickResourcePackQueue(outstandingResourcePacks.get(0).getId());
|
||||
tickResourcePackQueue(outstandingResourcePacks.getFirst().getId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
|
||||
final List<ResourcePackInfo> outstandingResourcePacks =
|
||||
this.outstandingResourcePacks.get(uuid);
|
||||
if (!outstandingResourcePacks.isEmpty()) {
|
||||
sendResourcePackRequestPacket(outstandingResourcePacks.get(0));
|
||||
sendResourcePackRequestPacket(outstandingResourcePacks.getFirst());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +124,7 @@ public final class ModernResourcePackHandler extends ResourcePackHandler {
|
||||
this.outstandingResourcePacks.get(uuid);
|
||||
final boolean peek = bundle.status().isIntermediate();
|
||||
final ResourcePackInfo queued = outstandingResourcePacks.isEmpty() ? null :
|
||||
peek ? outstandingResourcePacks.get(0) : outstandingResourcePacks.remove(0);
|
||||
peek ? outstandingResourcePacks.getFirst() : outstandingResourcePacks.removeFirst();
|
||||
|
||||
server.getEventManager()
|
||||
.fire(new PlayerResourcePackStatusEvent(this.player, uuid, bundle.status(), queued))
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ public abstract sealed class ResourcePackHandler
|
||||
}
|
||||
request.setRequired(queued.getShouldForce());
|
||||
request.setPrompt(queued.getPrompt() == null ? null :
|
||||
new ComponentHolder(player.getProtocolVersion(), queued.getPrompt()));
|
||||
new ComponentHolder(player.getProtocolVersion(), player.translateMessage(queued.getPrompt())));
|
||||
|
||||
player.getConnection().write(request);
|
||||
}
|
||||
|
||||
+51
-48
@@ -36,6 +36,7 @@ import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import net.kyori.adventure.text.Component;
|
||||
|
||||
/**
|
||||
* Common utilities for handling server list ping results.
|
||||
@@ -99,58 +100,60 @@ public class ServerListPingHandler {
|
||||
|
||||
CompletableFuture<List<ServerPing>> pingResponses = CompletableFutures.successfulAsList(pings,
|
||||
(ex) -> fallback);
|
||||
switch (mode) {
|
||||
case ALL:
|
||||
return pingResponses.thenApply(responses -> {
|
||||
// Find the first non-fallback
|
||||
for (ServerPing response : responses) {
|
||||
if (response == fallback) {
|
||||
continue;
|
||||
}
|
||||
return response;
|
||||
return switch (mode) {
|
||||
case ALL -> pingResponses.thenApply(responses -> {
|
||||
// Find the first non-fallback
|
||||
for (ServerPing response : responses) {
|
||||
if (response == fallback) {
|
||||
continue;
|
||||
}
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new ServerPing(
|
||||
fallback.getVersion(),
|
||||
fallback.getPlayers().orElse(null),
|
||||
response.getDescriptionComponent(),
|
||||
fallback.getFavicon().orElse(null),
|
||||
response.getModinfo().orElse(null)
|
||||
);
|
||||
if (response.getDescriptionComponent() == null) {
|
||||
return response.asBuilder()
|
||||
.description(Component.empty())
|
||||
.build();
|
||||
}
|
||||
return fallback;
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
return fallback;
|
||||
});
|
||||
case MODS -> pingResponses.thenApply(responses -> {
|
||||
// Find the first non-fallback that contains a mod list
|
||||
for (ServerPing response : responses) {
|
||||
if (response == fallback) {
|
||||
continue;
|
||||
}
|
||||
Optional<ModInfo> modInfo = response.getModinfo();
|
||||
if (modInfo.isPresent()) {
|
||||
return fallback.asBuilder().mods(modInfo.get()).build();
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
});
|
||||
case DESCRIPTION -> pingResponses.thenApply(responses -> {
|
||||
// Find the first non-fallback. If it includes a modlist, add it too.
|
||||
for (ServerPing response : responses) {
|
||||
if (response == fallback) {
|
||||
continue;
|
||||
}
|
||||
if (response.getDescriptionComponent() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new ServerPing(
|
||||
fallback.getVersion(),
|
||||
fallback.getPlayers().orElse(null),
|
||||
response.getDescriptionComponent(),
|
||||
fallback.getFavicon().orElse(null),
|
||||
response.getModinfo().orElse(null)
|
||||
);
|
||||
}
|
||||
return fallback;
|
||||
});
|
||||
// Not possible, but covered for completeness.
|
||||
default:
|
||||
return CompletableFuture.completedFuture(fallback);
|
||||
}
|
||||
default -> CompletableFuture.completedFuture(fallback);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,6 +33,7 @@ import net.kyori.adventure.permission.PermissionChecker;
|
||||
import net.kyori.adventure.platform.facet.FacetPointers;
|
||||
import net.kyori.adventure.platform.facet.FacetPointers.Type;
|
||||
import net.kyori.adventure.pointer.Pointers;
|
||||
import net.kyori.adventure.pointer.PointersSupplier;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
||||
@@ -59,11 +60,11 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
||||
|
||||
private final VelocityServer server;
|
||||
private PermissionFunction permissionFunction = ALWAYS_TRUE;
|
||||
private final @NotNull Pointers pointers = ConsoleCommandSource.super.pointers().toBuilder()
|
||||
.withDynamic(PermissionChecker.POINTER, this::getPermissionChecker)
|
||||
.withDynamic(Identity.LOCALE, () -> ClosestLocaleMatcher.INSTANCE
|
||||
private static final @NotNull PointersSupplier<VelocityConsole> POINTERS = PointersSupplier.<VelocityConsole>builder()
|
||||
.resolving(PermissionChecker.POINTER, VelocityConsole::getPermissionChecker)
|
||||
.resolving(Identity.LOCALE, (console) -> ClosestLocaleMatcher.INSTANCE
|
||||
.lookupClosest(Locale.getDefault()))
|
||||
.withStatic(FacetPointers.TYPE, Type.CONSOLE)
|
||||
.resolving(FacetPointers.TYPE, (console) -> Type.CONSOLE)
|
||||
.build();
|
||||
|
||||
public VelocityConsole(VelocityServer server) {
|
||||
@@ -136,6 +137,10 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
||||
if (!this.server.getCommandManager().executeAsync(this, command).join()) {
|
||||
sendMessage(Component.translatable("velocity.command.command-does-not-exist",
|
||||
NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
if (server.getConfiguration().isLogCommandExecutions()) {
|
||||
logger.info("CONSOLE -> executed command /{}", command);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("An error occurred while running this command.", e);
|
||||
@@ -149,6 +154,6 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
|
||||
|
||||
@Override
|
||||
public @NotNull Pointers pointers() {
|
||||
return pointers;
|
||||
return POINTERS.view(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,10 @@ import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER;
|
||||
import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT;
|
||||
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||
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.StateRegistry;
|
||||
import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder;
|
||||
@@ -72,6 +74,17 @@ public class ServerChannelInitializer extends ChannelInitializer<Channel> {
|
||||
new HandshakeSessionHandler(connection, this.server));
|
||||
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()) {
|
||||
ch.pipeline().addFirst(new HAProxyMessageDecoder());
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ public enum TransportType {
|
||||
return NIO;
|
||||
}
|
||||
|
||||
if (IoUring.isAvailable() && !Boolean.getBoolean("velocity.disable-iouring-transport")) {
|
||||
if (IoUring.isAvailable() && Boolean.getBoolean("velocity.enable-iouring-transport")) {
|
||||
return IO_URING;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.network.limiter;
|
||||
|
||||
/**
|
||||
* PacketLimiter enforces a limit on the number of bytes processed over a time window.
|
||||
* Implementations should be thread-safe.
|
||||
*/
|
||||
public interface PacketLimiter {
|
||||
/**
|
||||
* Attempts to record the specified number of bytes within the current window.
|
||||
*
|
||||
* @param bytes the number of bytes to record
|
||||
* @return true if the bytes are allowed and recorded; false if the limit would be exceeded
|
||||
*/
|
||||
boolean account(int bytes);
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.network.limiter;
|
||||
|
||||
import com.velocitypowered.proxy.util.IntervalledCounter;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* A moving-window limiter over a configurable number of seconds.
|
||||
* It enforces both packets-per-second and average bytes-per-second limits.
|
||||
* The effective cap over the full window equals limitPerSecond * windowSeconds.
|
||||
*/
|
||||
public final class SimpleBytesPerSecondLimiter implements PacketLimiter {
|
||||
@Nullable
|
||||
private final IntervalledCounter bytesCounter;
|
||||
@Nullable
|
||||
private final IntervalledCounter packetsCounter;
|
||||
private final int packetsPerSecond;
|
||||
private final int bytesPerSecond;
|
||||
|
||||
/**
|
||||
* Creates a new SimpleBytesPerSecondLimiter.
|
||||
*
|
||||
* @param packetsPerSecond maximum average packets per second allowed (> 0)
|
||||
* @param bytesPerSecond maximum average bytes per second allowed (> 0)
|
||||
* @param windowSeconds number of seconds in the moving window (> 0)
|
||||
*/
|
||||
public SimpleBytesPerSecondLimiter(int packetsPerSecond, int bytesPerSecond, int windowSeconds) {
|
||||
this.packetsPerSecond = packetsPerSecond;
|
||||
if (windowSeconds <= 0) {
|
||||
throw new IllegalArgumentException("windowSeconds must be > 0");
|
||||
}
|
||||
this.bytesPerSecond = bytesPerSecond;
|
||||
this.packetsCounter = packetsPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
|
||||
this.bytesCounter = bytesPerSecond > 0 ? new IntervalledCounter((long) (windowSeconds * 1.0e9)) : null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the given payload length as one packet and returns whether it is allowed.
|
||||
*/
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
@Override
|
||||
public boolean account(int bytes) {
|
||||
long currTime = System.nanoTime();
|
||||
if (packetsCounter != null) {
|
||||
packetsCounter.updateAndAdd(1, currTime);
|
||||
if (packetsCounter.getRate() > packetsPerSecond) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (bytesCounter != null) {
|
||||
bytesCounter.updateAndAdd(bytes, currTime);
|
||||
if (bytesCounter.getRate() > bytesPerSecond) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -71,7 +71,7 @@ public final class SeparatePoolInetNameResolver extends InetNameResolver {
|
||||
protected void doResolve(String inetHost, Promise<InetAddress> promise) throws Exception {
|
||||
List<InetAddress> addresses = cache.getIfPresent(inetHost);
|
||||
if (addresses != null) {
|
||||
promise.trySuccess(addresses.get(0));
|
||||
promise.trySuccess(addresses.getFirst());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+18
-21
@@ -83,7 +83,7 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
|
||||
@Override
|
||||
public PluginDescription createPluginFromCandidate(PluginDescription candidate) throws Exception {
|
||||
if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate)) {
|
||||
if (!(candidate instanceof JavaVelocityPluginDescriptionCandidate candidateInst)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
|
||||
}
|
||||
|
||||
@@ -93,8 +93,6 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
PluginClassLoader loader = new PluginClassLoader(new URL[]{pluginJarUrl});
|
||||
loader.addToClassloaders();
|
||||
|
||||
JavaVelocityPluginDescriptionCandidate candidateInst =
|
||||
(JavaVelocityPluginDescriptionCandidate) candidate;
|
||||
Class<?> mainClass = loader.loadClass(candidateInst.getMainClass());
|
||||
return createDescription(candidateInst, mainClass);
|
||||
}
|
||||
@@ -102,11 +100,10 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
@Override
|
||||
public Module createModule(PluginContainer container) {
|
||||
PluginDescription description = container.getDescription();
|
||||
if (!(description instanceof JavaVelocityPluginDescription)) {
|
||||
if (!(description instanceof JavaVelocityPluginDescription javaDescription)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
|
||||
}
|
||||
|
||||
JavaVelocityPluginDescription javaDescription = (JavaVelocityPluginDescription) description;
|
||||
Optional<Path> source = javaDescription.getSource();
|
||||
|
||||
if (source.isEmpty()) {
|
||||
@@ -118,24 +115,23 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
|
||||
@Override
|
||||
public void createPlugin(PluginContainer container, Module... modules) {
|
||||
if (!(container instanceof VelocityPluginContainer)) {
|
||||
if (!(container instanceof VelocityPluginContainer pluginContainer)) {
|
||||
throw new IllegalArgumentException("Container provided isn't of the Java plugin loader");
|
||||
}
|
||||
PluginDescription description = container.getDescription();
|
||||
if (!(description instanceof JavaVelocityPluginDescription)) {
|
||||
PluginDescription description = pluginContainer.getDescription();
|
||||
if (!(description instanceof JavaVelocityPluginDescription javaPluginDescription)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
|
||||
}
|
||||
|
||||
Injector injector = Guice.createInjector(modules);
|
||||
Object instance = injector
|
||||
.getInstance(((JavaVelocityPluginDescription) description).getMainClass());
|
||||
Object instance = injector.getInstance(javaPluginDescription.getMainClass());
|
||||
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException(
|
||||
"Got nothing from injector for plugin " + description.getId());
|
||||
}
|
||||
|
||||
((VelocityPluginContainer) container).setInstance(instance);
|
||||
pluginContainer.setInstance(instance);
|
||||
}
|
||||
|
||||
private Optional<SerializedPluginDescription> getSerializedPluginInfo(Path source)
|
||||
@@ -145,22 +141,23 @@ public class JavaPluginLoader implements PluginLoader {
|
||||
new BufferedInputStream(Files.newInputStream(source)))) {
|
||||
JarEntry entry;
|
||||
while ((entry = in.getNextJarEntry()) != null) {
|
||||
if (entry.getName().equals("velocity-plugin.json")) {
|
||||
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
|
||||
SerializedPluginDescription.class));
|
||||
switch (entry.getName()) {
|
||||
case "velocity-plugin.json" -> {
|
||||
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
return Optional.of(VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
|
||||
SerializedPluginDescription.class));
|
||||
}
|
||||
}
|
||||
case "paper-plugin.yml", "plugin.yml", "bungee.yml" -> foundBungeeBukkitPluginFile = true;
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.getName().equals("plugin.yml") || entry.getName().equals("bungee.yml")) {
|
||||
foundBungeeBukkitPluginFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundBungeeBukkitPluginFile) {
|
||||
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
|
||||
+ "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord "
|
||||
+ "plugins.");
|
||||
+ "be a Paper, Bukkit or BungeeCord plugin. Velocity does not support plugins from these "
|
||||
+ "platforms.");
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
|
||||
@@ -32,13 +32,18 @@ public interface MinecraftPacket {
|
||||
|
||||
boolean handle(MinecraftSessionHandler handler);
|
||||
|
||||
default int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
default int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
ProtocolVersion version) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
default int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
default int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
ProtocolVersion version) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
default int encodeSizeHint(ProtocolUtils.Direction direction,
|
||||
ProtocolVersion version) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ import io.netty.handler.codec.EncoderException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.nbt.BinaryTag;
|
||||
@@ -44,6 +46,7 @@ import net.kyori.adventure.nbt.BinaryTagIO;
|
||||
import net.kyori.adventure.nbt.BinaryTagType;
|
||||
import net.kyori.adventure.nbt.BinaryTagTypes;
|
||||
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.json.JSONOptions;
|
||||
import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer;
|
||||
@@ -61,6 +64,8 @@ public enum ProtocolUtils {
|
||||
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
|
||||
.options(
|
||||
OptionSchema.globalSchema().stateBuilder()
|
||||
// general options
|
||||
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
|
||||
// before 1.16
|
||||
.value(JSONOptions.EMIT_RGB, Boolean.FALSE)
|
||||
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.VALUE_FIELD)
|
||||
@@ -69,6 +74,8 @@ public enum ProtocolUtils {
|
||||
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
|
||||
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
|
||||
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
|
||||
// before 1.21.5
|
||||
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
@@ -77,6 +84,8 @@ public enum ProtocolUtils {
|
||||
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
|
||||
.options(
|
||||
OptionSchema.globalSchema().stateBuilder()
|
||||
// general options
|
||||
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
|
||||
// after 1.16
|
||||
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
|
||||
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
|
||||
@@ -86,6 +95,8 @@ public enum ProtocolUtils {
|
||||
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.FALSE)
|
||||
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.FALSE)
|
||||
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.FALSE)
|
||||
// before 1.21.5
|
||||
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
@@ -94,6 +105,8 @@ public enum ProtocolUtils {
|
||||
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
|
||||
.options(
|
||||
OptionSchema.globalSchema().stateBuilder()
|
||||
// general options
|
||||
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
|
||||
// after 1.16
|
||||
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
|
||||
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.CAMEL_CASE)
|
||||
@@ -103,6 +116,8 @@ public enum ProtocolUtils {
|
||||
.value(JSONOptions.EMIT_COMPACT_TEXT_COMPONENT, Boolean.TRUE)
|
||||
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_ID_AS_INT_ARRAY, Boolean.TRUE)
|
||||
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
|
||||
// before 1.21.5
|
||||
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.TRUE)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
@@ -111,6 +126,8 @@ public enum ProtocolUtils {
|
||||
.legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get())
|
||||
.options(
|
||||
OptionSchema.globalSchema().stateBuilder()
|
||||
// general options
|
||||
.value(JSONOptions.EMIT_CLICK_URL_HTTPS, Boolean.TRUE)
|
||||
// after 1.16
|
||||
.value(JSONOptions.EMIT_RGB, Boolean.TRUE)
|
||||
.value(JSONOptions.EMIT_HOVER_EVENT_TYPE, JSONOptions.HoverEventValueMode.SNAKE_CASE)
|
||||
@@ -121,6 +138,7 @@ public enum ProtocolUtils {
|
||||
// after 1.21.5
|
||||
.value(JSONOptions.EMIT_HOVER_SHOW_ENTITY_KEY_AS_TYPE_AND_UUID_AS_ID, Boolean.FALSE)
|
||||
.value(JSONOptions.VALIDATE_STRICT_EVENTS, Boolean.TRUE)
|
||||
.value(JSONOptions.EMIT_CHANGE_PAGE_CLICK_EVENT_PAGE_AS_STRING, Boolean.FALSE)
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
@@ -134,7 +152,7 @@ public enum ProtocolUtils {
|
||||
BinaryTagTypes.COMPOUND, BinaryTagTypes.INT_ARRAY, BinaryTagTypes.LONG_ARRAY};
|
||||
private static final QuietDecoderException BAD_VARINT_CACHED =
|
||||
new QuietDecoderException("Bad VarInt decoded");
|
||||
private static final int[] VAR_INT_LENGTHS = new int[65];
|
||||
private static final int[] VAR_INT_LENGTHS = new int[33];
|
||||
|
||||
static {
|
||||
for (int i = 0; i <= 32; ++i) {
|
||||
@@ -143,6 +161,9 @@ public enum ProtocolUtils {
|
||||
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() {
|
||||
return MinecraftDecoder.DEBUG ? new CorruptedFrameException("Bad VarInt decoded")
|
||||
: BAD_VARINT_CACHED;
|
||||
@@ -234,16 +255,15 @@ public enum ProtocolUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the specified {@code value} as a 21-bit Minecraft VarInt to the specified {@code buf}.
|
||||
* Directly encodes a 21-bit Minecraft VarInt, ready to be written with {@link ByteBuf#writeMedium(int)}.
|
||||
* The upper 11 bits will be discarded.
|
||||
*
|
||||
* @param buf the buffer to read from
|
||||
* @param value the integer to write
|
||||
* @param value the value to encode
|
||||
* @return the encoded value
|
||||
*/
|
||||
public static void write21BitVarInt(ByteBuf buf, int value) {
|
||||
public static int encode21BitVarInt(int value) {
|
||||
// See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/
|
||||
int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||
buf.writeMedium(w);
|
||||
return (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14);
|
||||
}
|
||||
|
||||
public static String readString(ByteBuf buf) {
|
||||
@@ -272,12 +292,22 @@ public enum ProtocolUtils {
|
||||
checkFrame(buf.isReadable(length),
|
||||
"Trying to read a string that is too long (wanted %s, only have %s)", length,
|
||||
buf.readableBytes());
|
||||
String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8);
|
||||
buf.skipBytes(length);
|
||||
String str = buf.readString(length, StandardCharsets.UTF_8);
|
||||
checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap);
|
||||
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.
|
||||
*
|
||||
@@ -310,6 +340,16 @@ public enum ProtocolUtils {
|
||||
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.
|
||||
*
|
||||
@@ -380,7 +420,10 @@ public enum ProtocolUtils {
|
||||
*/
|
||||
public static int[] readIntegerArray(ByteBuf buf) {
|
||||
int len = readVarInt(buf);
|
||||
checkArgument(len >= 0, "Got a negative-length integer array (%s)", len);
|
||||
checkFrame(len >= 0, "Got a negative-length integer array (%s)", len);
|
||||
checkFrame(buf.isReadable(len),
|
||||
"Trying to read an array that is too long (wanted %s, only have %s)", len,
|
||||
buf.readableBytes());
|
||||
int[] array = new int[len];
|
||||
for (int i = 0; i < len; i++) {
|
||||
array[i] = readVarInt(buf);
|
||||
@@ -500,6 +543,10 @@ public enum ProtocolUtils {
|
||||
*/
|
||||
public static String[] readStringArray(ByteBuf 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];
|
||||
for (int i = 0; i < length; i++) {
|
||||
ret[i] = readString(buf);
|
||||
@@ -611,6 +658,9 @@ public enum ProtocolUtils {
|
||||
|
||||
checkArgument(len <= FORGE_MAX_ARRAY_LENGTH,
|
||||
"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];
|
||||
buf.readBytes(ret);
|
||||
@@ -775,6 +825,63 @@ public enum ProtocolUtils {
|
||||
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.
|
||||
*/
|
||||
@@ -782,4 +889,4 @@ public enum ProtocolUtils {
|
||||
SERVERBOUND,
|
||||
CLIENTBOUND
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,12 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_4;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_5;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_6;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_9;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8;
|
||||
import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9;
|
||||
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.SUPPORTED_VERSIONS;
|
||||
import static com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
|
||||
@@ -58,7 +60,11 @@ import com.velocitypowered.proxy.protocol.packet.BossBarPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||
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.DialogClearPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.DialogShowPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.EncryptionRequestPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.EncryptionResponsePacket;
|
||||
@@ -81,6 +87,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerDataPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccessPacket;
|
||||
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.StatusPingPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.StatusRequestPacket;
|
||||
@@ -88,6 +95,7 @@ import com.velocitypowered.proxy.protocol.packet.StatusResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpdateTeamsPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfoPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
||||
@@ -101,6 +109,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.UnsignedPlayerComm
|
||||
import com.velocitypowered.proxy.protocol.packet.config.ActiveFeaturesPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.ClientboundCustomReportDetailsPacket;
|
||||
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.KnownPacksPacket;
|
||||
import com.velocitypowered.proxy.protocol.packet.config.RegistrySyncPacket;
|
||||
@@ -183,6 +193,12 @@ public enum StateRegistry {
|
||||
KnownPacksPacket.class,
|
||||
KnownPacksPacket::new,
|
||||
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(
|
||||
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
|
||||
@@ -237,6 +253,12 @@ public enum StateRegistry {
|
||||
map(0x0F, MINECRAFT_1_21, false));
|
||||
clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new,
|
||||
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 {
|
||||
@@ -258,7 +280,8 @@ public enum StateRegistry {
|
||||
map(0x0A, MINECRAFT_1_20_2, false),
|
||||
map(0x0B, MINECRAFT_1_20_5, 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(
|
||||
LegacyChatPacket.class,
|
||||
LegacyChatPacket::new,
|
||||
@@ -272,7 +295,8 @@ public enum StateRegistry {
|
||||
ChatAcknowledgementPacket::new,
|
||||
map(0x03, MINECRAFT_1_19_3, 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,
|
||||
map(0x03, MINECRAFT_1_19, false),
|
||||
map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false));
|
||||
@@ -283,18 +307,21 @@ public enum StateRegistry {
|
||||
map(0x04, MINECRAFT_1_19_3, false),
|
||||
map(0x05, MINECRAFT_1_20_5, 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,
|
||||
map(0x04, MINECRAFT_1_20_5, 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(
|
||||
SessionPlayerChatPacket.class,
|
||||
SessionPlayerChatPacket::new,
|
||||
map(0x05, MINECRAFT_1_19_3, false),
|
||||
map(0x06, MINECRAFT_1_20_5, false),
|
||||
map(0x07, MINECRAFT_1_21_2, false),
|
||||
map(0x08, MINECRAFT_1_21_6, false));
|
||||
map(0x08, MINECRAFT_1_21_6, false),
|
||||
map(0x09, MINECRAFT_26_1, false));
|
||||
serverbound.register(
|
||||
ClientSettingsPacket.class,
|
||||
ClientSettingsPacket::new,
|
||||
@@ -310,12 +337,14 @@ public enum StateRegistry {
|
||||
map(0x09, MINECRAFT_1_20_2, false),
|
||||
map(0x0A, MINECRAFT_1_20_5, 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(
|
||||
ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new,
|
||||
map(0x11, MINECRAFT_1_20_5, 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(
|
||||
PluginMessagePacket.class,
|
||||
PluginMessagePacket::new,
|
||||
@@ -334,7 +363,8 @@ public enum StateRegistry {
|
||||
map(0x10, MINECRAFT_1_20_3, false),
|
||||
map(0x12, MINECRAFT_1_20_5, 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(
|
||||
KeepAlivePacket.class,
|
||||
KeepAlivePacket::new,
|
||||
@@ -354,7 +384,8 @@ public enum StateRegistry {
|
||||
map(0x15, MINECRAFT_1_20_3, false),
|
||||
map(0x18, MINECRAFT_1_20_5, 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(
|
||||
ResourcePackResponsePacket.class,
|
||||
ResourcePackResponsePacket::new,
|
||||
@@ -372,13 +403,15 @@ public enum StateRegistry {
|
||||
map(0x2B, MINECRAFT_1_20_5, false),
|
||||
map(0x2D, MINECRAFT_1_21_2, 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(
|
||||
FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE,
|
||||
map(0x0B, MINECRAFT_1_20_2, false),
|
||||
map(0x0C, MINECRAFT_1_20_5, false),
|
||||
map(0x0E, MINECRAFT_1_21_2, false),
|
||||
map(0x0F, MINECRAFT_1_21_6, false));
|
||||
map(0x0F, MINECRAFT_1_21_6, false),
|
||||
map(0x10, MINECRAFT_26_1, false));
|
||||
|
||||
clientbound.register(
|
||||
BossBarPacket.class,
|
||||
@@ -430,6 +463,28 @@ public enum StateRegistry {
|
||||
ClientboundCookieRequestPacket.class, ClientboundCookieRequestPacket::new,
|
||||
map(0x16, MINECRAFT_1_20_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(
|
||||
PluginMessagePacket.class,
|
||||
PluginMessagePacket::new,
|
||||
@@ -465,7 +520,8 @@ public enum StateRegistry {
|
||||
map(0x1A, MINECRAFT_1_19_4, false),
|
||||
map(0x1B, MINECRAFT_1_20_2, 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(
|
||||
KeepAlivePacket.class,
|
||||
KeepAlivePacket::new,
|
||||
@@ -484,7 +540,9 @@ public enum StateRegistry {
|
||||
map(0x24, MINECRAFT_1_20_2, false),
|
||||
map(0x26, MINECRAFT_1_20_5, 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(
|
||||
JoinGamePacket.class,
|
||||
JoinGamePacket::new,
|
||||
@@ -503,7 +561,9 @@ public enum StateRegistry {
|
||||
map(0x29, MINECRAFT_1_20_2, false),
|
||||
map(0x2B, MINECRAFT_1_20_5, 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(
|
||||
RespawnPacket.class,
|
||||
RespawnPacket::new,
|
||||
@@ -525,14 +585,18 @@ public enum StateRegistry {
|
||||
map(0x45, MINECRAFT_1_20_3, true),
|
||||
map(0x47, MINECRAFT_1_20_5, 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(
|
||||
RemoveResourcePackPacket.class,
|
||||
RemoveResourcePackPacket::new,
|
||||
map(0x43, MINECRAFT_1_20_3, false),
|
||||
map(0x45, MINECRAFT_1_20_5, 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(
|
||||
ResourcePackRequestPacket.class,
|
||||
ResourcePackRequestPacket::new,
|
||||
@@ -554,7 +618,9 @@ public enum StateRegistry {
|
||||
map(0x44, MINECRAFT_1_20_3, false),
|
||||
map(0x46, MINECRAFT_1_20_5, 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(
|
||||
HeaderAndFooterPacket.class,
|
||||
HeaderAndFooterPacket::new,
|
||||
@@ -577,7 +643,9 @@ public enum StateRegistry {
|
||||
map(0x6A, MINECRAFT_1_20_3, true),
|
||||
map(0x6D, MINECRAFT_1_20_5, 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(
|
||||
LegacyTitlePacket.class,
|
||||
LegacyTitlePacket::new,
|
||||
@@ -599,7 +667,9 @@ public enum StateRegistry {
|
||||
map(0x61, MINECRAFT_1_20_3, true),
|
||||
map(0x63, MINECRAFT_1_20_5, 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(
|
||||
TitleTextPacket.class,
|
||||
TitleTextPacket::new,
|
||||
@@ -612,7 +682,9 @@ public enum StateRegistry {
|
||||
map(0x63, MINECRAFT_1_20_3, true),
|
||||
map(0x65, MINECRAFT_1_20_5, 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(
|
||||
TitleActionbarPacket.class,
|
||||
TitleActionbarPacket::new,
|
||||
@@ -625,7 +697,9 @@ public enum StateRegistry {
|
||||
map(0x4A, MINECRAFT_1_20_3, true),
|
||||
map(0x4C, MINECRAFT_1_20_5, 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(
|
||||
TitleTimesPacket.class,
|
||||
TitleTimesPacket::new,
|
||||
@@ -638,7 +712,9 @@ public enum StateRegistry {
|
||||
map(0x64, MINECRAFT_1_20_3, true),
|
||||
map(0x66, MINECRAFT_1_20_5, 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(
|
||||
TitleClearPacket.class,
|
||||
TitleClearPacket::new,
|
||||
@@ -668,7 +744,9 @@ public enum StateRegistry {
|
||||
map(0x3B, MINECRAFT_1_20_2, false),
|
||||
map(0x3D, MINECRAFT_1_20_5, 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(
|
||||
UpsertPlayerInfoPacket.class,
|
||||
UpsertPlayerInfoPacket::new,
|
||||
@@ -677,12 +755,16 @@ public enum StateRegistry {
|
||||
map(0x3C, MINECRAFT_1_20_2, false),
|
||||
map(0x3E, MINECRAFT_1_20_5, 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(
|
||||
ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new,
|
||||
map(0x6B, MINECRAFT_1_20_5, 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(
|
||||
SystemChatPacket.class,
|
||||
SystemChatPacket::new,
|
||||
@@ -694,7 +776,9 @@ public enum StateRegistry {
|
||||
map(0x69, MINECRAFT_1_20_3, true),
|
||||
map(0x6C, MINECRAFT_1_20_5, 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(
|
||||
PlayerChatCompletionPacket.class,
|
||||
PlayerChatCompletionPacket::new,
|
||||
@@ -715,7 +799,9 @@ public enum StateRegistry {
|
||||
map(0x49, MINECRAFT_1_20_3, false),
|
||||
map(0x4B, MINECRAFT_1_20_5, 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(
|
||||
StartUpdatePacket.class,
|
||||
() -> StartUpdatePacket.INSTANCE,
|
||||
@@ -723,7 +809,9 @@ public enum StateRegistry {
|
||||
map(0x67, MINECRAFT_1_20_3, false),
|
||||
map(0x69, MINECRAFT_1_20_5, 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(
|
||||
BundleDelimiterPacket.class,
|
||||
() -> BundleDelimiterPacket.INSTANCE,
|
||||
@@ -732,17 +820,39 @@ public enum StateRegistry {
|
||||
TransferPacket.class,
|
||||
TransferPacket::new,
|
||||
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(
|
||||
ClientboundCustomReportDetailsPacket.class,
|
||||
ClientboundCustomReportDetailsPacket::new,
|
||||
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(
|
||||
ClientboundServerLinksPacket.class,
|
||||
ClientboundServerLinksPacket::new,
|
||||
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));
|
||||
clientbound.register(UpdateTeamsPacket.class, UpdateTeamsPacket::new,
|
||||
map(0x41, ProtocolVersion.MINECRAFT_1_9, true),
|
||||
map(0x43, ProtocolVersion.MINECRAFT_1_12, true),
|
||||
map(0x44, ProtocolVersion.MINECRAFT_1_12_1, true),
|
||||
map(0x47, ProtocolVersion.MINECRAFT_1_13, true),
|
||||
map(0x4B, ProtocolVersion.MINECRAFT_1_14, true),
|
||||
map(0x4C, ProtocolVersion.MINECRAFT_1_15, true),
|
||||
map(0x55, ProtocolVersion.MINECRAFT_1_17, true),
|
||||
map(0x58, ProtocolVersion.MINECRAFT_1_19_1, true),
|
||||
map(0x56, ProtocolVersion.MINECRAFT_1_19_3, true),
|
||||
map(0x5A, ProtocolVersion.MINECRAFT_1_19_4, true),
|
||||
map(0x5C, ProtocolVersion.MINECRAFT_1_20_2, true),
|
||||
map(0x5E, ProtocolVersion.MINECRAFT_1_20_3, true),
|
||||
map(0x60, ProtocolVersion.MINECRAFT_1_20_5, true),
|
||||
map(0x67, ProtocolVersion.MINECRAFT_1_21_2, true)
|
||||
map(0x87, MINECRAFT_1_21_9, false),
|
||||
map(0x89, MINECRAFT_26_1, false));
|
||||
}
|
||||
},
|
||||
LOGIN {
|
||||
@@ -771,7 +881,7 @@ public enum StateRegistry {
|
||||
map(0x01, MINECRAFT_1_7_2, false));
|
||||
clientbound.register(
|
||||
ServerLoginSuccessPacket.class, ServerLoginSuccessPacket::new,
|
||||
map(0x02, MINECRAFT_1_7_2, false));
|
||||
map(0x02, MINECRAFT_1_7_2, false));
|
||||
clientbound.register(
|
||||
SetCompressionPacket.class, SetCompressionPacket::new,
|
||||
map(0x03, MINECRAFT_1_8, false));
|
||||
|
||||
+5
-6
@@ -119,7 +119,7 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
|
||||
int sessionId = queryMessage.readInt();
|
||||
|
||||
switch (type) {
|
||||
case QUERY_TYPE_HANDSHAKE: {
|
||||
case QUERY_TYPE_HANDSHAKE -> {
|
||||
// Generate new challenge token and put it into the sessions cache
|
||||
int challengeToken = random.nextInt();
|
||||
sessions.put(senderAddress, challengeToken);
|
||||
@@ -132,10 +132,9 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
|
||||
|
||||
DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender());
|
||||
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
|
||||
int challengeToken = queryMessage.readInt();
|
||||
Integer session = sessions.getIfPresent(senderAddress);
|
||||
@@ -190,10 +189,10 @@ public class GameSpyQueryHandler extends SimpleChannelInboundHandler<DatagramPac
|
||||
"Exception while writing GS4 response for query from {}", senderAddress, ex);
|
||||
return null;
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Invalid query type - just don't respond
|
||||
default -> {
|
||||
// Invalid query type - just don't respond
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+32
-6
@@ -33,20 +33,34 @@ import java.util.List;
|
||||
*/
|
||||
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 HARD_MAXIMUM_UNCOMPRESSED_SIZE = 128 * 1024 * 1024; // 128MiB
|
||||
|
||||
private static final int UNCOMPRESSED_CAP =
|
||||
private static final int CLIENTBOUND_UNCOMPRESSED_CAP =
|
||||
Boolean.getBoolean("velocity.increased-compression-cap")
|
||||
? 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 double MAX_COMPRESSION_RATIO = Double.parseDouble(System.getProperty("velocity.max-compression-ratio", "64"));
|
||||
private final ProtocolUtils.Direction direction;
|
||||
|
||||
private int threshold;
|
||||
private final VelocityCompressor compressor;
|
||||
|
||||
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor) {
|
||||
/**
|
||||
* Creates a new {@code MinecraftCompressDecoder} with the specified compression {@code threshold}.
|
||||
*
|
||||
* @param threshold the threshold for compression. Packets with uncompressed size below this threshold will not be compressed.
|
||||
* @param compressor the compressor instance to use
|
||||
* @param direction the direction of the packets being decoded
|
||||
*/
|
||||
public MinecraftCompressDecoder(int threshold, VelocityCompressor compressor, ProtocolUtils.Direction direction) {
|
||||
this.threshold = threshold;
|
||||
this.compressor = compressor;
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,17 +76,29 @@ public class MinecraftCompressDecoder extends MessageToMessageDecoder<ByteBuf> {
|
||||
out.add(in.retain());
|
||||
return;
|
||||
}
|
||||
int length = in.readableBytes();
|
||||
|
||||
checkFrame(claimedUncompressedSize >= threshold, "Uncompressed size %s is less than"
|
||||
+ " threshold %s", claimedUncompressedSize, threshold);
|
||||
checkFrame(claimedUncompressedSize <= UNCOMPRESSED_CAP,
|
||||
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
|
||||
UNCOMPRESSED_CAP);
|
||||
|
||||
if (direction == ProtocolUtils.Direction.CLIENTBOUND) {
|
||||
checkFrame(claimedUncompressedSize <= CLIENTBOUND_UNCOMPRESSED_CAP,
|
||||
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
|
||||
CLIENTBOUND_UNCOMPRESSED_CAP);
|
||||
} else {
|
||||
checkFrame(claimedUncompressedSize <= SERVERBOUND_UNCOMPRESSED_CAP,
|
||||
"Uncompressed size %s exceeds hard threshold of %s", claimedUncompressedSize,
|
||||
SERVERBOUND_UNCOMPRESSED_CAP);
|
||||
double maxCompressedAllowed = length * MAX_COMPRESSION_RATIO;
|
||||
checkFrame(claimedUncompressedSize <= maxCompressedAllowed,
|
||||
"Uncompressed size %s exceeds ratio threshold of %s for compressed sized %s", claimedUncompressedSize,
|
||||
maxCompressedAllowed, length);
|
||||
}
|
||||
ByteBuf compatibleIn = ensureCompatible(ctx.alloc(), compressor, in);
|
||||
ByteBuf uncompressed = preferredBuffer(ctx.alloc(), compressor, claimedUncompressedSize);
|
||||
try {
|
||||
compressor.inflate(compatibleIn, uncompressed, claimedUncompressedSize);
|
||||
checkFrame(uncompressed.writerIndex() == claimedUncompressedSize,
|
||||
"Decompressed size %s does not match claimed uncompressed size %s", uncompressed.writerIndex(), claimedUncompressedSize);
|
||||
out.add(uncompressed);
|
||||
} catch (Exception e) {
|
||||
uncompressed.release();
|
||||
|
||||
+3
-6
@@ -46,7 +46,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
|
||||
if (uncompressed < threshold) {
|
||||
// Under the threshold, there is nothing to do.
|
||||
ProtocolUtils.writeVarInt(out, uncompressed + 1);
|
||||
ProtocolUtils.writeVarInt(out, 0);
|
||||
out.writeByte(0);
|
||||
out.writeBytes(msg);
|
||||
} else {
|
||||
handleCompressed(ctx, msg, out);
|
||||
@@ -57,7 +57,7 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
|
||||
throws DataFormatException {
|
||||
int uncompressed = msg.readableBytes();
|
||||
|
||||
ProtocolUtils.write21BitVarInt(out, 0); // Dummy packet length
|
||||
out.writeMedium(0); // Reserve the packet length
|
||||
ProtocolUtils.writeVarInt(out, uncompressed);
|
||||
ByteBuf compatibleIn = MoreByteBufUtils.ensureCompatible(ctx.alloc(), compressor, msg);
|
||||
|
||||
@@ -72,11 +72,8 @@ public class MinecraftCompressorAndLengthEncoder extends MessageToByteEncoder<By
|
||||
throw new DataFormatException("The server sent a very large (over 2MiB compressed) packet.");
|
||||
}
|
||||
|
||||
int writerIndex = out.writerIndex();
|
||||
int packetLength = out.readableBytes() - 3;
|
||||
out.writerIndex(0);
|
||||
ProtocolUtils.write21BitVarInt(out, packetLength); // Rewrite packet length
|
||||
out.writerIndex(writerIndex);
|
||||
out.setMedium(0, ProtocolUtils.encode21BitVarInt(packetLength)); // Rewrite packet length
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+31
-21
@@ -57,7 +57,11 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
if (msg instanceof ByteBuf buf) {
|
||||
tryDecode(ctx, buf);
|
||||
try {
|
||||
tryDecode(ctx, buf);
|
||||
} finally {
|
||||
buf.release();
|
||||
}
|
||||
} else {
|
||||
ctx.fireChannelRead(msg);
|
||||
}
|
||||
@@ -65,7 +69,6 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
||||
|
||||
private void tryDecode(ChannelHandlerContext ctx, ByteBuf buf) throws Exception {
|
||||
if (!ctx.channel().isActive() || !buf.isReadable()) {
|
||||
buf.release();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,30 +77,29 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
||||
MinecraftPacket packet = this.registry.createPacket(packetId);
|
||||
if (packet == null) {
|
||||
buf.readerIndex(originalReaderIndex);
|
||||
ctx.fireChannelRead(buf);
|
||||
} else {
|
||||
try {
|
||||
doLengthSanityChecks(buf, packet);
|
||||
|
||||
try {
|
||||
packet.decode(buf, direction, registry.version);
|
||||
} catch (Exception e) {
|
||||
throw handleDecodeFailure(e, packet, packetId);
|
||||
}
|
||||
|
||||
if (buf.isReadable()) {
|
||||
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
|
||||
}
|
||||
ctx.fireChannelRead(packet);
|
||||
} finally {
|
||||
buf.release();
|
||||
if (this.direction == ProtocolUtils.Direction.SERVERBOUND && this.state != StateRegistry.PLAY) {
|
||||
throw this.handleInvalidPacketId(packetId);
|
||||
}
|
||||
ctx.fireChannelRead(buf.retain());
|
||||
} else {
|
||||
doLengthSanityChecks(buf, packet);
|
||||
|
||||
try {
|
||||
packet.decode(buf, direction, registry.version);
|
||||
} catch (Exception e) {
|
||||
throw handleDecodeFailure(e, packet, packetId);
|
||||
}
|
||||
|
||||
if (buf.isReadable()) {
|
||||
throw handleOverflow(packet, buf.readerIndex(), buf.writerIndex());
|
||||
}
|
||||
ctx.fireChannelRead(packet);
|
||||
}
|
||||
}
|
||||
|
||||
private void doLengthSanityChecks(ByteBuf buf, MinecraftPacket packet) throws Exception {
|
||||
int expectedMinLen = packet.expectedMinLength(buf, direction, registry.version);
|
||||
int expectedMaxLen = packet.expectedMaxLength(buf, direction, registry.version);
|
||||
int expectedMinLen = packet.decodeExpectedMinLength(buf, direction, registry.version);
|
||||
int expectedMaxLen = packet.decodeExpectedMaxLength(buf, direction, registry.version);
|
||||
if (expectedMaxLen != -1 && buf.readableBytes() > expectedMaxLen) {
|
||||
throw handleOverflow(packet, expectedMaxLen, buf.readableBytes());
|
||||
}
|
||||
@@ -133,6 +135,14 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
private Exception handleInvalidPacketId(int packetId) {
|
||||
if (DEBUG) {
|
||||
return new CorruptedFrameException("Invalid packet " + getExtraConnectionDetail(packetId));
|
||||
} else {
|
||||
return DECODE_FAILED;
|
||||
}
|
||||
}
|
||||
|
||||
private String getExtraConnectionDetail(int packetId) {
|
||||
return "Direction " + direction + " Protocol " + registry.version + " State " + state
|
||||
+ " ID 0x" + Integer.toHexString(packetId);
|
||||
|
||||
@@ -54,6 +54,19 @@ public class MinecraftEncoder extends MessageToByteEncoder<MinecraftPacket> {
|
||||
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) {
|
||||
this.registry = state.getProtocolRegistry(direction, protocolVersion);
|
||||
}
|
||||
|
||||
+85
-51
@@ -20,6 +20,7 @@ package com.velocitypowered.proxy.protocol.netty;
|
||||
import static io.netty.util.ByteProcessor.FIND_NON_NUL;
|
||||
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.network.limiter.PacketLimiter;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
@@ -32,6 +33,7 @@ import io.netty.handler.codec.CorruptedFrameException;
|
||||
import java.util.List;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Frames Minecraft server packets which are prefixed by a 21-bit VarInt encoding.
|
||||
@@ -44,6 +46,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
+ "Velocity with -Dvelocity.packet-decode-logging=true to see more.");
|
||||
private static final 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 =
|
||||
new QuietDecoderException("VarInt too big");
|
||||
private static final QuietDecoderException UNKNOWN_PACKET =
|
||||
@@ -52,6 +56,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
private final ProtocolUtils.Direction direction;
|
||||
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
|
||||
private StateRegistry state;
|
||||
@Nullable
|
||||
private PacketLimiter packetLimiter;
|
||||
|
||||
/**
|
||||
* Creates a new {@code MinecraftVarintFrameDecoder} decoding packets from the specified {@code Direction}.
|
||||
@@ -74,69 +80,93 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
}
|
||||
|
||||
// skip any runs of 0x00 we might find
|
||||
int wlen = in.readableBytes();
|
||||
int packetStart = in.forEachByte(FIND_NON_NUL);
|
||||
if (packetStart == -1) {
|
||||
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;
|
||||
}
|
||||
in.readerIndex(packetStart);
|
||||
|
||||
// try to read the length of the packet
|
||||
in.markReaderIndex();
|
||||
int preIndex = in.readerIndex();
|
||||
int length = readRawVarInt21(in);
|
||||
if (preIndex == in.readerIndex()) {
|
||||
return;
|
||||
}
|
||||
if (length < 0) {
|
||||
throw BAD_PACKET_LENGTH;
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) {
|
||||
StateRegistry.PacketRegistry.ProtocolRegistry registry =
|
||||
state.getProtocolRegistry(direction, ProtocolVersion.MINIMUM_VERSION);
|
||||
|
||||
final int index = in.readerIndex();
|
||||
final int packetId = readRawVarInt21(in);
|
||||
// Index hasn't changed, we've read nothing
|
||||
if (index == in.readerIndex()) {
|
||||
in.resetReaderIndex();
|
||||
return;
|
||||
}
|
||||
final int payloadLength = length - ProtocolUtils.varIntBytes(packetId);
|
||||
|
||||
MinecraftPacket packet = registry.createPacket(packetId);
|
||||
|
||||
// We handle every packet in this phase, if you said something we don't know, something is really wrong
|
||||
if (packet == null) {
|
||||
throw UNKNOWN_PACKET;
|
||||
}
|
||||
|
||||
// We 'technically' have the incoming bytes of a payload here, and so, these can actually parse
|
||||
// the packet if needed, so, we'll take advantage of the existing methods
|
||||
int expectedMinLen = packet.expectedMinLength(in, direction, registry.version);
|
||||
int expectedMaxLen = packet.expectedMaxLength(in, direction, registry.version);
|
||||
if (expectedMaxLen != -1 && payloadLength > expectedMaxLen) {
|
||||
throw handleOverflow(packet, expectedMaxLen, in.readableBytes());
|
||||
}
|
||||
if (payloadLength < expectedMinLen) {
|
||||
throw handleUnderflow(packet, expectedMaxLen, in.readableBytes());
|
||||
}
|
||||
|
||||
|
||||
in.readerIndex(index);
|
||||
try {
|
||||
int length = readRawVarInt21(in);
|
||||
if (packetStart == in.readerIndex()) {
|
||||
return;
|
||||
}
|
||||
if (length < 0) {
|
||||
throw BAD_PACKET_LENGTH;
|
||||
}
|
||||
|
||||
if (length > 0) {
|
||||
if (state == StateRegistry.HANDSHAKE && direction == ProtocolUtils.Direction.SERVERBOUND) {
|
||||
if (validateServerboundHandshakePacket(in, length)) {
|
||||
in.readerIndex(packetStart);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// note that zero-length packets are ignored
|
||||
if (length > 0) {
|
||||
if (in.readableBytes() < length) {
|
||||
in.readerIndex(packetStart);
|
||||
} else {
|
||||
// If enabled, rate-limit serverbound payload bytes based on frame length
|
||||
if (packetLimiter != null) {
|
||||
if (!packetLimiter.account(length)) {
|
||||
throw new QuietDecoderException(
|
||||
"Rate limit exceeded while processing packets for %s".formatted(
|
||||
ctx.channel().remoteAddress()));
|
||||
}
|
||||
}
|
||||
out.add(in.readRetainedSlice(length));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Reset buffer to consistent state before propagating exception to prevent memory leaks
|
||||
in.readerIndex(packetStart);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// note that zero-length packets are ignored
|
||||
if (length > 0) {
|
||||
if (in.readableBytes() < length) {
|
||||
in.resetReaderIndex();
|
||||
} else {
|
||||
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.decodeExpectedMinLength(in, direction, registry.version);
|
||||
int expectedMaxLen = packet.decodeExpectedMaxLength(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);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -240,4 +270,8 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder {
|
||||
public void setState(StateRegistry stateRegistry) {
|
||||
this.state = stateRegistry;
|
||||
}
|
||||
|
||||
public void setPacketLimiter(@Nullable PacketLimiter packetLimiter) {
|
||||
this.packetLimiter = packetLimiter;
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -21,6 +21,9 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufHolder;
|
||||
import io.netty.channel.ChannelDuplexHandler;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.util.ReferenceCountUtil;
|
||||
@@ -41,8 +44,13 @@ import org.jetbrains.annotations.NotNull;
|
||||
*/
|
||||
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 Queue<Object> queue = new ArrayDeque<>();
|
||||
private int queueSize = 0;
|
||||
|
||||
/**
|
||||
* Provides registries for client & server bound packets.
|
||||
@@ -64,6 +72,20 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
|
||||
}
|
||||
}
|
||||
|
||||
int length = 0;
|
||||
if (msg instanceof ByteBuf) {
|
||||
// keep track of raw packets
|
||||
length = ((ByteBuf) msg).readableBytes();
|
||||
} else if (msg instanceof ByteBufHolder) {
|
||||
// keep track of bytebufs wrapped inside packets
|
||||
length = ((ByteBufHolder) msg).content().readableBytes();
|
||||
}
|
||||
if (this.queueSize + length > MAXIMUM_SIZE) {
|
||||
ReferenceCountUtil.release(msg);
|
||||
throw QUEUE_LIMIT_FAILED;
|
||||
}
|
||||
this.queueSize += length;
|
||||
|
||||
// Otherwise, queue the packet
|
||||
this.queue.offer(msg);
|
||||
}
|
||||
@@ -90,5 +112,6 @@ public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
|
||||
ReferenceCountUtil.release(msg);
|
||||
}
|
||||
}
|
||||
this.queueSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
+26
-27
@@ -25,7 +25,6 @@ import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
@@ -45,9 +44,9 @@ import io.netty.buffer.ByteBuf;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenCustomHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Arrays;
|
||||
import java.util.Deque;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Predicate;
|
||||
@@ -86,14 +85,14 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
@Override
|
||||
public void decode(ByteBuf buf, Direction direction, ProtocolVersion protocolVersion) {
|
||||
int commands = ProtocolUtils.readVarInt(buf);
|
||||
WireNode[] wireNodes = new WireNode[commands];
|
||||
List<WireNode> wireNodes = ProtocolUtils.newList(commands);
|
||||
for (int i = 0; i < commands; i++) {
|
||||
wireNodes[i] = deserializeNode(buf, i, protocolVersion);
|
||||
wireNodes.add(deserializeNode(buf, i, protocolVersion));
|
||||
}
|
||||
|
||||
// Iterate over the deserialized nodes and attempt to form a graph. We also resolve any cycles
|
||||
// that exist.
|
||||
Queue<WireNode> nodeQueue = new ArrayDeque<>(Arrays.asList(wireNodes));
|
||||
Queue<WireNode> nodeQueue = new ArrayDeque<>(wireNodes);
|
||||
while (!nodeQueue.isEmpty()) {
|
||||
boolean cycling = false;
|
||||
|
||||
@@ -112,7 +111,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
int rootIdx = ProtocolUtils.readVarInt(buf);
|
||||
rootNode = (RootCommandNode<CommandSource>) wireNodes[rootIdx].built;
|
||||
rootNode = (RootCommandNode<CommandSource>) wireNodes.get(rootIdx).built;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -246,17 +245,17 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
this.validated = false;
|
||||
}
|
||||
|
||||
void validate(WireNode[] wireNodes) {
|
||||
void validate(List<WireNode> wireNodes) {
|
||||
// Ensure all children exist. Note that we delay checking if the node has been built yet;
|
||||
// that needs to come after this node is built.
|
||||
for (int child : children) {
|
||||
if (child < 0 || child >= wireNodes.length) {
|
||||
if (child < 0 || child >= wireNodes.size()) {
|
||||
throw new IllegalStateException("Node points to non-existent index " + child);
|
||||
}
|
||||
}
|
||||
|
||||
if (redirectTo != -1) {
|
||||
if (redirectTo < 0 || redirectTo >= wireNodes.length) {
|
||||
if (redirectTo < 0 || redirectTo >= wireNodes.size()) {
|
||||
throw new IllegalStateException("Redirect node points to non-existent index "
|
||||
+ redirectTo);
|
||||
}
|
||||
@@ -265,7 +264,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
this.validated = true;
|
||||
}
|
||||
|
||||
boolean toNode(WireNode[] wireNodes) {
|
||||
boolean toNode(List<WireNode> wireNodes) {
|
||||
if (!this.validated) {
|
||||
this.validate(wireNodes);
|
||||
}
|
||||
@@ -281,7 +280,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
|
||||
// Add any redirects
|
||||
if (redirectTo != -1) {
|
||||
WireNode redirect = wireNodes[redirectTo];
|
||||
WireNode redirect = wireNodes.get(redirectTo);
|
||||
if (redirect.built != null) {
|
||||
args.redirect(redirect.built);
|
||||
} else {
|
||||
@@ -305,7 +304,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
for (int child : children) {
|
||||
if (wireNodes[child].built == null) {
|
||||
if (wireNodes.get(child).built == null) {
|
||||
// The child is not yet deserialized. The node can't be built now.
|
||||
return false;
|
||||
}
|
||||
@@ -313,7 +312,7 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
|
||||
// Associate children with nodes
|
||||
for (int child : children) {
|
||||
CommandNode<CommandSource> childNode = wireNodes[child].built;
|
||||
CommandNode<CommandSource> childNode = wireNodes.get(child).built;
|
||||
if (!(childNode instanceof RootCommandNode)) {
|
||||
built.addChild(childNode);
|
||||
}
|
||||
@@ -331,12 +330,10 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
.add("redirectTo", redirectTo);
|
||||
|
||||
if (args != null) {
|
||||
if (args instanceof LiteralArgumentBuilder) {
|
||||
helper.add("argsLabel",
|
||||
((LiteralArgumentBuilder<CommandSource>) args).getLiteral());
|
||||
} else if (args instanceof RequiredArgumentBuilder) {
|
||||
helper.add("argsName",
|
||||
((RequiredArgumentBuilder<CommandSource, ?>) args).getName());
|
||||
if (args instanceof LiteralArgumentBuilder literal) {
|
||||
helper.add("argsLabel", literal.getLiteral());
|
||||
} else if (args instanceof RequiredArgumentBuilder required) {
|
||||
helper.add("argsName", required.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,18 +345,20 @@ public class AvailableCommandsPacket implements MinecraftPacket {
|
||||
* A placeholder {@link SuggestionProvider} used internally to preserve the suggestion provider
|
||||
* name.
|
||||
*/
|
||||
public static class ProtocolSuggestionProvider implements SuggestionProvider<CommandSource> {
|
||||
|
||||
private final String name;
|
||||
|
||||
public ProtocolSuggestionProvider(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
public record ProtocolSuggestionProvider(String name) implements SuggestionProvider<CommandSource> {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Suggestions> getSuggestions(CommandContext<CommandSource> context,
|
||||
SuggestionsBuilder builder) throws CommandSyntaxException {
|
||||
SuggestionsBuilder builder) {
|
||||
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,30 +207,22 @@ public class BossBarPacket implements MinecraftPacket {
|
||||
this.uuid = ProtocolUtils.readUuid(buf);
|
||||
this.action = ProtocolUtils.readVarInt(buf);
|
||||
switch (action) {
|
||||
case ADD:
|
||||
case ADD -> {
|
||||
this.name = ComponentHolder.read(buf, version);
|
||||
this.percent = buf.readFloat();
|
||||
this.color = ProtocolUtils.readVarInt(buf);
|
||||
this.overlay = ProtocolUtils.readVarInt(buf);
|
||||
this.flags = buf.readUnsignedByte();
|
||||
break;
|
||||
case REMOVE:
|
||||
break;
|
||||
case UPDATE_PERCENT:
|
||||
this.percent = buf.readFloat();
|
||||
break;
|
||||
case UPDATE_NAME:
|
||||
this.name = ComponentHolder.read(buf, version);
|
||||
break;
|
||||
case UPDATE_STYLE:
|
||||
}
|
||||
case REMOVE -> {}
|
||||
case UPDATE_PERCENT -> this.percent = buf.readFloat();
|
||||
case UPDATE_NAME -> this.name = ComponentHolder.read(buf, version);
|
||||
case UPDATE_STYLE -> {
|
||||
this.color = ProtocolUtils.readVarInt(buf);
|
||||
this.overlay = ProtocolUtils.readVarInt(buf);
|
||||
break;
|
||||
case UPDATE_PROPERTIES:
|
||||
this.flags = buf.readUnsignedByte();
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Unknown action " + action);
|
||||
}
|
||||
case UPDATE_PROPERTIES -> this.flags = buf.readUnsignedByte();
|
||||
default -> throw new UnsupportedOperationException("Unknown action " + action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,36 +234,30 @@ public class BossBarPacket implements MinecraftPacket {
|
||||
ProtocolUtils.writeUuid(buf, uuid);
|
||||
ProtocolUtils.writeVarInt(buf, action);
|
||||
switch (action) {
|
||||
case ADD:
|
||||
if (name == null) {
|
||||
throw new IllegalStateException("No name specified!");
|
||||
}
|
||||
name.write(buf);
|
||||
buf.writeFloat(percent);
|
||||
case ADD -> {
|
||||
if (name == null) {
|
||||
throw new IllegalStateException("No name specified!");
|
||||
}
|
||||
name.write(buf);
|
||||
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, overlay);
|
||||
buf.writeByte(flags);
|
||||
break;
|
||||
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);
|
||||
}
|
||||
case UPDATE_PROPERTIES -> buf.writeByte(flags);
|
||||
default -> throw new UnsupportedOperationException("Unknown action " + action);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-3
@@ -22,8 +22,8 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ClientSettingsPacket implements MinecraftPacket {
|
||||
@@ -135,7 +135,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
|
||||
return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance +
|
||||
", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" +
|
||||
skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + textFilteringEnabled +
|
||||
", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}';
|
||||
", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,6 +206,16 @@ public class ClientSettingsPacket implements MinecraftPacket {
|
||||
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
|
||||
public boolean equals(@Nullable final Object o) {
|
||||
if (this == o) {
|
||||
@@ -237,7 +247,7 @@ public class ClientSettingsPacket implements MinecraftPacket {
|
||||
difficulty,
|
||||
skinParts,
|
||||
mainHand,
|
||||
textFilteringEnabled,
|
||||
textFilteringEnabled,
|
||||
clientListingAllowed,
|
||||
particleStatus);
|
||||
}
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -107,7 +107,7 @@ public class EncryptionResponsePacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
// 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.
|
||||
int base = 256 + 2 + 2;
|
||||
@@ -123,8 +123,8 @@ public class EncryptionResponsePacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
int base = expectedMaxLength(buf, direction, version);
|
||||
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
int base = decodeExpectedMaxLength(buf, direction, version);
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
|
||||
// These are "optional"
|
||||
base -= 128 + 8;
|
||||
|
||||
@@ -24,6 +24,7 @@ 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 io.netty.buffer.ByteBuf;
|
||||
|
||||
public class HandshakePacket implements MinecraftPacket {
|
||||
@@ -108,14 +109,21 @@ public class HandshakePacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
ProtocolVersion version) {
|
||||
return 7;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
ProtocolVersion version) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,28 @@ public class KeepAlivePacket implements MinecraftPacket {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_2)) {
|
||||
return Long.BYTES;
|
||||
} else if (version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
||||
return 5;
|
||||
} else {
|
||||
return Integer.BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMinLength(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) {
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_2)) {
|
||||
return Long.BYTES;
|
||||
} else if (version.noLessThan(ProtocolVersion.MINECRAFT_1_8)) {
|
||||
return 1;
|
||||
} else {
|
||||
return Integer.BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
|
||||
+28
-43
@@ -69,33 +69,25 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
|
||||
Item item = new Item(ProtocolUtils.readUuid(buf));
|
||||
items.add(item);
|
||||
switch (action) {
|
||||
case ADD_PLAYER:
|
||||
case ADD_PLAYER -> {
|
||||
item.setName(ProtocolUtils.readString(buf));
|
||||
item.setProperties(ProtocolUtils.readProperties(buf));
|
||||
item.setGameMode(ProtocolUtils.readVarInt(buf));
|
||||
item.setLatency(ProtocolUtils.readVarInt(buf));
|
||||
item.setDisplayName(readOptionalComponent(buf, version));
|
||||
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
|
||||
if (buf.readBoolean()) {
|
||||
item.setPlayerKey(ProtocolUtils.readPlayerKey(version, buf));
|
||||
}
|
||||
if (buf.readBoolean()) {
|
||||
item.setPlayerKey(ProtocolUtils.readPlayerKey(version, buf));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UPDATE_GAMEMODE:
|
||||
item.setGameMode(ProtocolUtils.readVarInt(buf));
|
||||
break;
|
||||
case UPDATE_LATENCY:
|
||||
item.setLatency(ProtocolUtils.readVarInt(buf));
|
||||
break;
|
||||
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);
|
||||
}
|
||||
case UPDATE_GAMEMODE -> item.setGameMode(ProtocolUtils.readVarInt(buf));
|
||||
case UPDATE_LATENCY -> item.setLatency(ProtocolUtils.readVarInt(buf));
|
||||
case UPDATE_DISPLAY_NAME -> item.setDisplayName(readOptionalComponent(buf, version));
|
||||
case REMOVE_PLAYER -> {
|
||||
//Do nothing, all that is needed is the uuid
|
||||
}
|
||||
default -> throw new UnsupportedOperationException("Unknown action " + action);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -126,39 +118,32 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
|
||||
|
||||
ProtocolUtils.writeUuid(buf, uuid);
|
||||
switch (action) {
|
||||
case ADD_PLAYER:
|
||||
case ADD_PLAYER -> {
|
||||
ProtocolUtils.writeString(buf, item.getName());
|
||||
ProtocolUtils.writeProperties(buf, item.getProperties());
|
||||
ProtocolUtils.writeVarInt(buf, item.getGameMode());
|
||||
ProtocolUtils.writeVarInt(buf, item.getLatency());
|
||||
writeDisplayName(buf, item.getDisplayName(), version);
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
|
||||
if (item.getPlayerKey() != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writePlayerKey(buf, item.getPlayerKey());
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
if (item.getPlayerKey() != null) {
|
||||
buf.writeBoolean(true);
|
||||
ProtocolUtils.writePlayerKey(buf, item.getPlayerKey());
|
||||
} else {
|
||||
buf.writeBoolean(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case UPDATE_GAMEMODE:
|
||||
ProtocolUtils.writeVarInt(buf, item.getGameMode());
|
||||
break;
|
||||
case UPDATE_LATENCY:
|
||||
ProtocolUtils.writeVarInt(buf, item.getLatency());
|
||||
break;
|
||||
case UPDATE_DISPLAY_NAME:
|
||||
writeDisplayName(buf, item.getDisplayName(), version);
|
||||
break;
|
||||
case REMOVE_PLAYER:
|
||||
}
|
||||
case UPDATE_GAMEMODE -> ProtocolUtils.writeVarInt(buf, item.getGameMode());
|
||||
case UPDATE_LATENCY -> ProtocolUtils.writeVarInt(buf, item.getLatency());
|
||||
case UPDATE_DISPLAY_NAME -> writeDisplayName(buf, item.getDisplayName(), version);
|
||||
case REMOVE_PLAYER -> {
|
||||
// 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 {
|
||||
Item item = items.get(0);
|
||||
Item item = items.getFirst();
|
||||
Component displayNameComponent = item.getDisplayName();
|
||||
if (displayNameComponent != null) {
|
||||
String displayName = LegacyComponentSerializer.legacySection()
|
||||
@@ -269,7 +254,7 @@ public class LegacyPlayerListItemPacket implements MinecraftPacket {
|
||||
return this;
|
||||
}
|
||||
|
||||
public IdentifiedKey getPlayerKey() {
|
||||
public @Nullable IdentifiedKey getPlayerKey() {
|
||||
return playerKey;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ public class LoginAcknowledgedPacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
ProtocolVersion version) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ 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.util.DeferredByteBufHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@@ -86,4 +87,9 @@ public class LoginPluginMessagePacket extends DeferredByteBufHolder implements M
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
|
||||
return content().readableBytes();
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -21,6 +21,7 @@ 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.util.DeferredByteBufHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
@@ -88,4 +89,9 @@ public class LoginPluginResponsePacket extends DeferredByteBufHolder implements
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
|
||||
return content().readableBytes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,16 @@ public class PingIdentifyPacket implements MinecraftPacket {
|
||||
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
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
|
||||
+33
@@ -23,6 +23,7 @@ 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.util.DeferredByteBufHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
@@ -30,6 +31,9 @@ import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class PluginMessagePacket extends DeferredByteBufHolder implements MinecraftPacket {
|
||||
|
||||
private static final int MAX_PAYLOAD_SIZE_CLIENTBOUND = getPayloadLimit(Direction.CLIENTBOUND);
|
||||
private static final int MAX_PAYLOAD_SIZE_SERVERBOUND = getPayloadLimit(Direction.SERVERBOUND);
|
||||
|
||||
private @Nullable String channel;
|
||||
|
||||
public PluginMessagePacket() {
|
||||
@@ -49,6 +53,19 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
|
||||
return channel;
|
||||
}
|
||||
|
||||
private static int getPayloadLimit(Direction direction) {
|
||||
if (System.getProperty("velocity.max-plugin-message-payload-size") != null) {
|
||||
return Integer.getInteger("velocity.max-plugin-message-payload-size");
|
||||
}
|
||||
if (direction == Direction.SERVERBOUND) {
|
||||
return Integer.getInteger("velocity.max-plugin-message-payload-size.serverbound", 32767);
|
||||
} else {
|
||||
// This is the vanilla expected limit, a payload this large feels like a nightmare given the trust
|
||||
// we give to servers...
|
||||
return Integer.getInteger("velocity.max-plugin-message-payload-size.clientbound", 1048576);
|
||||
}
|
||||
}
|
||||
|
||||
public void setChannel(String channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
@@ -99,6 +116,17 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES +
|
||||
(direction == Direction.CLIENTBOUND ? MAX_PAYLOAD_SIZE_CLIENTBOUND : MAX_PAYLOAD_SIZE_SERVERBOUND);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
return 1 + 0 + 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
@@ -143,4 +171,9 @@ public class PluginMessagePacket extends DeferredByteBufHolder implements Minecr
|
||||
public PluginMessagePacket touch(Object hint) {
|
||||
return (PluginMessagePacket) super.touch(hint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
|
||||
return content().readableBytes();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -17,7 +17,6 @@
|
||||
|
||||
package com.velocitypowered.proxy.protocol.packet;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.velocitypowered.api.network.ProtocolVersion;
|
||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||
@@ -51,7 +50,7 @@ public class RemovePlayerInfoPacket implements MinecraftPacket {
|
||||
public void decode(ByteBuf buf, ProtocolUtils.Direction direction,
|
||||
ProtocolVersion protocolVersion) {
|
||||
int length = ProtocolUtils.readVarInt(buf);
|
||||
Collection<UUID> profilesToRemove = Lists.newArrayListWithCapacity(length);
|
||||
Collection<UUID> profilesToRemove = ProtocolUtils.newList(length);
|
||||
for (int idx = 0; idx < length; idx++) {
|
||||
profilesToRemove.add(ProtocolUtils.readUuid(buf));
|
||||
}
|
||||
|
||||
+21
-1
@@ -80,6 +80,26 @@ public class ResourcePackResponsePacket implements MinecraftPacket {
|
||||
ProtocolUtils.writeVarInt(buf, status.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
|
||||
return Long.BYTES * 2 + 1;
|
||||
} else if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_9_4)) {
|
||||
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_3)) {
|
||||
return Long.BYTES * 2 + 1;
|
||||
} else if (version.noGreaterThan(ProtocolVersion.MINECRAFT_1_9_4)) {
|
||||
return 1 + 0 + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
@@ -93,4 +113,4 @@ public class ResourcePackResponsePacket implements MinecraftPacket {
|
||||
", status=" + status +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.velocitypowered.api.util.Favicon;
|
||||
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.packet.chat.ComponentHolder;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -121,4 +122,9 @@ public class ServerDataPacket implements MinecraftPacket {
|
||||
public void setSecureChatEnforced(boolean secureChatEnforced) {
|
||||
this.secureChatEnforced = secureChatEnforced;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int encodeSizeHint(Direction direction, ProtocolVersion version) {
|
||||
return 8 * 1024;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -150,7 +150,7 @@ public class ServerLoginPacket implements MinecraftPacket {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int expectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
// Accommodate the rare (but likely malicious) use of UTF-8 usernames, since it is technically
|
||||
// legal on the protocol level.
|
||||
int base = 1 + (16 * 3);
|
||||
|
||||
+8
@@ -23,6 +23,7 @@ import com.velocitypowered.api.util.UuidUtils;
|
||||
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.util.VelocityProperties;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import java.util.List;
|
||||
@@ -132,4 +133,11 @@ public class ServerLoginSuccessPacket implements MinecraftPacket {
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -65,6 +65,16 @@ public class ServerboundCookieResponsePacket implements MinecraftPacket {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMaxLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
return ProtocolUtils.DEFAULT_MAX_STRING_BYTES + 1 + 2 + 5120;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int decodeExpectedMinLength(ByteBuf buf, Direction direction, ProtocolVersion version) {
|
||||
return 1 + 0 + 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(MinecraftSessionHandler handler) {
|
||||
return handler.handle(this);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user