From 6f6d55e9e6f7c88d0781f027533221044c3aa613 Mon Sep 17 00:00:00 2001 From: powercas_gamer Date: Sat, 14 Sep 2024 13:02:08 +0200 Subject: [PATCH 01/21] [ci skip] make 'runShadow' workingDir use the 'run' directory (#1398) --- proxy/build.gradle.kts | 9 +++++++++ .../java/com/velocitypowered/proxy/VelocityServer.java | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 5e1387b06..035ff24de 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -92,6 +92,15 @@ tasks { dependsOn(configurateBuildTask) from(zipTree(configurateBuildTask.map { it.outputs.files.singleFile })) } + + runShadow { + workingDir = file("run").also(File::mkdirs) + standardInput = System.`in` + } + named("run") { + workingDir = file("run").also(File::mkdirs) + standardInput = System.`in` // Doesn't work? + } } dependencies { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 004243616..be0c8de9b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -100,6 +100,7 @@ import net.kyori.adventure.translation.GlobalTranslator; import net.kyori.adventure.translation.TranslationRegistry; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.bstats.MetricsBase; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; @@ -263,7 +264,13 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { this.cm.queryBind(configuration.getBind().getHostString(), configuration.getQueryPort()); } - Metrics.VelocityMetrics.startMetrics(this, configuration.getMetrics()); + final String defaultPackage = new String( + new byte[] { 'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's' }); + if (!MetricsBase.class.getPackage().getName().startsWith(defaultPackage)) { + Metrics.VelocityMetrics.startMetrics(this, configuration.getMetrics()); + } else { + logger.warn("debug environment, metrics is disabled!"); + } } private void registerTranslations() { From 2299b78ad36d682b7c5805505e39f2691ae33fdd Mon Sep 17 00:00:00 2001 From: skbeh <60107333+skbeh@users.noreply.github.com> Date: Sat, 14 Sep 2024 11:13:57 +0000 Subject: [PATCH 02/21] fix: apply message in `PlayerChatEvent` when handling `SessionPlayerChatPacket` (#1411) Fix 1.19.3+ unsigned chat not being changed by `PlayerChatEvent`. --- .../proxy/protocol/packet/chat/session/SessionChatHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java index 0731f64ed..74b5747f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/chat/session/SessionChatHandler.java @@ -71,7 +71,8 @@ public class SessionChatHandler implements ChatHandler invalidChange(logger, player); return null; } - return this.player.getChatBuilderFactory().builder().message(packet.message) + return this.player.getChatBuilderFactory().builder() + .message(chatResult.getMessage().orElse(packet.getMessage())) .setTimestamp(packet.timestamp) .setLastSeenMessages(newLastSeenMessages) .toServer(); From 78f6cfc26cbbb49c5b1479da3c9ea594564eb95f Mon Sep 17 00:00:00 2001 From: Timon Date: Sat, 14 Sep 2024 13:18:23 +0200 Subject: [PATCH 03/21] feat: Require any ConsoleCommandSender for the shutdown command (#1428) --- .../velocitypowered/proxy/command/builtin/ShutdownCommand.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java index cb9e44f81..402a0ce89 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java @@ -23,6 +23,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.velocitypowered.api.command.BrigadierCommand; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.proxy.VelocityServer; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; @@ -43,7 +44,7 @@ public final class ShutdownCommand { */ public static BrigadierCommand command(final VelocityServer server) { return new BrigadierCommand(LiteralArgumentBuilder.literal("shutdown") - .requires(source -> source == server.getConsoleCommandSource()) + .requires(source -> source instanceof ConsoleCommandSource) .executes(context -> { server.shutdown(true); return Command.SINGLE_SUCCESS; From 4eb02c8d383dbbb566ae020b6b4f55a529075a92 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 13:45:34 -0400 Subject: [PATCH 04/21] Do not store the dimension registry for each connected server This was unused and did little more than add unnecessary memory usage. The only time we need the dimension registry is during server switching - once that is done, we no longer need the registry. --- .../connection/backend/VelocityServerConnection.java | 11 ----------- .../connection/client/ClientPlaySessionHandler.java | 2 -- 2 files changed, 13 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index edf3a9148..af3f81796 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -53,8 +53,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; -import net.kyori.adventure.nbt.CompoundBinaryTag; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.jetbrains.annotations.NotNull; @@ -72,7 +70,6 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private boolean gracefulDisconnect = false; private BackendConnectionPhase connectionPhase = BackendConnectionPhases.UNKNOWN; private final Map pendingPings = new HashMap<>(); - private @MonotonicNonNull CompoundBinaryTag activeDimensionRegistry; /** * Initializes a new server connection. @@ -366,12 +363,4 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, public boolean hasCompletedJoin() { return hasCompletedJoin; } - - public CompoundBinaryTag getActiveDimensionRegistry() { - return activeDimensionRegistry; - } - - public void setActiveDimensionRegistry(CompoundBinaryTag activeDimensionRegistry) { - this.activeDimensionRegistry = activeDimensionRegistry; - } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index fed61693f..b41a24ccf 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -565,8 +565,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } - destination.setActiveDimensionRegistry(joinGame.getRegistry()); // 1.16 - // Remove previous boss bars. These don't get cleared when sending JoinGame, thus the need to // track them. for (UUID serverBossBar : serverBossBars) { From ffa78d2a92a06b44b06652bd03af0c4a62ea52a6 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 17:00:15 -0400 Subject: [PATCH 05/21] Small clean-ups in event manager. --- .../velocitypowered/proxy/event/EventTypeTracker.java | 5 +++-- .../proxy/event/VelocityEventManager.java | 11 ----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java b/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java index 808e79b92..f96de135c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/EventTypeTracker.java @@ -33,8 +33,9 @@ class EventTypeTracker { } public Collection> getFriendsOf(final Class eventType) { - if (friends.containsKey(eventType)) { - return friends.get(eventType); + ImmutableSet> existingFriends = friends.get(eventType); + if (existingFriends != null) { + return existingFriends; } final Collection> types = getEventTypes(eventType); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index ae0295653..f00c3877c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -156,17 +156,6 @@ public class VelocityEventManager implements EventManager { } } - enum AsyncType { - /** - * The complete event will be handled on an async thread. - */ - ALWAYS, - /** - * The event will never run async, everything is handled on the netty thread. - */ - NEVER - } - static final class HandlersCache { final HandlerRegistration[] handlers; From 4f227badc20dc30b0f6d84b5349c8809481dcbb1 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 14 May 2023 04:32:58 -0400 Subject: [PATCH 06/21] Reintroduce sync event execution to the Velocity event system This required a not-insubstantial number of bug fixes, since the sync support had bit-rotted somewhat. This PR also corrects a number of bugs. Finally. the per-plugin executor services are now used to execute all async event tasks. --- .../api/event/EventManager.java | 18 ++ .../velocitypowered/api/event/PostOrder.java | 2 +- .../velocitypowered/api/event/Subscribe.java | 34 ++-- .../velocitypowered/proxy/VelocityServer.java | 1 - .../proxy/command/VelocityCommandManager.java | 9 +- .../proxy/event/VelocityEventManager.java | 154 +++++++++++++----- .../proxy/command/CommandTestSuite.java | 11 -- .../proxy/event/EventTest.java | 146 ++++++++++++----- .../proxy/testutil/FakePluginManager.java | 29 ++-- 9 files changed, 292 insertions(+), 112 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/event/EventManager.java b/api/src/main/java/com/velocitypowered/api/event/EventManager.java index 63cae6f32..b8702ab35 100644 --- a/api/src/main/java/com/velocitypowered/api/event/EventManager.java +++ b/api/src/main/java/com/velocitypowered/api/event/EventManager.java @@ -45,10 +45,28 @@ public interface EventManager { * @param postOrder the order in which events should be posted to the handler * @param handler the handler to register * @param the event type to handle + * @deprecated use {@link #register(Object, Class, short, EventHandler)} instead */ + @Deprecated void register(Object plugin, Class eventClass, PostOrder postOrder, EventHandler handler); + /** + * Requests that the specified {@code handler} listen for events and associate it with the {@code + * plugin}. + * + *

Note that this method will register a non-asynchronous listener by default. If you want to + * use an asynchronous event handler, return {@link EventTask#async(Runnable)} from the handler.

+ * + * @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 handler the handler to register + * @param the event type to handle + */ + void register(Object plugin, Class eventClass, short postOrder, + EventHandler handler); + /** * Fires the specified event to the event bus asynchronously. This allows Velocity to continue * servicing connections while a plugin handles a potentially long-running operation such as a diff --git a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java index dde8a4379..0d52ed2c0 100644 --- a/api/src/main/java/com/velocitypowered/api/event/PostOrder.java +++ b/api/src/main/java/com/velocitypowered/api/event/PostOrder.java @@ -12,6 +12,6 @@ package com.velocitypowered.api.event; */ public enum PostOrder { - FIRST, EARLY, NORMAL, LATE, LAST + FIRST, EARLY, NORMAL, LATE, LAST, CUSTOM } diff --git a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java index bee71a3cb..abb96c949 100644 --- a/api/src/main/java/com/velocitypowered/api/event/Subscribe.java +++ b/api/src/main/java/com/velocitypowered/api/event/Subscribe.java @@ -22,24 +22,38 @@ public @interface Subscribe { /** * The order events will be posted to this listener. * + * @deprecated specify the order using {@link #priority()} instead * @return the order */ + @Deprecated PostOrder order() default PostOrder.NORMAL; /** - * Whether the handler must be called asynchronously. + * The priority of this event handler. Priorities are used to determine the order in which event + * handlers are called. The higher the priority, the earlier the event handler will be called. * - *

This option currently has no effect, but in the future it will. In Velocity 3.0.0, - * all event handlers run asynchronously by default. You are encouraged to determine whether or - * not to enable it now. This option is being provided as a migration aid.

+ *

Note that due to compatibility constraints, you must specify {@link PostOrder#CUSTOM} + * in order to use this field.

* - *

If this method returns {@code true}, the method is guaranteed to be executed - * asynchronously. Otherwise, the handler may be executed on the current thread or - * asynchronously. This still means you must consider thread-safety in your - * event listeners as the "current thread" can and will be different each time.

+ * @return the priority + */ + short priority() default Short.MIN_VALUE; + + /** + * Whether the handler must be called asynchronously. By default, all event handlers are called + * asynchronously. * - *

If any method handler targeting an event type is marked with {@code true}, then every - * handler targeting that event type will be executed asynchronously.

+ *

For performance (for instance, if you use {@link EventTask#withContinuation}), you can + * optionally specify false. This option will become {@code false} by default + * in a future release of Velocity.

+ * + *

If this is {@code true}, the method is guaranteed to be executed asynchronously. Otherwise, + * the handler may be executed on the current thread or asynchronously. This still means + * you must consider thread-safety in your event listeners as the "current thread" can + * and will be different each time.

+ * + *

Note that if any method handler targeting an event type is marked with {@code true}, then + * every handler targeting that event type will be executed asynchronously.

* * @return Requires async */ diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index be0c8de9b..2a362ccc9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -540,7 +540,6 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { eventManager.fire(new ProxyShutdownEvent()).join(); - timedOut = !eventManager.shutdown() || timedOut; timedOut = !scheduler.shutdown() || timedOut; if (timedOut) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index a4fab9321..24fc6a059 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -49,6 +49,8 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ForkJoinPool; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -71,6 +73,7 @@ public class VelocityCommandManager implements CommandManager { private final SuggestionsProvider suggestionsProvider; private final CommandGraphInjector injector; private final Map commandMetas; + private final ExecutorService asyncExecutor; /** * Constructs a command manager. @@ -89,6 +92,7 @@ public class VelocityCommandManager implements CommandManager { this.suggestionsProvider = new SuggestionsProvider<>(this.dispatcher, this.lock.readLock()); this.injector = new CommandGraphInjector<>(this.dispatcher, this.lock.readLock()); this.commandMetas = new ConcurrentHashMap<>(); + this.asyncExecutor = ForkJoinPool.commonPool(); // TODO: remove entirely } public void setAnnounceProxyCommands(boolean announceProxyCommands) { @@ -266,7 +270,7 @@ public class VelocityCommandManager implements CommandManager { return false; } return executeImmediately0(source, commandResult.getCommand().orElse(event.getCommand())); - }, eventManager.getAsyncExecutor()); + }, asyncExecutor); } @Override @@ -275,8 +279,7 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return CompletableFuture.supplyAsync( - () -> executeImmediately0(source, cmdLine), eventManager.getAsyncExecutor()); + return CompletableFuture.supplyAsync(() -> executeImmediately0(source, cmdLine), asyncExecutor); } /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java index f00c3877c..4c48068cb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/event/VelocityEventManager.java @@ -25,7 +25,6 @@ import com.google.common.base.VerifyException; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ListMultimap; import com.google.common.reflect.TypeToken; -import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.EventHandler; import com.velocitypowered.api.event.EventManager; @@ -38,6 +37,7 @@ import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.proxy.event.UntargetedEventHandler.EventTaskHandler; import com.velocitypowered.proxy.event.UntargetedEventHandler.VoidHandler; import com.velocitypowered.proxy.event.UntargetedEventHandler.WithContinuationHandler; +import com.velocitypowered.proxy.util.collect.Enum2IntMap; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; @@ -55,9 +55,6 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.function.BiConsumer; @@ -76,6 +73,14 @@ import org.lanternpowered.lmbda.LambdaType; */ public class VelocityEventManager implements EventManager { + private static final Enum2IntMap POST_ORDER_MAP = new Enum2IntMap.Builder<>(PostOrder.class) + .put(PostOrder.FIRST, Short.MAX_VALUE - 1) + .put(PostOrder.EARLY, Short.MAX_VALUE / 2) + .put(PostOrder.NORMAL, 0) + .put(PostOrder.LATE, Short.MIN_VALUE / 2) + .put(PostOrder.LAST, Short.MIN_VALUE + 1) + .put(PostOrder.CUSTOM, 0) + .build(); private static final Logger logger = LogManager.getLogger(VelocityEventManager.class); private static final MethodHandles.Lookup methodHandlesLookup = MethodHandles.lookup(); @@ -87,9 +92,8 @@ public class VelocityEventManager implements EventManager { LambdaType.of(WithContinuationHandler.class); private static final Comparator handlerComparator = - Comparator.comparingInt(o -> o.order); + Collections.reverseOrder(Comparator.comparingInt(o -> o.order)); - private final ExecutorService asyncExecutor; private final PluginManager pluginManager; private final ListMultimap, HandlerRegistration> handlersByType = @@ -112,9 +116,6 @@ public class VelocityEventManager implements EventManager { */ public VelocityEventManager(final PluginManager pluginManager) { this.pluginManager = pluginManager; - this.asyncExecutor = Executors - .newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactoryBuilder() - .setNameFormat("Velocity Async Event Executor - #%d").setDaemon(true).build()); } /** @@ -140,6 +141,7 @@ public class VelocityEventManager implements EventManager { final short order; final Class eventType; final EventHandler handler; + final AsyncType asyncType; /** * The instance of the {@link EventHandler} or the listener instance that was registered. @@ -147,20 +149,40 @@ public class VelocityEventManager implements EventManager { final Object instance; public HandlerRegistration(final PluginContainer plugin, final short order, - final Class eventType, final Object instance, final EventHandler handler) { + final Class eventType, final Object instance, final EventHandler handler, + final AsyncType asyncType) { this.plugin = plugin; this.order = order; this.eventType = eventType; this.instance = instance; this.handler = handler; + this.asyncType = asyncType; } } + enum AsyncType { + /** + * The event will never run async, everything is handled on the netty thread. + */ + NEVER, + /** + * The event will initially start on the thread calling the {@code fire} method, and possibly + * switch over to an async thread. + */ + SOMETIMES, + /** + * The complete event will be handled on an async thread. + */ + ALWAYS + } + static final class HandlersCache { + final AsyncType asyncType; final HandlerRegistration[] handlers; - HandlersCache(final HandlerRegistration[] handlers) { + HandlersCache(AsyncType asyncType, final HandlerRegistration[] handlers) { + this.asyncType = asyncType; this.handlers = handlers; } } @@ -183,7 +205,15 @@ public class VelocityEventManager implements EventManager { } baked.sort(handlerComparator); - return new HandlersCache(baked.toArray(new HandlerRegistration[0])); + + AsyncType asyncType = AsyncType.NEVER; + for (HandlerRegistration registration : baked) { + if (registration.asyncType.compareTo(asyncType) > 0) { + asyncType = registration.asyncType; + } + } + + return new HandlersCache(asyncType, baked.toArray(new HandlerRegistration[0])); } /** @@ -219,15 +249,17 @@ public class VelocityEventManager implements EventManager { static final class MethodHandlerInfo { final Method method; + final AsyncType asyncType; final @Nullable Class eventType; final short order; final @Nullable String errors; final @Nullable Class continuationType; - private MethodHandlerInfo(final Method method, final @Nullable Class eventType, - final short order, final @Nullable String errors, + private MethodHandlerInfo(final Method method, final AsyncType asyncType, + final @Nullable Class eventType, final short order, final @Nullable String errors, final @Nullable Class continuationType) { this.method = method; + this.asyncType = asyncType; this.eventType = eventType; this.order = order; this.errors = errors; @@ -291,17 +323,41 @@ public class VelocityEventManager implements EventManager { } } } + AsyncType asyncType = AsyncType.NEVER; + final Class returnType = method.getReturnType(); if (handlerAdapter == null) { - final Class returnType = method.getReturnType(); if (returnType != void.class && continuationType == Continuation.class) { errors.add("method return type must be void if a continuation parameter is provided"); } else if (returnType != void.class && returnType != EventTask.class) { - errors.add("method return type must be void or EventTask"); + errors.add("method return type must be void, AsyncTask, " + + "EventTask.Basic or EventTask.WithContinuation"); + } else if (returnType == EventTask.class) { + // technically, for compatibility, we *should* assume that the method must be invoked + // async, however, from examining some publicly-available plugins, developers did + // generally follow the contract and returned an EventTask only if they wanted this + // behavior. enable it for them. + asyncType = AsyncType.SOMETIMES; } + } else { + // for custom handlers, we always expect a return type of EventTask. this feature appears + // to have not been used in the wild AFAIK, so it gets the new behavior by default + asyncType = AsyncType.SOMETIMES; + } + + if (paramCount == 1 && returnType == void.class && subscribe.async()) { + // these are almost always a dead giveaway of a plugin that will need its handlers + // run async, so unless we're told otherwise, we'll assume that's the case + asyncType = AsyncType.ALWAYS; + } + + final short order; + if (subscribe.order() == PostOrder.CUSTOM) { + order = subscribe.priority(); + } else { + order = (short) POST_ORDER_MAP.get(subscribe.order()); } - final short order = (short) subscribe.order().ordinal(); final String errorsJoined = errors.isEmpty() ? null : String.join(",", errors); - collected.put(key, new MethodHandlerInfo(method, eventType, order, errorsJoined, + collected.put(key, new MethodHandlerInfo(method, asyncType, eventType, order, errorsJoined, continuationType)); } final Class superclass = targetClass.getSuperclass(); @@ -340,12 +396,29 @@ public class VelocityEventManager implements EventManager { @SuppressWarnings("unchecked") public void register(final Object plugin, final Class eventClass, final PostOrder order, final EventHandler handler) { + if (order == PostOrder.CUSTOM) { + throw new IllegalArgumentException( + "This method does not support custom post orders. Use the overload with short instead." + ); + } + register(plugin, eventClass, (short) POST_ORDER_MAP.get(order), handler, AsyncType.ALWAYS); + } + + @Override + public void register(Object plugin, Class eventClass, short postOrder, + EventHandler handler) { + register(plugin, eventClass, postOrder, handler, AsyncType.SOMETIMES); + } + + private void register(Object plugin, Class eventClass, short postOrder, + EventHandler handler, AsyncType asyncType) { final PluginContainer pluginContainer = pluginManager.ensurePluginContainer(plugin); requireNonNull(eventClass, "eventClass"); requireNonNull(handler, "handler"); final HandlerRegistration registration = new HandlerRegistration(pluginContainer, - (short) order.ordinal(), eventClass, handler, (EventHandler) handler); + postOrder, eventClass, handler, (EventHandler) handler, + AsyncType.ALWAYS); register(Collections.singletonList(registration)); } @@ -375,7 +448,7 @@ public class VelocityEventManager implements EventManager { final EventHandler handler = untargetedHandler.buildHandler(listener); registrations.add(new HandlerRegistration(pluginContainer, info.order, - info.eventType, listener, handler)); + info.eventType, listener, handler, info.asyncType)); } register(registrations); @@ -462,10 +535,13 @@ public class VelocityEventManager implements EventManager { private void fire(final @Nullable CompletableFuture future, final E event, final HandlersCache handlersCache) { - // In Velocity 1.1.0, all events were fired asynchronously. As Velocity 3.0.0 is intended to be - // largely (albeit not 100%) compatible with 1.1.x, we also fire events async. This behavior - // will go away in Velocity Polymer. - asyncExecutor.execute(() -> fire(future, event, 0, true, handlersCache.handlers)); + final HandlerRegistration registration = handlersCache.handlers[0]; + if (registration.asyncType == AsyncType.ALWAYS) { + registration.plugin.getExecutorService().execute( + () -> fire(future, event, 0, true, handlersCache.handlers)); + } else { + fire(future, event, 0, false, handlersCache.handlers); + } } private static final int TASK_STATE_DEFAULT = 0; @@ -494,6 +570,7 @@ public class VelocityEventManager implements EventManager { private final @Nullable CompletableFuture future; private final boolean currentlyAsync; private final E event; + private final Thread firedOnThread; // This field is modified via a VarHandle, so this field is used and cannot be final. @SuppressWarnings({"UnusedVariable", "FieldMayBeFinal", "FieldCanBeLocal"}) @@ -516,6 +593,7 @@ public class VelocityEventManager implements EventManager { this.event = event; this.index = index; this.currentlyAsync = currentlyAsync; + this.firedOnThread = Thread.currentThread(); } @Override @@ -526,8 +604,8 @@ public class VelocityEventManager implements EventManager { } /** - * Executes the task and returns whether the next one should be executed immediately after this - * one without scheduling. + * Executes the task and returns whether the next handler should be executed immediately + * after this one, without additional scheduling. */ boolean execute() { state = TASK_STATE_EXECUTING; @@ -569,7 +647,18 @@ public class VelocityEventManager implements EventManager { } if (!CONTINUATION_TASK_STATE.compareAndSet( this, TASK_STATE_EXECUTING, TASK_STATE_CONTINUE_IMMEDIATELY)) { - asyncExecutor.execute(() -> fire(future, event, index + 1, true, registrations)); + // We established earlier that registrations[index + 1] is a valid index. + // If we are remaining in the same thread for the next handler, fire + // the next event immediately, else fire it within the executor service + // of the plugin with the next handler. + final HandlerRegistration next = registrations[index + 1]; + final Thread currentThread = Thread.currentThread(); + if (currentThread == firedOnThread && next.asyncType != AsyncType.ALWAYS) { + fire(future, event, index + 1, currentlyAsync, registrations); + } else { + next.plugin.getExecutorService().execute(() -> + fire(future, event, index + 1, true, registrations)); + } } } @@ -595,7 +684,7 @@ public class VelocityEventManager implements EventManager { continue; } } else { - asyncExecutor.execute(continuationTask); + registration.plugin.getExecutorService().execute(continuationTask); } // fire will continue in another thread once the async task is // executed and the continuation is resumed @@ -615,13 +704,4 @@ public class VelocityEventManager implements EventManager { logger.error("Couldn't pass {} to {} {}", registration.eventType.getSimpleName(), pluginDescription.getId(), pluginDescription.getVersion().orElse(""), t); } - - public boolean shutdown() throws InterruptedException { - asyncExecutor.shutdown(); - return asyncExecutor.awaitTermination(10, TimeUnit.SECONDS); - } - - public ExecutorService getAsyncExecutor() { - return asyncExecutor; - } } \ No newline at end of file diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java index 9bd202fa4..c8e17d0f8 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java @@ -31,7 +31,6 @@ import com.velocitypowered.proxy.event.MockEventManager; import com.velocitypowered.proxy.event.VelocityEventManager; import java.util.Arrays; import java.util.Collection; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -47,16 +46,6 @@ abstract class CommandTestSuite { eventManager = new MockEventManager(); } - @AfterAll - static void afterAll() { - try { - eventManager.shutdown(); - eventManager = null; - } catch (final InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - @BeforeEach void setUp() { this.manager = new VelocityCommandManager(eventManager); diff --git a/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java b/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java index e05df200f..fc817f4db 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/event/EventTest.java @@ -41,20 +41,24 @@ import org.junit.jupiter.api.TestInstance; public class EventTest { public static final String CONTINUATION_TEST_THREAD_NAME = "Continuation test thread"; - private final VelocityEventManager eventManager = - new VelocityEventManager(new FakePluginManager()); + private final FakePluginManager pluginManager = new FakePluginManager(); + private final VelocityEventManager eventManager = new VelocityEventManager(pluginManager); @AfterAll void shutdown() throws Exception { - eventManager.shutdown(); + pluginManager.shutdown(); } static final class TestEvent { } + static void assertSyncThread(final Thread thread) { + assertEquals(Thread.currentThread(), thread); + } + static void assertAsyncThread(final Thread thread) { - assertTrue(thread.getName().contains("Velocity Async Event Executor")); + assertTrue(thread.getName().contains("Test Async Thread")); } static void assertContinuationThread(final Thread thread) { @@ -90,6 +94,7 @@ public class EventTest { eventManager.fire(new TestEvent()).get(); } finally { eventManager.unregisterListeners(FakePluginManager.PLUGIN_A); + eventManager.unregisterListeners(FakePluginManager.PLUGIN_B); } // Check that the order is A < B < C. @@ -119,6 +124,7 @@ public class EventTest { eventManager.fire(new TestEvent()).get(); } finally { eventManager.unregisterListeners(FakePluginManager.PLUGIN_A); + eventManager.unregisterListeners(FakePluginManager.PLUGIN_B); } // Check that the order is A < B < C. @@ -126,6 +132,26 @@ public class EventTest { assertTrue(listener2Invoked.get() < listener3Invoked.get(), "Listener C invoked before B!"); } + @Test + void testAlwaysSync() throws Exception { + final AlwaysSyncListener listener = new AlwaysSyncListener(); + handleMethodListener(listener); + assertSyncThread(listener.thread); + assertEquals(1, listener.result); + } + + static final class AlwaysSyncListener { + + @MonotonicNonNull Thread thread; + int result; + + @Subscribe(async = false) + void sync(TestEvent event) { + result++; + thread = Thread.currentThread(); + } + } + @Test void testAlwaysAsync() throws Exception { final AlwaysAsyncListener listener = new AlwaysAsyncListener(); @@ -143,7 +169,7 @@ public class EventTest { @MonotonicNonNull Thread threadC; int result; - @Subscribe + @Subscribe(async = true, order = PostOrder.EARLY) void firstAsync(TestEvent event) { result++; threadA = Thread.currentThread(); @@ -155,50 +181,93 @@ public class EventTest { return EventTask.async(() -> result++); } - @Subscribe + @Subscribe(order = PostOrder.LATE) void thirdAsync(TestEvent event) { result++; threadC = Thread.currentThread(); } } + @Test + void testSometimesAsync() throws Exception { + final SometimesAsyncListener listener = new SometimesAsyncListener(); + handleMethodListener(listener); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertAsyncThread(listener.threadC); + assertAsyncThread(listener.threadD); + assertEquals(3, listener.result); + } + + static final class SometimesAsyncListener { + + @MonotonicNonNull Thread threadA; + @MonotonicNonNull Thread threadB; + @MonotonicNonNull Thread threadC; + @MonotonicNonNull Thread threadD; + int result; + + @Subscribe(order = PostOrder.EARLY, async = false) + void notAsync(TestEvent event) { + result++; + threadA = Thread.currentThread(); + } + + @Subscribe + EventTask notAsyncUntilTask(TestEvent event) { + threadB = Thread.currentThread(); + return EventTask.async(() -> { + threadC = Thread.currentThread(); + result++; + }); + } + + @Subscribe(order = PostOrder.LATE, async = false) + void stillAsyncAfterTask(TestEvent event) { + threadD = Thread.currentThread(); + result++; + } + } + @Test void testContinuation() throws Exception { final ContinuationListener listener = new ContinuationListener(); handleMethodListener(listener); - assertAsyncThread(listener.thread1); - assertAsyncThread(listener.thread2); - assertContinuationThread(listener.thread2Custom); - assertAsyncThread(listener.thread3); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertAsyncThread(listener.threadC); assertEquals(2, listener.value.get()); } static final class ContinuationListener { - @MonotonicNonNull Thread thread1; - @MonotonicNonNull Thread thread2; - @MonotonicNonNull Thread thread2Custom; - @MonotonicNonNull Thread thread3; + @MonotonicNonNull Thread threadA; + @MonotonicNonNull Thread threadB; + @MonotonicNonNull Thread threadC; final AtomicInteger value = new AtomicInteger(); @Subscribe(order = PostOrder.EARLY) EventTask continuation(TestEvent event) { - thread1 = Thread.currentThread(); + threadA = Thread.currentThread(); return EventTask.withContinuation(continuation -> { value.incrementAndGet(); - thread2 = Thread.currentThread(); + threadB = Thread.currentThread(); new Thread(() -> { - thread2Custom = Thread.currentThread(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } value.incrementAndGet(); continuation.resume(); - }, CONTINUATION_TEST_THREAD_NAME).start(); + }).start(); }); } @Subscribe(order = PostOrder.LATE) void afterContinuation(TestEvent event) { - thread3 = Thread.currentThread(); + threadC = Thread.currentThread(); } } @@ -207,9 +276,9 @@ public class EventTest { final ResumeContinuationImmediatelyListener listener = new ResumeContinuationImmediatelyListener(); handleMethodListener(listener); - assertAsyncThread(listener.threadA); - assertAsyncThread(listener.threadB); - assertAsyncThread(listener.threadC); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertSyncThread(listener.threadC); assertEquals(2, listener.result); } @@ -241,42 +310,44 @@ public class EventTest { void testContinuationParameter() throws Exception { final ContinuationParameterListener listener = new ContinuationParameterListener(); handleMethodListener(listener); - assertAsyncThread(listener.thread1); - assertAsyncThread(listener.thread2); - assertContinuationThread(listener.thread2Custom); - assertAsyncThread(listener.thread3); + assertSyncThread(listener.threadA); + assertSyncThread(listener.threadB); + assertAsyncThread(listener.threadC); assertEquals(3, listener.result.get()); } static final class ContinuationParameterListener { - @MonotonicNonNull Thread thread1; - @MonotonicNonNull Thread thread2; - @MonotonicNonNull Thread thread2Custom; - @MonotonicNonNull Thread thread3; + @MonotonicNonNull Thread threadA; + @MonotonicNonNull Thread threadB; + @MonotonicNonNull Thread threadC; final AtomicInteger result = new AtomicInteger(); @Subscribe void resume(TestEvent event, Continuation continuation) { - thread1 = Thread.currentThread(); + threadA = Thread.currentThread(); result.incrementAndGet(); continuation.resume(); } @Subscribe(order = PostOrder.LATE) void resumeFromCustomThread(TestEvent event, Continuation continuation) { - thread2 = Thread.currentThread(); + threadB = Thread.currentThread(); new Thread(() -> { - thread2Custom = Thread.currentThread(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } result.incrementAndGet(); continuation.resume(); - }, CONTINUATION_TEST_THREAD_NAME).start(); + }).start(); } @Subscribe(order = PostOrder.LAST) void afterCustomThread(TestEvent event, Continuation continuation) { - thread3 = Thread.currentThread(); + threadC = Thread.currentThread(); result.incrementAndGet(); continuation.resume(); } @@ -328,8 +399,7 @@ public class EventTest { + "the second is the fancy continuation"); } }, - new TypeToken>() { - }, + new TypeToken>() {}, invokeFunction -> (instance, event) -> EventTask.withContinuation(continuation -> invokeFunction.accept(instance, event, new FancyContinuationImpl(continuation)) @@ -349,4 +419,4 @@ public class EventTest { continuation.resume(); } } -} \ No newline at end of file +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java index 04ba3867d..b1bc1af72 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java @@ -18,6 +18,7 @@ package com.velocitypowered.proxy.testutil; import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; @@ -25,7 +26,7 @@ import java.nio.file.Path; import java.util.Collection; import java.util.Optional; import java.util.concurrent.ExecutorService; -import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Executors; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -36,15 +37,19 @@ public class FakePluginManager implements PluginManager { public static final Object PLUGIN_A = new Object(); public static final Object PLUGIN_B = new Object(); - private static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A); - private static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B); + private final PluginContainer containerA = new FakePluginContainer("a", PLUGIN_A); + private final PluginContainer containerB = new FakePluginContainer("b", PLUGIN_B); + + private ExecutorService service = Executors.newCachedThreadPool( + new ThreadFactoryBuilder().setNameFormat("Test Async Thread").setDaemon(true).build() + ); @Override public @NonNull Optional fromInstance(@NonNull Object instance) { if (instance == PLUGIN_A) { - return Optional.of(PC_A); + return Optional.of(containerA); } else if (instance == PLUGIN_B) { - return Optional.of(PC_B); + return Optional.of(containerB); } else { return Optional.empty(); } @@ -54,9 +59,9 @@ public class FakePluginManager implements PluginManager { public @NonNull Optional getPlugin(@NonNull String id) { switch (id) { case "a": - return Optional.of(PC_A); + return Optional.of(containerA); case "b": - return Optional.of(PC_B); + return Optional.of(containerB); default: return Optional.empty(); } @@ -64,7 +69,7 @@ public class FakePluginManager implements PluginManager { @Override public @NonNull Collection getPlugins() { - return ImmutableList.of(PC_A, PC_B); + return ImmutableList.of(containerA, containerB); } @Override @@ -77,16 +82,18 @@ public class FakePluginManager implements PluginManager { throw new UnsupportedOperationException(); } - private static class FakePluginContainer implements PluginContainer { + public void shutdown() { + this.service.shutdownNow(); + } + + private class FakePluginContainer implements PluginContainer { private final String id; private final Object instance; - private final ExecutorService service; private FakePluginContainer(String id, Object instance) { this.id = id; this.instance = instance; - this.service = ForkJoinPool.commonPool(); } @Override From 2016d1482f185daf4b92b6775fb4a8d2ef1e0da3 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 20:22:55 -0400 Subject: [PATCH 07/21] Deprecate anonymous command registrations --- .../api/command/CommandManager.java | 4 + .../velocitypowered/proxy/VelocityServer.java | 55 ++++++++++-- .../proxy/command/VelocityCommandManager.java | 70 +++++++++++---- .../proxy/command/VelocityCommands.java | 90 ++++++++++++++++++- .../VelocityArgumentCommandNode.java | 10 +++ .../VelocityBrigadierCommandWrapper.java | 67 ++++++++++++++ .../proxy/command/builtin/GlistCommand.java | 9 +- .../proxy/command/builtin/SendCommand.java | 9 +- .../registrar/BrigadierCommandRegistrar.java | 6 +- .../registrar/InvocableCommandRegistrar.java | 5 +- .../proxy/plugin/VelocityPluginManager.java | 7 +- .../plugin/virtual/VelocityVirtualPlugin.java | 29 ++++++ .../proxy/command/BrigadierCommandTests.java | 2 +- .../proxy/command/CommandTestSuite.java | 3 +- .../proxy/testutil/FakePluginManager.java | 9 +- 15 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index 257fc9e64..ad3893738 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -45,7 +45,9 @@ public interface CommandManager { * @throws IllegalArgumentException if one of the given aliases is already registered, or * the given command does not implement a registrable {@link Command} subinterface * @see Command for a list of registrable Command subinterfaces + * @deprecated use {@link #register(CommandMeta, Command)} instead with a plugin specified */ + @Deprecated default void register(String alias, Command command, String... otherAliases) { register(metaBuilder(alias).aliases(otherAliases).build(), command); } @@ -55,7 +57,9 @@ public interface CommandManager { * * @param command the command to register * @throws IllegalArgumentException if the node alias is already registered + * @deprecated use {@link #register(CommandMeta, Command)} instead with a plugin specified */ + @Deprecated void register(BrigadierCommand command); /** diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index 2a362ccc9..2563f984f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -22,11 +22,13 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; 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.ProxyReloadEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; @@ -52,6 +54,9 @@ import com.velocitypowered.proxy.crypto.EncryptionUtils; import com.velocitypowered.proxy.event.VelocityEventManager; import com.velocitypowered.proxy.network.ConnectionManager; import com.velocitypowered.proxy.plugin.VelocityPluginManager; +import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer; +import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.protocol.util.GameProfileSerializer; @@ -77,6 +82,7 @@ import java.nio.file.Path; import java.security.KeyPair; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Locale; import java.util.Map; import java.util.Objects; @@ -111,6 +117,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public class VelocityServer implements ProxyServer, ForwardingAudience { + public static final String VELOCITY_URL = "https://velocitypowered.com"; + private static final Logger logger = LogManager.getLogger(VelocityServer.class); public static final Gson GENERAL_GSON = new GsonBuilder() .registerTypeHierarchyAdapter(Favicon.class, FaviconSerializer.INSTANCE) @@ -163,7 +171,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { VelocityServer(final ProxyOptions options) { pluginManager = new VelocityPluginManager(this); eventManager = new VelocityEventManager(pluginManager); - commandManager = new VelocityCommandManager(eventManager); + commandManager = new VelocityCommandManager(eventManager, pluginManager); scheduler = new VelocityScheduler(pluginManager); console = new VelocityConsole(this); cm = new ConnectionManager(this); @@ -200,6 +208,16 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { return new ProxyVersion(implName, implVendor, implVersion); } + private VelocityPluginContainer createVirtualPlugin() { + ProxyVersion version = getVersion(); + PluginDescription description = new VelocityPluginDescription( + "velocity", version.getName(), version.getVersion(), "The Velocity proxy", + VELOCITY_URL, ImmutableList.of(version.getVendor()), Collections.emptyList(), null); + VelocityPluginContainer container = new VelocityPluginContainer(description); + container.setInstance(VelocityVirtualPlugin.INSTANCE); + return container; + } + @Override public VelocityCommandManager getCommandManager() { return commandManager; @@ -214,6 +232,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { void start() { logger.info("Booting up {} {}...", getVersion().getName(), getVersion().getVersion()); console.setupStreams(); + pluginManager.registerPlugin(this.createVirtualPlugin()); registerTranslations(); @@ -222,11 +241,35 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { cm.logChannelInformation(); // Initialize commands first - commandManager.register(VelocityCommand.create(this)); - commandManager.register(CallbackCommand.create()); - commandManager.register(ServerCommand.create(this)); - commandManager.register("shutdown", ShutdownCommand.command(this), - "end", "stop"); + final BrigadierCommand velocityParentCommand = VelocityCommand.create(this); + commandManager.register( + commandManager.metaBuilder(velocityParentCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + velocityParentCommand + ); + final BrigadierCommand callbackCommand = CallbackCommand.create(); + commandManager.register( + commandManager.metaBuilder(callbackCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + velocityParentCommand + ); + final BrigadierCommand serverCommand = ServerCommand.create(this); + commandManager.register( + commandManager.metaBuilder(serverCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + serverCommand + ); + final BrigadierCommand shutdownCommand = ShutdownCommand.command(this); + commandManager.register( + commandManager.metaBuilder(shutdownCommand) + .plugin(VelocityVirtualPlugin.INSTANCE) + .aliases("end", "stop") + .build(), + shutdownCommand + ); new GlistCommand(this).register(); new SendCommand(this).register(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java index 24fc6a059..f8bb39f07 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -20,6 +20,7 @@ package com.velocitypowered.proxy.command; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; +import com.google.common.util.concurrent.MoreExecutors; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.Message; import com.mojang.brigadier.ParseResults; @@ -37,11 +38,15 @@ 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; +import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import com.velocitypowered.proxy.command.registrar.BrigadierCommandRegistrar; import com.velocitypowered.proxy.command.registrar.CommandRegistrar; import com.velocitypowered.proxy.command.registrar.RawCommandRegistrar; import com.velocitypowered.proxy.command.registrar.SimpleCommandRegistrar; import com.velocitypowered.proxy.event.VelocityEventManager; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; +import io.netty.util.concurrent.FastThreadLocalThread; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -49,8 +54,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.Executor; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -73,14 +77,16 @@ public class VelocityCommandManager implements CommandManager { private final SuggestionsProvider suggestionsProvider; private final CommandGraphInjector injector; private final Map commandMetas; - private final ExecutorService asyncExecutor; + private final PluginManager pluginManager; /** * Constructs a command manager. * * @param eventManager the event manager */ - public VelocityCommandManager(final VelocityEventManager eventManager) { + public VelocityCommandManager(final VelocityEventManager eventManager, + PluginManager pluginManager) { + this.pluginManager = pluginManager; this.lock = new ReentrantReadWriteLock(); this.dispatcher = new CommandDispatcher<>(); this.eventManager = Preconditions.checkNotNull(eventManager); @@ -92,7 +98,6 @@ public class VelocityCommandManager implements CommandManager { this.suggestionsProvider = new SuggestionsProvider<>(this.dispatcher, this.lock.readLock()); this.injector = new CommandGraphInjector<>(this.dispatcher, this.lock.readLock()); this.commandMetas = new ConcurrentHashMap<>(); - this.asyncExecutor = ForkJoinPool.commonPool(); // TODO: remove entirely } public void setAnnounceProxyCommands(boolean announceProxyCommands) { @@ -222,16 +227,13 @@ public class VelocityCommandManager implements CommandManager { return eventManager.fire(new CommandExecuteEvent(source, cmdLine)); } - private boolean executeImmediately0(final CommandSource source, final String cmdLine) { + private boolean executeImmediately0(final CommandSource source, final ParseResults parsed) { Preconditions.checkNotNull(source, "source"); - Preconditions.checkNotNull(cmdLine, "cmdLine"); - final String normalizedInput = VelocityCommands.normalizeInput(cmdLine, true); CommandResult result = CommandResult.EXCEPTION; try { // The parse can fail if the requirement predicates throw - final ParseResults parse = this.parse(normalizedInput, source); - boolean executed = dispatcher.execute(parse) != BrigadierCommand.FORWARD; + boolean executed = dispatcher.execute(parsed) != BrigadierCommand.FORWARD; result = executed ? CommandResult.EXECUTED : CommandResult.FORWARDED; return executed; } catch (final CommandSyntaxException e) { @@ -253,9 +255,9 @@ 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 " + cmdLine + " for " + source, e); + throw new RuntimeException("Unable to invoke command " + parsed.getReader().getString() + "for " + source, e); } finally { - eventManager.fireAndForget(new PostCommandInvocationEvent(source, cmdLine, result)); + eventManager.fireAndForget(new PostCommandInvocationEvent(source, parsed.getReader().getString(), result)); } } @@ -264,13 +266,17 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return callCommandEvent(source, cmdLine).thenApplyAsync(event -> { + return callCommandEvent(source, cmdLine).thenComposeAsync(event -> { CommandExecuteEvent.CommandResult commandResult = event.getResult(); if (commandResult.isForwardToServer() || !commandResult.isAllowed()) { - return false; + return CompletableFuture.completedFuture(false); } - return executeImmediately0(source, commandResult.getCommand().orElse(event.getCommand())); - }, asyncExecutor); + final ParseResults parsed = this.parse( + commandResult.getCommand().orElse(cmdLine), source); + return CompletableFuture.supplyAsync( + () -> executeImmediately0(source, parsed), this.getAsyncExecutor(parsed) + ); + }, figureAsyncExecutorForParsing()); } @Override @@ -279,7 +285,13 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(source, "source"); Preconditions.checkNotNull(cmdLine, "cmdLine"); - return CompletableFuture.supplyAsync(() -> executeImmediately0(source, cmdLine), asyncExecutor); + return CompletableFuture.supplyAsync( + () -> this.parse(cmdLine, source), figureAsyncExecutorForParsing() + ).thenCompose( + parsed -> CompletableFuture.supplyAsync( + () -> executeImmediately0(source, parsed), this.getAsyncExecutor(parsed) + ) + ); } /** @@ -327,9 +339,10 @@ public class VelocityCommandManager implements CommandManager { * @return the parse results */ private ParseResults parse(final String input, final CommandSource source) { + final String normalizedInput = VelocityCommands.normalizeInput(input, true); lock.readLock().lock(); try { - return dispatcher.parse(input, source); + return dispatcher.parse(normalizedInput, source); } finally { lock.readLock().unlock(); } @@ -373,4 +386,25 @@ public class VelocityCommandManager implements CommandManager { public CommandGraphInjector getInjector() { return injector; } + + private Executor getAsyncExecutor(ParseResults parse) { + Object registrant; + if (parse.getContext().getCommand() instanceof VelocityBrigadierCommandWrapper vbcw) { + registrant = vbcw.registrant() == null ? VelocityVirtualPlugin.INSTANCE : vbcw.registrant(); + } else { + registrant = VelocityVirtualPlugin.INSTANCE; + } + return pluginManager.ensurePluginContainer(registrant).getExecutorService(); + } + + private Executor figureAsyncExecutorForParsing() { + final Thread thread = Thread.currentThread(); + if (thread instanceof FastThreadLocalThread) { + // we *never* want to block the Netty event loop, so use the async executor + return pluginManager.ensurePluginContainer(VelocityVirtualPlugin.INSTANCE).getExecutorService(); + } else { + // it's some other thread that isn't a Netty event loop thread. direct execution it is! + return MoreExecutors.directExecutor(); + } + } } \ No newline at end of file diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java index 2f10ed25b..3c2ad1991 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommands.java @@ -24,6 +24,7 @@ import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.ParsedArgument; import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; @@ -32,6 +33,7 @@ import com.velocitypowered.api.command.CommandManager; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.InvocableCommand; import com.velocitypowered.proxy.command.brigadier.VelocityArgumentCommandNode; +import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import java.util.List; import java.util.Locale; import java.util.Map; @@ -44,6 +46,59 @@ import org.checkerframework.checker.nullness.qual.Nullable; */ public final class VelocityCommands { + // Wrapping + + /** + * Walks the command node tree and wraps all {@link Command} instances in a {@link VelocityBrigadierCommandWrapper}, + * to indicate the plugin that registered the command. This also has the side effect of cloning + * the command node tree. + * + * @param delegate the command node to wrap + * @param registrant the plugin that registered the command + * @return the wrapped command node + */ + public static CommandNode wrap(final CommandNode delegate, + final @Nullable Object registrant) { + Preconditions.checkNotNull(delegate, "delegate"); + if (registrant == null) { + // the registrant is null if the `plugin` was absent when we try to register the command + return delegate; + } + + com.mojang.brigadier.Command maybeCommand = delegate.getCommand(); + if (maybeCommand != null && !(maybeCommand instanceof VelocityBrigadierCommandWrapper)) { + maybeCommand = VelocityBrigadierCommandWrapper.wrap(delegate.getCommand(), registrant); + } + + if (delegate instanceof LiteralCommandNode lcn) { + var literalBuilder = shallowCopyAsBuilder(lcn, delegate.getName(), true); + literalBuilder.executes(maybeCommand); + // we also need to wrap any children + for (final CommandNode child : delegate.getChildren()) { + literalBuilder.then(wrap(child, registrant)); + } + if (delegate.getRedirect() != null) { + literalBuilder.redirect(wrap(delegate.getRedirect(), registrant)); + } + return literalBuilder.build(); + } else if (delegate instanceof VelocityArgumentCommandNode 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 child : delegate.getChildren()) { + argBuilder.then(wrap(child, registrant)); + } + if (delegate.getRedirect() != null) { + argBuilder.redirect(wrap(delegate.getRedirect(), registrant)); + } + return argBuilder.build(); + } else { + throw new IllegalArgumentException("Unsupported node type: " + delegate.getClass()); + } + } + // Normalization /** @@ -135,6 +190,33 @@ public final class VelocityCommands { */ public static LiteralCommandNode shallowCopy( final LiteralCommandNode original, final String newName) { + return shallowCopy(original, newName, original.getCommand()); + } + + /** + * Creates a copy of the given literal with the specified name. + * + * @param original the literal node to copy + * @param newName the name of the returned literal node + * @param newCommand the new command to set on the copied node + * @return a copy of the literal with the given name + */ + private static LiteralCommandNode shallowCopy( + final LiteralCommandNode original, final String newName, + final com.mojang.brigadier.Command newCommand) { + return shallowCopyAsBuilder(original, newName, false).executes(newCommand).build(); + } + + /** + * Creates a copy of the given literal with the specified name. + * + * @param original the literal node to copy + * @param newName the name of the returned literal node + * @return a copy of the literal with the given name + */ + private static LiteralArgumentBuilder shallowCopyAsBuilder( + final LiteralCommandNode original, final String newName, + final boolean skipChildren) { // Brigadier resolves the redirect of a node if further input can be parsed. // Let be a literal node having a redirect to a literal. Then, // the context returned by CommandDispatcher#parseNodes when given the input @@ -150,10 +232,12 @@ public final class VelocityCommands { .requiresWithContext(original.getContextRequirement()) .forward(original.getRedirect(), original.getRedirectModifier(), original.isFork()) .executes(original.getCommand()); - for (final CommandNode child : original.getChildren()) { - builder.then(child); + if (!skipChildren) { + for (final CommandNode child : original.getChildren()) { + builder.then(child); + } } - return builder.build(); + return builder; } // Arguments node diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java index 9faa29294..03ba6d35e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityArgumentCommandNode.java @@ -93,6 +93,16 @@ public class VelocityArgumentCommandNode extends ArgumentCommandNode withCommand(Command command) { + return new VelocityArgumentCommandNode<>(getName(), type, command, getRequirement(), + getContextRequirement(), getRedirect(), getRedirectModifier(), isFork(), getCustomSuggestions()); + } + + public VelocityArgumentCommandNode withRedirect(CommandNode target) { + return new VelocityArgumentCommandNode<>(getName(), type, getCommand(), getRequirement(), + getContextRequirement(), target, getRedirectModifier(), isFork(), getCustomSuggestions()); + } + @Override public boolean isValidInput(final String input) { return true; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java new file mode 100644 index 000000000..8502134cc --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/brigadier/VelocityBrigadierCommandWrapper.java @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +package com.velocitypowered.proxy.command.brigadier; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.velocitypowered.api.command.CommandSource; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Wraps a Brigadier command to allow us to track the registrant. + */ +public class VelocityBrigadierCommandWrapper implements Command { + + private final Command delegate; + private final Object registrant; + + private VelocityBrigadierCommandWrapper(Command delegate, Object registrant) { + this.delegate = delegate; + this.registrant = registrant; + } + + /** + * Transforms the given command into a {@code VelocityBrigadierCommandWrapper} if the registrant + * is not null and if the command is not already wrapped. + * + * @param delegate the command to wrap + * @param registrant the registrant of the command + * @return the wrapped command, if necessary + */ + public static Command wrap(Command delegate, @Nullable Object registrant) { + if (registrant == null) { + // nothing to wrap + return delegate; + } + if (delegate instanceof VelocityBrigadierCommandWrapper) { + // already wrapped + return delegate; + } + return new VelocityBrigadierCommandWrapper(delegate, registrant); + } + + @Override + public int run(CommandContext context) throws CommandSyntaxException { + return delegate.run(context); + } + + public Object registrant() { + return registrant; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java index 2cf8c92d2..2b73dcc13 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java @@ -31,6 +31,7 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import java.util.List; import java.util.Optional; import net.kyori.adventure.text.Component; @@ -80,7 +81,13 @@ public class GlistCommand { .executes(this::serverCount) .build(); rootNode.then(serverNode); - server.getCommandManager().register(new BrigadierCommand(rootNode)); + final BrigadierCommand command = new BrigadierCommand(rootNode); + server.getCommandManager().register( + server.getCommandManager().metaBuilder(command) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + command + ); } private int totalCount(final CommandContext context) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java index 626131daa..d0df03488 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/SendCommand.java @@ -30,6 +30,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import java.util.Objects; import java.util.Optional; import net.kyori.adventure.text.Component; @@ -96,7 +97,13 @@ public class SendCommand { .build(); playerNode.then(serverNode); rootNode.then(playerNode.build()); - server.getCommandManager().register(new BrigadierCommand(rootNode.build())); + final BrigadierCommand command = new BrigadierCommand(rootNode); + server.getCommandManager().register( + server.getCommandManager().metaBuilder(command) + .plugin(VelocityVirtualPlugin.INSTANCE) + .build(), + command + ); } private int usage(final CommandContext context) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java index bc8d02de4..74d3f30ee 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/BrigadierCommandRegistrar.java @@ -40,17 +40,19 @@ public final class BrigadierCommandRegistrar extends AbstractCommandRegistrar
literal = command.getNode(); + final LiteralCommandNode wrapped = + (LiteralCommandNode) VelocityCommands.wrap(literal, meta.getPlugin()); final String primaryAlias = literal.getName(); if (VelocityCommands.isValidAlias(primaryAlias)) { // Register directly without copying - this.register(literal); + this.register(wrapped); } for (final String alias : meta.getAliases()) { if (primaryAlias.equals(alias)) { continue; } - this.register(literal, alias); + this.register(wrapped, alias); } // Brigadier commands don't support hinting, ignore diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java index a526fe3de..380f45e39 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/registrar/InvocableCommandRegistrar.java @@ -32,6 +32,7 @@ import com.velocitypowered.api.command.InvocableCommand; import com.velocitypowered.proxy.command.VelocityCommandMeta; import com.velocitypowered.proxy.command.VelocityCommands; import com.velocitypowered.proxy.command.brigadier.VelocityArgumentBuilder; +import com.velocitypowered.proxy.command.brigadier.VelocityBrigadierCommandWrapper; import com.velocitypowered.proxy.command.invocation.CommandInvocationFactory; import java.util.Iterator; import java.util.concurrent.locks.Lock; @@ -76,11 +77,11 @@ abstract class InvocableCommandRegistrar, final I invocation = invocationFactory.create(context); return command.hasPermission(invocation); }; - final Command callback = context -> { + final Command callback = VelocityBrigadierCommandWrapper.wrap(context -> { final I invocation = invocationFactory.create(context); command.execute(invocation); return 1; // handled - }; + }, meta.getPlugin()); final LiteralCommandNode literal = LiteralArgumentBuilder .literal(alias) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java index 0af477c42..6bd0e0085 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -68,7 +68,12 @@ public class VelocityPluginManager implements PluginManager { this.server = checkNotNull(server, "server"); } - private void registerPlugin(PluginContainer plugin) { + /** + * Registers a plugin with the plugin manager. + * + * @param plugin the plugin to register + */ + public void registerPlugin(PluginContainer plugin) { pluginsById.put(plugin.getDescription().getId(), plugin); Optional instance = plugin.getInstance(); instance.ifPresent(o -> pluginInstances.put(o, plugin)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java new file mode 100644 index 000000000..08e4f57c3 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/virtual/VelocityVirtualPlugin.java @@ -0,0 +1,29 @@ +/* + * 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 . + */ + +package com.velocitypowered.proxy.plugin.virtual; + +/** + * A singleton plugin object that represents the Velocity proxy itself. + */ +public class VelocityVirtualPlugin { + @SuppressWarnings("InstantiationOfUtilityClass") + public static final VelocityVirtualPlugin INSTANCE = new VelocityVirtualPlugin(); + + private VelocityVirtualPlugin() { + } +} diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java b/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java index f4110ba52..abbfbc422 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/BrigadierCommandTests.java @@ -230,7 +230,7 @@ public class BrigadierCommandTests extends CommandTestSuite { final Exception wrapper = assertThrows(CompletionException.class, () -> manager.executeAsync(source, "hello").join()); - assertSame(expected, wrapper.getCause().getCause()); + assertSame(expected, wrapper.getCause()); } // Suggestions diff --git a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java index c8e17d0f8..66e49838a 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/command/CommandTestSuite.java @@ -29,6 +29,7 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.proxy.event.MockEventManager; import com.velocitypowered.proxy.event.VelocityEventManager; +import com.velocitypowered.proxy.testutil.FakePluginManager; import java.util.Arrays; import java.util.Collection; import org.junit.jupiter.api.BeforeAll; @@ -48,7 +49,7 @@ abstract class CommandTestSuite { @BeforeEach void setUp() { - this.manager = new VelocityCommandManager(eventManager); + this.manager = new VelocityCommandManager(eventManager, new FakePluginManager()); } final void assertHandled(final String input) { diff --git a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java index b1bc1af72..7992ac52e 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java @@ -22,6 +22,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; import java.nio.file.Path; import java.util.Collection; import java.util.Optional; @@ -39,6 +40,8 @@ public class FakePluginManager implements PluginManager { private final PluginContainer containerA = new FakePluginContainer("a", PLUGIN_A); private final PluginContainer containerB = new FakePluginContainer("b", PLUGIN_B); + private final PluginContainer containerVelocity = new FakePluginContainer("velocity", + VelocityVirtualPlugin.INSTANCE); private ExecutorService service = Executors.newCachedThreadPool( new ThreadFactoryBuilder().setNameFormat("Test Async Thread").setDaemon(true).build() @@ -50,6 +53,8 @@ public class FakePluginManager implements PluginManager { return Optional.of(containerA); } else if (instance == PLUGIN_B) { return Optional.of(containerB); + } else if (instance == VelocityVirtualPlugin.INSTANCE) { + return Optional.of(containerVelocity); } else { return Optional.empty(); } @@ -62,6 +67,8 @@ public class FakePluginManager implements PluginManager { return Optional.of(containerA); case "b": return Optional.of(containerB); + case "velocity": + return Optional.of(containerVelocity); default: return Optional.empty(); } @@ -69,7 +76,7 @@ public class FakePluginManager implements PluginManager { @Override public @NonNull Collection getPlugins() { - return ImmutableList.of(containerA, containerB); + return ImmutableList.of(containerVelocity, containerA, containerB); } @Override From b66aa3fb4e6d2350c85523685c5c2420c029ade8 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 15 Sep 2024 20:22:55 -0400 Subject: [PATCH 08/21] Bump to Velocity 3.4.0 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index cd9c67796..55c2ed10e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ group=com.velocitypowered -version=3.3.0-SNAPSHOT +version=3.4.0-SNAPSHOT From ef1f5009d333a302927e3266c04903052408e57a Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 23 Sep 2024 03:42:12 +0200 Subject: [PATCH 09/21] =?UTF-8?q?Adjust=20HAProxy's=20existance=20to=20log?= =?UTF-8?q?=20when=20the=20proxy=20protocol=20is=20enabled=20=E2=80=A6=20(?= =?UTF-8?q?#1436)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adjust HAProxy's existance to log when the proxy protocol is enabled during bind. * Added additional warning message, instead of changing the main one. We can see what the preference would be. --- .../velocitypowered/proxy/network/ConnectionManager.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index be1db5522..7af894f46 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -109,6 +109,12 @@ public final class ConnectionManager { final Channel channel = future.channel(); if (future.isSuccess()) { this.endpoints.put(address, new Endpoint(channel, ListenerType.MINECRAFT)); + + // Warn people with console access that HAProxy is in use, see PR: #1436 + if (this.server.getConfiguration().isProxyProtocol()) { + LOGGER.warn("Using HAProxy and listening on {}, please ensure this listener is adequately firewalled.", channel.localAddress()); + } + LOGGER.info("Listening on {}", channel.localAddress()); // Fire the proxy bound event after the socket is bound From 99aaf3ce4ef37297985a6b2ad6d7992289546637 Mon Sep 17 00:00:00 2001 From: Isaac - The456 Date: Mon, 7 Oct 2024 10:41:17 +0100 Subject: [PATCH 10/21] expose raw vhost (#1423) --- .../velocitypowered/api/proxy/InboundConnection.java | 11 ++++++++++- .../proxy/connection/client/AuthSessionHandler.java | 2 +- .../proxy/connection/client/ConnectedPlayer.java | 9 ++++++++- .../connection/client/HandshakeSessionHandler.java | 5 +++++ .../connection/client/InitialInboundConnection.java | 5 +++++ .../connection/client/LoginInboundConnection.java | 5 +++++ 6 files changed, 34 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java index 224abbd6e..1b16e9e14 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/InboundConnection.java @@ -26,11 +26,20 @@ public interface InboundConnection { /** * Returns the hostname that the user entered into the client, if applicable. - * + *
+ * This is partially processed, including removing a trailing dot, and discarding data after a null byte. + * @return the hostname from the client */ Optional getVirtualHost(); + /** + * Returns the raw hostname that the client sent, if applicable. + * + * @return the raw hostname from the client + */ + Optional getRawVirtualHost(); + /** * Determine whether or not the player remains online. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index ac02bcf76..6165a8467 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -96,7 +96,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { // Initiate a regular connection and move over to it. ConnectedPlayer player = new ConnectedPlayer(server, profileEvent.getGameProfile(), - mcConnection, inbound.getVirtualHost().orElse(null), onlineMode, + mcConnection, inbound.getVirtualHost().orElse(null), inbound.getRawVirtualHost().orElse(null), onlineMode, inbound.getIdentifiedKey()); this.connectedPlayer = player; if (!server.canRegisterConnection(player)) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 2b22fc7bc..46c3d63d2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -155,6 +155,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, */ private final MinecraftConnection connection; private final @Nullable InetSocketAddress virtualHost; + private final @Nullable String rawVirtualHost; private GameProfile profile; private PermissionFunction permissionFunction; private int tryIndex = 0; @@ -191,12 +192,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final ChatBuilderFactory chatBuilderFactory; ConnectedPlayer(VelocityServer server, GameProfile profile, MinecraftConnection connection, - @Nullable InetSocketAddress virtualHost, boolean onlineMode, + @Nullable InetSocketAddress virtualHost, @Nullable String rawVirtualHost, boolean onlineMode, @Nullable IdentifiedKey playerKey) { this.server = server; this.profile = profile; this.connection = connection; this.virtualHost = virtualHost; + this.rawVirtualHost = rawVirtualHost; this.permissionFunction = PermissionFunction.ALWAYS_UNDEFINED; this.connectionPhase = connection.getType().getInitialClientPhase(); this.onlineMode = onlineMode; @@ -356,6 +358,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return Optional.ofNullable(virtualHost); } + @Override + public Optional getRawVirtualHost() { + return Optional.ofNullable(rawVirtualHost); + } + void setPermissionFunction(PermissionFunction permissionFunction) { this.permissionFunction = permissionFunction; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java index fae6533db..f31e08126 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/HandshakeSessionHandler.java @@ -243,6 +243,11 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { return Optional.ofNullable(ping.getVhost()); } + @Override + public Optional getRawVirtualHost() { + return getVirtualHost().map(InetSocketAddress::getHostName); + } + @Override public boolean isActive() { return !connection.isClosed(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java index 2441e2ac5..7b9183745 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialInboundConnection.java @@ -63,6 +63,11 @@ public final class InitialInboundConnection implements VelocityInboundConnection return Optional.of(InetSocketAddress.createUnresolved(cleanedAddress, handshake.getPort())); } + @Override + public Optional getRawVirtualHost() { + return Optional.of(handshake.getServerAddress()); + } + @Override public boolean isActive() { return connection.getChannel().isActive(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java index 837376f95..18596e4e6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginInboundConnection.java @@ -71,6 +71,11 @@ public class LoginInboundConnection implements LoginPhaseConnection, KeyIdentifi return delegate.getVirtualHost(); } + @Override + public Optional getRawVirtualHost() { + return delegate.getRawVirtualHost(); + } + @Override public boolean isActive() { return delegate.isActive(); From 9a93bbe99013eda85fccf58ef59ee07a4fac7244 Mon Sep 17 00:00:00 2001 From: Elikill58 Date: Mon, 21 Oct 2024 02:05:29 +0200 Subject: [PATCH 11/21] Add deprecation notice (#1442) --- .../velocitypowered/api/event/player/PlayerChatEvent.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java index 786173972..dd1fae29b 100644 --- a/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/player/PlayerChatEvent.java @@ -51,6 +51,13 @@ public final class PlayerChatEvent implements ResultedEvent Date: Sun, 20 Oct 2024 17:06:01 -0700 Subject: [PATCH 12/21] set annotation processor to isolating to decrease compile times on big plugins (#1434) Signed-off-by: U5B <56985400+U5B@users.noreply.github.com> --- .../resources/META-INF/gradle/incremental.annotation.processors | 1 + 1 file changed, 1 insertion(+) create mode 100644 api/src/ap/resources/META-INF/gradle/incremental.annotation.processors diff --git a/api/src/ap/resources/META-INF/gradle/incremental.annotation.processors b/api/src/ap/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000..4e3ccc3f8 --- /dev/null +++ b/api/src/ap/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +com.velocitypowered.api.plugin.ap.PluginAnnotationProcessor,isolating \ No newline at end of file From d4e89dbdda9cde355c3e365c67d5428c541afb2f Mon Sep 17 00:00:00 2001 From: Jackson Date: Mon, 21 Oct 2024 11:09:55 +1100 Subject: [PATCH 13/21] Ability to specify servers from the command line (#1445) --- .../velocitypowered/proxy/ProxyOptions.java | 54 +++++++++++++++++++ .../velocitypowered/proxy/VelocityServer.java | 10 +++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java index 64a49917d..c660bf50e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java @@ -17,11 +17,18 @@ package com.velocitypowered.proxy; +import com.velocitypowered.api.proxy.server.ServerInfo; import java.io.IOException; +import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.List; + +import com.velocitypowered.proxy.util.AddressUtil; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; +import joptsimple.ValueConversionException; +import joptsimple.ValueConverter; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -35,6 +42,8 @@ public final class ProxyOptions { private final boolean help; private final @Nullable Integer port; private final @Nullable Boolean haproxy; + private final boolean ignoreConfigServers; + private final List servers; ProxyOptions(final String[] args) { final OptionParser parser = new OptionParser(); @@ -49,11 +58,20 @@ public final class ProxyOptions { "Choose whether to enable haproxy protocol. " + "The configuration haproxy protocol will be ignored.") .withRequiredArg().ofType(Boolean.class); + final OptionSpec servers = parser.accepts("add-server", + "Define a server mapping. " + + "You must ensure that server name is not also registered in the config or use --ignore-config-servers.") + .withRequiredArg().withValuesConvertedBy(new ServerInfoConverter()); + final OptionSpec ignoreConfigServers = parser.accepts("ignore-config-servers", + "Skip registering servers from the config file. " + + "Useful in dynamic setups or with the --add-server flag."); final OptionSet set = parser.parse(args); this.help = set.has(help); this.port = port.value(set); this.haproxy = haproxy.value(set); + this.servers = servers.values(set); + this.ignoreConfigServers = set.has(ignoreConfigServers); if (this.help) { try { @@ -75,4 +93,40 @@ public final class ProxyOptions { public @Nullable Boolean isHaproxy() { return this.haproxy; } + + public boolean isIgnoreConfigServers() { + return this.ignoreConfigServers; + } + + public List getServers() { + return this.servers; + } + + private static class ServerInfoConverter implements ValueConverter { + + @Override + public ServerInfo convert(String s) { + String[] split = s.split(":", 2); + if (split.length < 2) { + throw new ValueConversionException("Invalid server format. Use :
"); + } + InetSocketAddress address; + try { + address = AddressUtil.parseAddress(split[1]); + } catch (IllegalStateException e) { + throw new ValueConversionException("Invalid hostname for server flag with name: " + split[0]); + } + return new ServerInfo(split[0], address); + } + + @Override + public Class valueType() { + return ServerInfo.class; + } + + @Override + public String valuePattern() { + return "name>: entry : configuration.getServers().entrySet()) { - servers.register(new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()))); + for (ServerInfo cliServer : options.getServers()) { + servers.register(cliServer); + } + + if (!options.isIgnoreConfigServers()) { + for (Map.Entry entry : configuration.getServers().entrySet()) { + servers.register(new ServerInfo(entry.getKey(), AddressUtil.parseAddress(entry.getValue()))); + } } ipAttemptLimiter = Ratelimiters.createWithMilliseconds(configuration.getLoginRatelimit()); From 2c8ce219f0827823f95fd8c3dc68d1fc22a4bb31 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 20 Oct 2024 20:20:42 -0400 Subject: [PATCH 14/21] fix checkstyle, closes #1446 --- .../src/main/java/com/velocitypowered/proxy/ProxyOptions.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java index c660bf50e..d0b7f34f2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java @@ -18,12 +18,11 @@ package com.velocitypowered.proxy; import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.proxy.util.AddressUtil; import java.io.IOException; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.List; - -import com.velocitypowered.proxy.util.AddressUtil; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; From 05235da55d7a1dc59958ffa95163ea983275037e Mon Sep 17 00:00:00 2001 From: Gero Date: Tue, 22 Oct 2024 17:33:55 +0200 Subject: [PATCH 15/21] 1.21.2 (#1447) * 24w33a * 24w34a * 24w38a * 24w40a * 1.21.2 --- .../api/network/ProtocolVersion.java | 3 +- .../api/proxy/player/PlayerSettings.java | 23 ++++ .../client/ClientSettingsWrapper.java | 26 +++-- .../proxy/protocol/StateRegistry.java | 100 ++++++++++++------ .../protocol/packet/ClientSettingsPacket.java | 48 ++++++--- .../proxy/protocol/packet/JoinGamePacket.java | 19 ++++ .../proxy/protocol/packet/RespawnPacket.java | 24 ++++- .../packet/UpsertPlayerInfoPacket.java | 15 +++ 8 files changed, 203 insertions(+), 55 deletions(-) diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 8bb0c98b0..1c6b6d536 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -87,7 +87,8 @@ public enum ProtocolVersion implements Ordered { MINECRAFT_1_20_2(764, "1.20.2"), MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"), MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), - MINECRAFT_1_21(767, "1.21", "1.21.1"); + MINECRAFT_1_21(767, "1.21", "1.21.1"), + MINECRAFT_1_21_2(768, "1.21.2"); private static final int SNAPSHOT_BIT = 30; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/player/PlayerSettings.java b/api/src/main/java/com/velocitypowered/api/proxy/player/PlayerSettings.java index 320b1c203..416cbf154 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/player/PlayerSettings.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/player/PlayerSettings.java @@ -68,6 +68,20 @@ public interface PlayerSettings { */ boolean isClientListingAllowed(); + /** + * Returns if the client has text filtering enabled. + * + * @return if text filtering is enabled + */ + boolean isTextFilteringEnabled(); + + /** + * Returns the selected "Particles" option state. + * + * @return the particle option + */ + ParticleStatus getParticleStatus(); + /** * The client's current chat display mode. */ @@ -84,4 +98,13 @@ public interface PlayerSettings { LEFT, RIGHT } + + /** + * The client's current "Particles" option state. + */ + enum ParticleStatus { + ALL, + DECREASED, + MINIMAL + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java index 5032ff1f4..f8d3107cc 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientSettingsWrapper.java @@ -30,7 +30,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; public class ClientSettingsWrapper implements PlayerSettings { static final PlayerSettings DEFAULT = new ClientSettingsWrapper( - new ClientSettingsPacket("en_US", (byte) 10, 0, true, (short) 127, 1, true, false)); + new ClientSettingsPacket("en_us", (byte) 2, 0, true, (short) 0, 1, false, false, 0)); private final ClientSettingsPacket settings; private final SkinParts parts; @@ -56,11 +56,11 @@ public class ClientSettingsWrapper implements PlayerSettings { @Override public ChatMode getChatMode() { - int chat = settings.getChatVisibility(); - if (chat < 0 || chat > 2) { - return ChatMode.SHOWN; - } - return ChatMode.values()[chat]; + return switch (settings.getChatVisibility()) { + case 1 -> ChatMode.COMMANDS_ONLY; + case 2 -> ChatMode.HIDDEN; + default -> ChatMode.SHOWN; + }; } @Override @@ -83,6 +83,20 @@ public class ClientSettingsWrapper implements PlayerSettings { return settings.isClientListingAllowed(); } + @Override + public boolean isTextFilteringEnabled() { + return settings.isTextFilteringEnabled(); + } + + @Override + public ParticleStatus getParticleStatus() { + return switch (settings.getParticleStatus()) { + case 1 -> ParticleStatus.DECREASED; + case 2 -> ParticleStatus.MINIMAL; + default -> ParticleStatus.ALL; + }; + } + @Override public boolean equals(@Nullable final Object o) { if (this == o) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 41d444a36..1bd70248a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -37,6 +37,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_5; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_21_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -252,7 +253,8 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19_3, false), map(0x09, MINECRAFT_1_19_4, false), map(0x0A, MINECRAFT_1_20_2, false), - map(0x0B, MINECRAFT_1_20_5, false)); + map(0x0B, MINECRAFT_1_20_5, false), + map(0x0D, MINECRAFT_1_21_2, false)); serverbound.register( LegacyChatPacket.class, LegacyChatPacket::new, @@ -264,7 +266,8 @@ public enum StateRegistry { serverbound.register( ChatAcknowledgementPacket.class, ChatAcknowledgementPacket::new, - map(0x03, MINECRAFT_1_19_3, false)); + map(0x03, MINECRAFT_1_19_3, false), + map(0x04, MINECRAFT_1_21_2, false)); serverbound.register(KeyedPlayerCommandPacket.class, KeyedPlayerCommandPacket::new, map(0x03, MINECRAFT_1_19, false), map(0x04, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); @@ -273,14 +276,17 @@ public enum StateRegistry { map(0x05, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); serverbound.register(SessionPlayerCommandPacket.class, SessionPlayerCommandPacket::new, map(0x04, MINECRAFT_1_19_3, false), - map(0x05, MINECRAFT_1_20_5, false)); + map(0x05, MINECRAFT_1_20_5, false), + map(0x06, MINECRAFT_1_21_2, false)); serverbound.register(UnsignedPlayerCommandPacket.class, UnsignedPlayerCommandPacket::new, - map(0x04, MINECRAFT_1_20_5, false)); + map(0x04, MINECRAFT_1_20_5, false), + map(0x05, MINECRAFT_1_21_2, false)); serverbound.register( SessionPlayerChatPacket.class, SessionPlayerChatPacket::new, map(0x05, MINECRAFT_1_19_3, false), - map(0x06, MINECRAFT_1_20_5, false)); + map(0x06, MINECRAFT_1_20_5, false), + map(0x07, MINECRAFT_1_21_2, false)); serverbound.register( ClientSettingsPacket.class, ClientSettingsPacket::new, @@ -294,10 +300,12 @@ public enum StateRegistry { map(0x07, MINECRAFT_1_19_3, false), map(0x08, MINECRAFT_1_19_4, false), map(0x09, MINECRAFT_1_20_2, false), - map(0x0A, MINECRAFT_1_20_5, false)); + map(0x0A, MINECRAFT_1_20_5, false), + map(0x0C, MINECRAFT_1_21_2, false)); serverbound.register( ServerboundCookieResponsePacket.class, ServerboundCookieResponsePacket::new, - map(0x11, MINECRAFT_1_20_5, false)); + map(0x11, MINECRAFT_1_20_5, false), + map(0x13, MINECRAFT_1_21_2, false)); serverbound.register( PluginMessagePacket.class, PluginMessagePacket::new, @@ -314,7 +322,8 @@ public enum StateRegistry { map(0x0D, MINECRAFT_1_19_4, false), map(0x0F, MINECRAFT_1_20_2, false), map(0x10, MINECRAFT_1_20_3, false), - map(0x12, MINECRAFT_1_20_5, false)); + map(0x12, MINECRAFT_1_20_5, false), + map(0x14, MINECRAFT_1_21_2, false)); serverbound.register( KeepAlivePacket.class, KeepAlivePacket::new, @@ -332,7 +341,8 @@ public enum StateRegistry { map(0x12, MINECRAFT_1_19_4, false), map(0x14, MINECRAFT_1_20_2, false), map(0x15, MINECRAFT_1_20_3, false), - map(0x18, MINECRAFT_1_20_5, false)); + map(0x18, MINECRAFT_1_20_5, false), + map(0x1A, MINECRAFT_1_21_2, false)); serverbound.register( ResourcePackResponsePacket.class, ResourcePackResponsePacket::new, @@ -347,11 +357,13 @@ public enum StateRegistry { map(0x24, MINECRAFT_1_19_1, false), map(0x27, MINECRAFT_1_20_2, false), map(0x28, MINECRAFT_1_20_3, false), - map(0x2B, MINECRAFT_1_20_5, false)); + map(0x2B, MINECRAFT_1_20_5, false), + map(0x2D, MINECRAFT_1_21_2, false)); serverbound.register( FinishedUpdatePacket.class, () -> FinishedUpdatePacket.INSTANCE, map(0x0B, MINECRAFT_1_20_2, false), - map(0x0C, MINECRAFT_1_20_5, false)); + map(0x0C, MINECRAFT_1_20_5, false), + map(0x0E, MINECRAFT_1_21_2, false)); clientbound.register( BossBarPacket.class, @@ -449,7 +461,8 @@ public enum StateRegistry { map(0x1F, MINECRAFT_1_19_3, false), map(0x23, MINECRAFT_1_19_4, false), map(0x24, MINECRAFT_1_20_2, false), - map(0x26, MINECRAFT_1_20_5, false)); + map(0x26, MINECRAFT_1_20_5, false), + map(0x27, MINECRAFT_1_21_2, false)); clientbound.register( JoinGamePacket.class, JoinGamePacket::new, @@ -466,7 +479,8 @@ public enum StateRegistry { map(0x24, MINECRAFT_1_19_3, false), map(0x28, MINECRAFT_1_19_4, false), map(0x29, MINECRAFT_1_20_2, false), - map(0x2B, MINECRAFT_1_20_5, false)); + map(0x2B, MINECRAFT_1_20_5, false), + map(0x2C, MINECRAFT_1_21_2, false)); clientbound.register( RespawnPacket.class, RespawnPacket::new, @@ -486,12 +500,14 @@ public enum StateRegistry { map(0x41, MINECRAFT_1_19_4, true), map(0x43, MINECRAFT_1_20_2, true), map(0x45, MINECRAFT_1_20_3, true), - map(0x47, MINECRAFT_1_20_5, true)); + map(0x47, MINECRAFT_1_20_5, true), + map(0x4C, MINECRAFT_1_21_2, true)); clientbound.register( RemoveResourcePackPacket.class, RemoveResourcePackPacket::new, map(0x43, MINECRAFT_1_20_3, false), - map(0x45, MINECRAFT_1_20_5, false)); + map(0x45, MINECRAFT_1_20_5, false), + map(0x4A, MINECRAFT_1_21_2, false)); clientbound.register( ResourcePackRequestPacket.class, ResourcePackRequestPacket::new, @@ -511,7 +527,8 @@ public enum StateRegistry { map(0x40, MINECRAFT_1_19_4, false), map(0x42, MINECRAFT_1_20_2, false), map(0x44, MINECRAFT_1_20_3, false), - map(0x46, MINECRAFT_1_20_5, false)); + map(0x46, MINECRAFT_1_20_5, false), + map(0x4B, MINECRAFT_1_21_2, false)); clientbound.register( HeaderAndFooterPacket.class, HeaderAndFooterPacket::new, @@ -532,7 +549,8 @@ public enum StateRegistry { map(0x65, MINECRAFT_1_19_4, true), map(0x68, MINECRAFT_1_20_2, true), map(0x6A, MINECRAFT_1_20_3, true), - map(0x6D, MINECRAFT_1_20_5, true)); + map(0x6D, MINECRAFT_1_20_5, true), + map(0x74, MINECRAFT_1_21_2, true)); clientbound.register( LegacyTitlePacket.class, LegacyTitlePacket::new, @@ -552,7 +570,8 @@ public enum StateRegistry { map(0x5D, MINECRAFT_1_19_4, true), map(0x5F, MINECRAFT_1_20_2, true), map(0x61, MINECRAFT_1_20_3, true), - map(0x63, MINECRAFT_1_20_5, true)); + map(0x63, MINECRAFT_1_20_5, true), + map(0x6A, MINECRAFT_1_21_2, true)); clientbound.register( TitleTextPacket.class, TitleTextPacket::new, @@ -563,7 +582,8 @@ public enum StateRegistry { map(0x5F, MINECRAFT_1_19_4, true), map(0x61, MINECRAFT_1_20_2, true), map(0x63, MINECRAFT_1_20_3, true), - map(0x65, MINECRAFT_1_20_5, true)); + map(0x65, MINECRAFT_1_20_5, true), + map(0x6C, MINECRAFT_1_21_2, true)); clientbound.register( TitleActionbarPacket.class, TitleActionbarPacket::new, @@ -574,7 +594,8 @@ public enum StateRegistry { map(0x46, MINECRAFT_1_19_4, true), map(0x48, MINECRAFT_1_20_2, true), map(0x4A, MINECRAFT_1_20_3, true), - map(0x4C, MINECRAFT_1_20_5, true)); + map(0x4C, MINECRAFT_1_20_5, true), + map(0x51, MINECRAFT_1_21_2, true)); clientbound.register( TitleTimesPacket.class, TitleTimesPacket::new, @@ -585,7 +606,8 @@ public enum StateRegistry { map(0x60, MINECRAFT_1_19_4, true), map(0x62, MINECRAFT_1_20_2, true), map(0x64, MINECRAFT_1_20_3, true), - map(0x66, MINECRAFT_1_20_5, true)); + map(0x66, MINECRAFT_1_20_5, true), + map(0x6D, MINECRAFT_1_21_2, true)); clientbound.register( TitleClearPacket.class, TitleClearPacket::new, @@ -612,17 +634,20 @@ public enum StateRegistry { map(0x35, MINECRAFT_1_19_3, false), map(0x39, MINECRAFT_1_19_4, false), map(0x3B, MINECRAFT_1_20_2, false), - map(0x3D, MINECRAFT_1_20_5, false)); + map(0x3D, MINECRAFT_1_20_5, false), + map(0x3F, MINECRAFT_1_21_2, false)); clientbound.register( UpsertPlayerInfoPacket.class, UpsertPlayerInfoPacket::new, map(0x36, MINECRAFT_1_19_3, false), map(0x3A, MINECRAFT_1_19_4, false), map(0x3C, MINECRAFT_1_20_2, false), - map(0x3E, MINECRAFT_1_20_5, false)); + map(0x3E, MINECRAFT_1_20_5, false), + map(0x40, MINECRAFT_1_21_2, false)); clientbound.register( ClientboundStoreCookiePacket.class, ClientboundStoreCookiePacket::new, - map(0x6B, MINECRAFT_1_20_5, false)); + map(0x6B, MINECRAFT_1_20_5, false), + map(0x72, MINECRAFT_1_21_2, false)); clientbound.register( SystemChatPacket.class, SystemChatPacket::new, @@ -632,7 +657,8 @@ public enum StateRegistry { map(0x64, MINECRAFT_1_19_4, true), map(0x67, MINECRAFT_1_20_2, true), map(0x69, MINECRAFT_1_20_3, true), - map(0x6C, MINECRAFT_1_20_5, true)); + map(0x6C, MINECRAFT_1_20_5, true), + map(0x73, MINECRAFT_1_21_2, true)); clientbound.register( PlayerChatCompletionPacket.class, PlayerChatCompletionPacket::new, @@ -650,13 +676,15 @@ public enum StateRegistry { map(0x45, MINECRAFT_1_19_4, false), map(0x47, MINECRAFT_1_20_2, false), map(0x49, MINECRAFT_1_20_3, false), - map(0x4B, MINECRAFT_1_20_5, false)); + map(0x4B, MINECRAFT_1_20_5, false), + map(0x50, MINECRAFT_1_21_2, false)); clientbound.register( StartUpdatePacket.class, () -> StartUpdatePacket.INSTANCE, map(0x65, MINECRAFT_1_20_2, false), map(0x67, MINECRAFT_1_20_3, false), - map(0x69, MINECRAFT_1_20_5, false)); + map(0x69, MINECRAFT_1_20_5, false), + map(0x70, MINECRAFT_1_21_2, false)); clientbound.register( BundleDelimiterPacket.class, () -> BundleDelimiterPacket.INSTANCE, @@ -664,12 +692,18 @@ public enum StateRegistry { clientbound.register( TransferPacket.class, TransferPacket::new, - map(0x73, MINECRAFT_1_20_5, false) - ); - clientbound.register(ClientboundCustomReportDetailsPacket.class, ClientboundCustomReportDetailsPacket::new, - map(0x7A, MINECRAFT_1_21, false)); - clientbound.register(ClientboundServerLinksPacket.class, ClientboundServerLinksPacket::new, - map(0x7B, MINECRAFT_1_21, false)); + map(0x73, MINECRAFT_1_20_5, false), + map(0x7A, MINECRAFT_1_21_2, false)); + clientbound.register( + ClientboundCustomReportDetailsPacket.class, + ClientboundCustomReportDetailsPacket::new, + map(0x7A, MINECRAFT_1_21, false), + map(0x81, MINECRAFT_1_21_2, false)); + clientbound.register( + ClientboundServerLinksPacket.class, + ClientboundServerLinksPacket::new, + map(0x7B, MINECRAFT_1_21, false), + map(0x82, MINECRAFT_1_21_2, false)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java index 0e3668030..39e6fde02 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettingsPacket.java @@ -34,21 +34,25 @@ public class ClientSettingsPacket implements MinecraftPacket { private byte difficulty; // 1.7 Protocol private short skinParts; private int mainHand; - private boolean chatFilteringEnabled; // Added in 1.17 + private boolean textFilteringEnabled; // Added in 1.17 private boolean clientListingAllowed; // Added in 1.18, overwrites server-list "anonymous" mode + private int particleStatus; // Added in 1.21.2 public ClientSettingsPacket() { } public ClientSettingsPacket(String locale, byte viewDistance, int chatVisibility, boolean chatColors, - short skinParts, int mainHand, boolean chatFilteringEnabled, boolean clientListingAllowed) { + short skinParts, int mainHand, boolean textFilteringEnabled, boolean clientListingAllowed, + int particleStatus) { this.locale = locale; this.viewDistance = viewDistance; this.chatVisibility = chatVisibility; this.chatColors = chatColors; this.skinParts = skinParts; this.mainHand = mainHand; + this.textFilteringEnabled = textFilteringEnabled; this.clientListingAllowed = clientListingAllowed; + this.particleStatus = particleStatus; } public String getLocale() { @@ -102,12 +106,12 @@ public class ClientSettingsPacket implements MinecraftPacket { this.mainHand = mainHand; } - public boolean isChatFilteringEnabled() { - return chatFilteringEnabled; + public boolean isTextFilteringEnabled() { + return textFilteringEnabled; } - public void setChatFilteringEnabled(boolean chatFilteringEnabled) { - this.chatFilteringEnabled = chatFilteringEnabled; + public void setTextFilteringEnabled(boolean textFilteringEnabled) { + this.textFilteringEnabled = textFilteringEnabled; } public boolean isClientListingAllowed() { @@ -118,12 +122,20 @@ public class ClientSettingsPacket implements MinecraftPacket { this.clientListingAllowed = clientListingAllowed; } + public int getParticleStatus() { + return particleStatus; + } + + public void setParticleStatus(int particleStatus) { + this.particleStatus = particleStatus; + } + @Override public String toString() { return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance + ", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" + - skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + chatFilteringEnabled + - ", clientListingAllowed=" + clientListingAllowed + '}'; + skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + textFilteringEnabled + + ", clientListingAllowed=" + clientListingAllowed + ", particleStatus=" + particleStatus + '}'; } @Override @@ -143,10 +155,14 @@ public class ClientSettingsPacket implements MinecraftPacket { this.mainHand = ProtocolUtils.readVarInt(buf); if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) { - this.chatFilteringEnabled = buf.readBoolean(); + this.textFilteringEnabled = buf.readBoolean(); if (version.noLessThan(ProtocolVersion.MINECRAFT_1_18)) { this.clientListingAllowed = buf.readBoolean(); + + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) { + this.particleStatus = ProtocolUtils.readVarInt(buf); + } } } } @@ -172,11 +188,15 @@ public class ClientSettingsPacket implements MinecraftPacket { ProtocolUtils.writeVarInt(buf, mainHand); if (version.noLessThan(ProtocolVersion.MINECRAFT_1_17)) { - buf.writeBoolean(chatFilteringEnabled); + buf.writeBoolean(textFilteringEnabled); if (version.noLessThan(ProtocolVersion.MINECRAFT_1_18)) { buf.writeBoolean(clientListingAllowed); } + + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) { + ProtocolUtils.writeVarInt(buf, particleStatus); + } } } } @@ -201,8 +221,9 @@ public class ClientSettingsPacket implements MinecraftPacket { && difficulty == that.difficulty && skinParts == that.skinParts && mainHand == that.mainHand - && chatFilteringEnabled == that.chatFilteringEnabled + && textFilteringEnabled == that.textFilteringEnabled && clientListingAllowed == that.clientListingAllowed + && particleStatus == that.particleStatus && Objects.equals(locale, that.locale); } @@ -216,7 +237,8 @@ public class ClientSettingsPacket implements MinecraftPacket { difficulty, skinParts, mainHand, - chatFilteringEnabled, - clientListingAllowed); + textFilteringEnabled, + clientListingAllowed, + particleStatus); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java index f1d2b6400..787d858eb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGamePacket.java @@ -51,6 +51,7 @@ public class JoinGamePacket implements MinecraftPacket { private int simulationDistance; // 1.18+ private @Nullable Pair lastDeathPosition; // 1.19+ private int portalCooldown; // 1.20+ + private int seaLevel; // 1.21.2+ private boolean enforcesSecureChat; // 1.20.5+ public int getEntityId() { @@ -181,6 +182,14 @@ public class JoinGamePacket implements MinecraftPacket { this.portalCooldown = portalCooldown; } + public int getSeaLevel() { + return seaLevel; + } + + public void setSeaLevel(int seaLevel) { + this.seaLevel = seaLevel; + } + public boolean getEnforcesSecureChat() { return this.enforcesSecureChat; } @@ -204,6 +213,7 @@ public class JoinGamePacket implements MinecraftPacket { dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + ", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown + + ", seaLevel=" + seaLevel + '}'; } @@ -343,6 +353,11 @@ public class JoinGamePacket implements MinecraftPacket { } this.portalCooldown = ProtocolUtils.readVarInt(buf); + + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) { + this.seaLevel = ProtocolUtils.readVarInt(buf); + } + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { this.enforcesSecureChat = buf.readBoolean(); } @@ -491,6 +506,10 @@ public class JoinGamePacket implements MinecraftPacket { ProtocolUtils.writeVarInt(buf, portalCooldown); + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) { + ProtocolUtils.writeVarInt(buf, seaLevel); + } + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) { buf.writeBoolean(this.enforcesSecureChat); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java index ceaee9fd2..9304dc3f9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java @@ -41,6 +41,7 @@ public class RespawnPacket implements MinecraftPacket { private CompoundBinaryTag currentDimensionData; // 1.16.2+ private @Nullable Pair lastDeathPosition; // 1.19+ private int portalCooldown; // 1.20+ + private int seaLevel; // 1.21.2+ public RespawnPacket() { } @@ -48,7 +49,8 @@ public class RespawnPacket implements MinecraftPacket { public RespawnPacket(int dimension, long partialHashedSeed, short difficulty, short gamemode, String levelType, byte dataToKeep, DimensionInfo dimensionInfo, short previousGamemode, CompoundBinaryTag currentDimensionData, - @Nullable Pair lastDeathPosition, int portalCooldown) { + @Nullable Pair lastDeathPosition, int portalCooldown, + int seaLevel) { this.dimension = dimension; this.partialHashedSeed = partialHashedSeed; this.difficulty = difficulty; @@ -60,13 +62,15 @@ public class RespawnPacket implements MinecraftPacket { this.currentDimensionData = currentDimensionData; this.lastDeathPosition = lastDeathPosition; this.portalCooldown = portalCooldown; + this.seaLevel = seaLevel; } public static RespawnPacket fromJoinGame(JoinGamePacket joinGame) { return new RespawnPacket(joinGame.getDimension(), joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), (byte) 0, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition(), joinGame.getPortalCooldown()); + joinGame.getCurrentDimensionData(), joinGame.getLastDeathPosition(), + joinGame.getPortalCooldown(), joinGame.getSeaLevel()); } public int getDimension() { @@ -141,6 +145,14 @@ public class RespawnPacket implements MinecraftPacket { this.portalCooldown = portalCooldown; } + public int getSeaLevel() { + return seaLevel; + } + + public void setSeaLevel(int seaLevel) { + this.seaLevel = seaLevel; + } + @Override public String toString() { return "Respawn{" @@ -155,6 +167,7 @@ public class RespawnPacket implements MinecraftPacket { + ", previousGamemode=" + previousGamemode + ", dimensionData=" + currentDimensionData + ", portalCooldown=" + portalCooldown + + ", seaLevel=" + seaLevel + '}'; } @@ -204,6 +217,9 @@ public class RespawnPacket implements MinecraftPacket { if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20)) { this.portalCooldown = ProtocolUtils.readVarInt(buf); } + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) { + this.seaLevel = ProtocolUtils.readVarInt(buf); + } if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) { this.dataToKeep = buf.readByte(); } @@ -262,6 +278,10 @@ public class RespawnPacket implements MinecraftPacket { ProtocolUtils.writeVarInt(buf, portalCooldown); } + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_1)) { + ProtocolUtils.writeVarInt(buf, seaLevel); + } + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_2)) { buf.writeByte(dataToKeep); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfoPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfoPacket.java index 4c70536b0..e2d40d2e3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfoPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/UpsertPlayerInfoPacket.java @@ -188,6 +188,11 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket { if (info.displayName != null) { info.displayName.write(buf); } + }), + UPDATE_LIST_ORDER((version, buf, info) -> { // read + info.listOrder = ProtocolUtils.readVarInt(buf); + }, (version, buf, info) -> { // write + ProtocolUtils.writeVarInt(buf, info.listOrder); }); private final Read read; @@ -218,6 +223,7 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket { private int gameMode; @Nullable private ComponentHolder displayName; + private int listOrder; @Nullable private RemoteChatSession chatSession; @@ -250,6 +256,10 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket { return displayName; } + public int getListOrder() { + return listOrder; + } + @Nullable public RemoteChatSession getChatSession() { return chatSession; @@ -275,6 +285,10 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket { this.displayName = displayName; } + public void setListOrder(int listOrder) { + this.listOrder = listOrder; + } + public void setChatSession(@Nullable RemoteChatSession chatSession) { this.chatSession = chatSession; } @@ -288,6 +302,7 @@ public class UpsertPlayerInfoPacket implements MinecraftPacket { ", latency=" + latency + ", gameMode=" + gameMode + ", displayName=" + displayName + + ", listOrder=" + listOrder + ", chatSession=" + chatSession + '}'; } From 7a9227d517f7d95296458600fe2ca34f0f2bc296 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 22 Oct 2024 19:01:55 +0200 Subject: [PATCH 16/21] Fix extra respawn packet byte on <1.21.2 (#1448) --- .../velocitypowered/proxy/protocol/packet/RespawnPacket.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java index 9304dc3f9..fd9c8ca77 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/RespawnPacket.java @@ -278,7 +278,7 @@ public class RespawnPacket implements MinecraftPacket { ProtocolUtils.writeVarInt(buf, portalCooldown); } - if (version.noLessThan(ProtocolVersion.MINECRAFT_1_12_1)) { + if (version.noLessThan(ProtocolVersion.MINECRAFT_1_21_2)) { ProtocolUtils.writeVarInt(buf, seaLevel); } From 69ee655d7490705f950699f9d3b0a206671456ab Mon Sep 17 00:00:00 2001 From: Timon Date: Wed, 23 Oct 2024 20:39:12 +0200 Subject: [PATCH 17/21] Update 1.21.2 protocol to include 1.21.3 release (#1449) --- .../java/com/velocitypowered/api/network/ProtocolVersion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 1c6b6d536..3b5bbcb85 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -88,7 +88,7 @@ public enum ProtocolVersion implements Ordered { MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"), MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"), MINECRAFT_1_21(767, "1.21", "1.21.1"), - MINECRAFT_1_21_2(768, "1.21.2"); + MINECRAFT_1_21_2(768, "1.21.2", "1.21.3"); private static final int SNAPSHOT_BIT = 30; From b135148dfc007e9e116ff9822c65d491ace2157c Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 27 Oct 2024 13:54:08 -0400 Subject: [PATCH 18/21] fix a typo --- .../proxy/protocol/netty/MinecraftVarintFrameDecoder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java index 7bf7563ea..84f35381e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintFrameDecoder.java @@ -46,6 +46,7 @@ public class MinecraftVarintFrameDecoder extends ByteToMessageDecoder { // skip any runs of 0x00 we might find int packetStart = in.forEachByte(FIND_NON_NUL); if (packetStart == -1) { + in.clear(); return; } in.readerIndex(packetStart); From f2d6e143ae872f7da83d69a97acca0412923c7f6 Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 27 Oct 2024 14:10:33 -0400 Subject: [PATCH 19/21] Update several dependencies --- gradle/libs.versions.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 510c4d62a..58e8e11bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ configurate3 = "3.7.3" configurate4 = "4.1.2" flare = "2.0.1" -log4j = "2.22.1" -netty = "4.1.106.Final" +log4j = "2.24.1" +netty = "4.1.114.Final" [plugins] indra-publishing = "net.kyori.indra.publishing:2.0.6" @@ -18,7 +18,7 @@ auto-service = "com.google.auto.service:auto-service:1.0.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1" brigadier = "com.velocitypowered:velocity-brigadier:1.0.0-SNAPSHOT" bstats = "org.bstats:bstats-base:3.0.2" -caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.5" +caffeine = "com.github.ben-manes.caffeine:caffeine:3.1.8" checker-qual = "org.checkerframework:checker-qual:3.42.0" checkstyle = "com.puppycrawl.tools:checkstyle:10.9.3" completablefutures = "com.spotify:completable-futures:0.3.6" @@ -28,15 +28,15 @@ configurate3-gson = { module = "org.spongepowered:configurate-gson", version.ref configurate4-hocon = { module = "org.spongepowered:configurate-hocon", version.ref = "configurate4" } configurate4-yaml = { module = "org.spongepowered:configurate-yaml", version.ref = "configurate4" } configurate4-gson = { module = "org.spongepowered:configurate-gson", version.ref = "configurate4" } -disruptor = "com.lmax:disruptor:3.4.4" -fastutil = "it.unimi.dsi:fastutil:8.5.12" +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.23.0" +jline = "org.jline:jline-terminal-jansi:3.27.1" 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" -kyori-ansi = "net.kyori:ansi:1.0.3" +kyori-ansi = "net.kyori:ansi:1.1.0" 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" From dc40e160d7dbbf63c448ca0fd6b2788d206f22eb Mon Sep 17 00:00:00 2001 From: Aaron <71191102+RealBauHD@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:27:09 +0100 Subject: [PATCH 20/21] replace old velocitypowered.com links (#1399) * replace old links * content to 'PaperMC', cause its probably the best --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- .../proxy/command/builtin/VelocityCommand.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 82cada2b6..ec81079b6 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -44,7 +44,7 @@ body: ``` [17:44:10 INFO]: Velocity 3.3.0-SNAPSHOT (git-9d25d309-b400) [17:44:10 INFO]: Copyright 2018-2023 Velocity Contributors. Velocity is licensed under the terms of the GNU General Public License v3. - [17:44:10 INFO]: velocitypowered.com - GitHub + [17:44:10 INFO]: PaperMC - GitHub ``` validations: diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 79ac41e74..85bf2061b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -174,10 +174,10 @@ public final class VelocityCommand { if (version.getName().equals("Velocity")) { final TextComponent embellishment = Component.text() .append(Component.text() - .content("velocitypowered.com") + .content("PaperMC") .color(NamedTextColor.GREEN) .clickEvent( - ClickEvent.openUrl("https://velocitypowered.com")) + ClickEvent.openUrl("https://papermc.io/software/velocity")) .build()) .append(Component.text(" - ")) .append(Component.text() From 08a42b3723633ea5eb6b96c0bb42180f3c2b07eb Mon Sep 17 00:00:00 2001 From: Andrew Steinborn Date: Sun, 27 Oct 2024 15:13:16 -0400 Subject: [PATCH 21/21] Replace home-made legacy hover event serializer with Adventure's implementation This technically can break backwards compatibility, but this seems to be very unlikely to be the practice in reality. (The Velocity implementation probably wasn't correct, anyway.) --- gradle/libs.versions.toml | 3 +- proxy/build.gradle.kts | 2 +- .../proxy/protocol/ProtocolUtils.java | 9 +- .../VelocityLegacyHoverEventSerializer.java | 123 ------------------ 4 files changed, 7 insertions(+), 130 deletions(-) delete mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 58e8e11bf..7557ed42c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,8 @@ spotless = "com.diffplug.spotless:6.25.0" [libraries] adventure-bom = "net.kyori:adventure-bom:4.17.0" -adventure-facet = "net.kyori:adventure-platform-facet:4.3.2" +adventure-text-serializer-json-legacy-impl = "net.kyori:adventure-text-serializer-json-legacy-impl:4.17.0" +adventure-facet = "net.kyori:adventure-platform-facet:4.3.4" asm = "org.ow2.asm:asm:9.6" auto-service = "com.google.auto.service:auto-service:1.0.1" auto-service-annotations = "com.google.auto.service:auto-service-annotations:1.0.1" diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 035ff24de..9b74dae57 100644 --- a/proxy/build.gradle.kts +++ b/proxy/build.gradle.kts @@ -127,7 +127,7 @@ dependencies { runtimeOnly(libs.disruptor) implementation(libs.fastutil) implementation(platform(libs.adventure.bom)) - implementation("net.kyori:adventure-nbt") + implementation(libs.adventure.text.serializer.json.legacy.impl) implementation(libs.adventure.facet) implementation(libs.completablefutures) implementation(libs.nightconfig) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 797a58223..ca63c1e56 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -25,7 +25,6 @@ import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.util.VelocityLegacyHoverEventSerializer; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; @@ -47,6 +46,7 @@ import net.kyori.adventure.nbt.BinaryTagTypes; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.json.JSONOptions; +import net.kyori.adventure.text.serializer.json.legacyimpl.NBTLegacyHoverEventSerializer; import net.kyori.option.OptionState; /** @@ -58,8 +58,7 @@ public enum ProtocolUtils { private static final GsonComponentSerializer PRE_1_16_SERIALIZER = GsonComponentSerializer.builder() .downsampleColors() - .emitLegacyHoverEvent() - .legacyHoverEventSerializer(VelocityLegacyHoverEventSerializer.INSTANCE) + .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .options( OptionState.optionState() // before 1.16 @@ -74,7 +73,7 @@ public enum ProtocolUtils { .build(); private static final GsonComponentSerializer PRE_1_20_3_SERIALIZER = GsonComponentSerializer.builder() - .legacyHoverEventSerializer(VelocityLegacyHoverEventSerializer.INSTANCE) + .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .options( OptionState.optionState() // after 1.16 @@ -89,7 +88,7 @@ public enum ProtocolUtils { .build(); private static final GsonComponentSerializer MODERN_SERIALIZER = GsonComponentSerializer.builder() - .legacyHoverEventSerializer(VelocityLegacyHoverEventSerializer.INSTANCE) + .legacyHoverEventSerializer(NBTLegacyHoverEventSerializer.get()) .options( OptionState.optionState() // after 1.16 diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java deleted file mode 100644 index e667a9b16..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (C) 2020-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 . - */ - -package com.velocitypowered.proxy.protocol.util; - -import java.io.IOException; -import java.util.UUID; -import net.kyori.adventure.key.Key; -import net.kyori.adventure.nbt.CompoundBinaryTag; -import net.kyori.adventure.nbt.TagStringIO; -import net.kyori.adventure.nbt.api.BinaryTagHolder; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.event.HoverEvent; -import net.kyori.adventure.text.event.HoverEvent.ShowEntity; -import net.kyori.adventure.text.event.HoverEvent.ShowItem; -import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; -import net.kyori.adventure.util.Codec.Decoder; -import net.kyori.adventure.util.Codec.Encoder; -import org.checkerframework.checker.nullness.qual.NonNull; - -/** - * An implementation of {@link LegacyHoverEventSerializer} that implements the interface in the most - * literal, albeit "incompatible" way possible. - */ -public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSerializer { - - public static final LegacyHoverEventSerializer INSTANCE = - new VelocityLegacyHoverEventSerializer(); - - private VelocityLegacyHoverEventSerializer() { - - } - - private static Key legacyIdToFakeKey(byte id) { - return Key.key("velocity", "legacy_hover/id_" + id); - } - - @Override - public HoverEvent.@NonNull ShowItem deserializeShowItem(@NonNull Component input) - throws IOException { - String snbt = PlainTextComponentSerializer.plainText().serialize(input); - CompoundBinaryTag item = TagStringIO.get().asCompound(snbt); - - Key key; - String idIfString = item.getString("id", ""); - if (idIfString.isEmpty()) { - key = legacyIdToFakeKey(item.getByte("id")); - } else { - key = Key.key(idIfString); - } - - byte count = item.getByte("Count", (byte) 1); - return ShowItem.of(key, count, BinaryTagHolder.binaryTagHolder(snbt)); - } - - @Override - public HoverEvent.@NonNull ShowEntity deserializeShowEntity(@NonNull Component input, - Decoder componentDecoder) throws IOException { - String snbt = PlainTextComponentSerializer.plainText().serialize(input); - CompoundBinaryTag item = TagStringIO.get().asCompound(snbt); - - Component name; - try { - name = componentDecoder.decode(item.getString("name")); - } catch (Exception e) { - name = Component.text(item.getString("name")); - } - - return ShowEntity.of(Key.key(item.getString("type")), - UUID.fromString(item.getString("id")), - name); - } - - @Override - public @NonNull Component serializeShowItem(HoverEvent.@NonNull ShowItem input) - throws IOException { - final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder() - .putByte("Count", (byte) input.count()); - - String keyAsString = input.item().asString(); - if (keyAsString.startsWith("velocity:legacy_hover/id_")) { - builder.putByte("id", Byte.parseByte(keyAsString - .substring("velocity:legacy_hover/id_".length()))); - } else { - builder.putString("id", keyAsString); - } - - BinaryTagHolder nbt = input.nbt(); - if (nbt != null) { - builder.put("tag", TagStringIO.get().asCompound(nbt.string())); - } - - return Component.text(TagStringIO.get().asString(builder.build())); - } - - @Override - public @NonNull Component serializeShowEntity(HoverEvent.@NonNull ShowEntity input, - Encoder componentEncoder) throws IOException { - CompoundBinaryTag.Builder tag = CompoundBinaryTag.builder() - .putString("id", input.id().toString()) - .putString("type", input.type().asString()); - Component name = input.name(); - if (name != null) { - tag.putString("name", componentEncoder.encode(name)); - } - return Component.text(TagStringIO.get().asString(tag.build())); - } -}