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/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 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/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/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 { 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", "1.21.3"); private static final int SNAPSHOT_BIT = 30; 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/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/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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 510c4d62a..7557ed42c 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" @@ -12,13 +12,14 @@ 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" 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 +29,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" diff --git a/proxy/build.gradle.kts b/proxy/build.gradle.kts index 5e1387b06..9b74dae57 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 { @@ -118,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/ProxyOptions.java b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java index 64a49917d..d0b7f34f2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/ProxyOptions.java @@ -17,11 +17,17 @@ 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 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 +41,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 +57,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 +92,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()); @@ -263,7 +313,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() { @@ -533,7 +589,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..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,6 +54,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.stream.Collectors; @@ -71,13 +77,16 @@ public class VelocityCommandManager implements CommandManager { private final SuggestionsProvider suggestionsProvider; private final CommandGraphInjector injector; private final Map commandMetas; + 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); @@ -218,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) { @@ -249,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)); } } @@ -260,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())); - }, eventManager.getAsyncExecutor()); + final ParseResults parsed = this.parse( + commandResult.getCommand().orElse(cmdLine), source); + return CompletableFuture.supplyAsync( + () -> executeImmediately0(source, parsed), this.getAsyncExecutor(parsed) + ); + }, figureAsyncExecutorForParsing()); } @Override @@ -276,7 +286,12 @@ public class VelocityCommandManager implements CommandManager { Preconditions.checkNotNull(cmdLine, "cmdLine"); return CompletableFuture.supplyAsync( - () -> executeImmediately0(source, cmdLine), eventManager.getAsyncExecutor()); + () -> this.parse(cmdLine, source), figureAsyncExecutorForParsing() + ).thenCompose( + parsed -> CompletableFuture.supplyAsync( + () -> executeImmediately0(source, parsed), this.getAsyncExecutor(parsed) + ) + ); } /** @@ -324,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(); } @@ -370,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/builtin/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ShutdownCommand.java index a77adc9b1..b60ead91a 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 @@ -24,6 +24,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.Component; import net.kyori.adventure.text.minimessage.MiniMessage; @@ -45,7 +46,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; 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() 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/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/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/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) { 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/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(); 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..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,31 +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 complete event will be handled on an async thread. - */ - ALWAYS, /** * The event will never run async, everything is handled on the netty thread. */ - NEVER + 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; } } @@ -194,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])); } /** @@ -230,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; @@ -302,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(); @@ -351,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)); } @@ -386,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); @@ -473,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; @@ -505,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"}) @@ -527,6 +593,7 @@ public class VelocityEventManager implements EventManager { this.event = event; this.index = index; this.currentlyAsync = currentlyAsync; + this.firedOnThread = Thread.currentThread(); } @Override @@ -537,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; @@ -580,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)); + } } } @@ -606,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 @@ -626,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/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 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/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/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/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); 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..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 @@ -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_21_2)) { + 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 + '}'; } 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(); 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())); - } -} 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 9bd202fa4..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,9 +29,9 @@ 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.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -47,19 +47,9 @@ 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); + this.manager = new VelocityCommandManager(eventManager, new FakePluginManager()); } final void assertHandled(final String input) { 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..7992ac52e 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/testutil/FakePluginManager.java @@ -18,14 +18,16 @@ 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; +import com.velocitypowered.proxy.plugin.virtual.VelocityVirtualPlugin; 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 +38,23 @@ 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 final PluginContainer containerVelocity = new FakePluginContainer("velocity", + VelocityVirtualPlugin.INSTANCE); + + 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 if (instance == VelocityVirtualPlugin.INSTANCE) { + return Optional.of(containerVelocity); } else { return Optional.empty(); } @@ -54,9 +64,11 @@ 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); + case "velocity": + return Optional.of(containerVelocity); default: return Optional.empty(); } @@ -64,7 +76,7 @@ public class FakePluginManager implements PluginManager { @Override public @NonNull Collection getPlugins() { - return ImmutableList.of(PC_A, PC_B); + return ImmutableList.of(containerVelocity, containerA, containerB); } @Override @@ -77,16 +89,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