diff --git a/.gitignore b/.gitignore index fd8f6bff5..8df1bfa3e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,137 +1,87 @@ - -# Created by https://www.gitignore.io/api/java,gradle,intellij - ### Intellij ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf -.idea/**/compiler.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format +.idea/ *.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin +/out/ +*.iml .idea_modules/ - -# JIRA plugin atlassian-ide-plugin.xml -# Cursive Clojure plugin -.idea/replstate.xml +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders +.externalToolBuilders/ +*.launch +.factorypath +.recommenders/ +.apt_generated/ +.project +.classpath -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties +### Linux ### +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* -# Editor-based Rest Client -.idea/httpRequests +### macOS ### +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk -### Intellij Patch ### -# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 - -*.iml -modules.xml -.idea/misc.xml -*.ipr -.idea/vcs.xml - -# Sonarlint plugin -.idea/sonarlint - -### Java ### -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Eclipse # -**/.classpath -**/.project -**/.settings/ -**/bin/ - -# NetBeans Gradle# +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ .nb-gradle/ -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.lnk ### Gradle ### .gradle -build/ - -# Ignore Gradle GUI config +/build/ +/out/ gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar - -# Cache of project .gradletasknamecache -# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -# gradle/wrapper/gradle-wrapper.properties - - -# End of https://www.gitignore.io/api/java,gradle,intellij - -# Other trash +### Other trash ### logs/ /velocity.toml server-icon.png /bin/ run/ +plugins/ \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index bbf4ab4ab..19ae32e52 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,36 +1,33 @@ pipeline { - agent { - docker { - image 'velocitypowered/openjdk8-plus-git:slim' - args '-v gradle-cache:/root/.gradle:rw -v maven-repo:/maven-repo:rw -v javadoc:/javadoc' - } - } + agent none stages { stage('Build') { + agent { + docker { + image 'velocitypowered/openjdk8-plus-git:slim' + args '-v gradle-cache:/root/.gradle:rw' + } + } steps { - sh './gradlew build' + sh './gradlew build --no-daemon' archiveArtifacts 'proxy/build/libs/*-all.jar,api/build/libs/*-all.jar' } } - stage('Deploy Artifacts') { + stage('Deploy') { when { expression { GIT_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() return GIT_BRANCH == 'master' } } - steps { - sh 'export MAVEN_DEPLOYMENT=true; ./gradlew publish' - } - } - stage('Deploy Javadoc') { - when { - expression { - GIT_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim() - return GIT_BRANCH == 'master' + agent { + docker { + image 'velocitypowered/openjdk8-plus-git:slim' + args '-v gradle-cache:/root/.gradle:rw -v maven-repo:/maven-repo:rw -v javadoc:/javadoc:rw' } } steps { + sh 'export MAVEN_DEPLOYMENT=true; ./gradlew publish --no-daemon' sh 'rsync -av --delete ./api/build/docs/javadoc/ /javadoc' } } diff --git a/README.md b/README.md index 493fdb342..d585b15ab 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,8 @@ [![Build Status](https://img.shields.io/jenkins/s/https/ci.velocitypowered.com/job/velocity/job/master.svg)](https://ci.velocitypowered.com/job/velocity/job/master/) [![Join our Discord](https://img.shields.io/discord/472484458856185878.svg?logo=discord&label=)](https://discord.gg/8cB9Bgf) -Velocity is a next-generation Minecraft: Java Edition proxy suite. It is -designed specifically for enhanced server support and scalability whilst -not compromising flexibility. +A Minecraft server proxy with unparalleled server support, scalability, +and flexibility. Velocity is licensed under the MIT license for ultimate permissiveness and expanding the pool of potential contributors and users. @@ -40,8 +39,7 @@ page. ## Status Velocity is currently in an alpha state: it is prone to change at any time and -is not currently suitable for production usage. For development and testing -purposes, however, Velocity is fully-fledged and ready to go. +is currently only suitable for small servers and development/testing. Velocity supports Minecraft 1.8-1.13.1, and has full support for Paper and Sponge. Forge is fully supported but mod compatibility may vary. \ No newline at end of file diff --git a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java index a00f9c7d3..6a8310c72 100644 --- a/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java +++ b/api/src/ap/java/com/velocitypowered/api/plugin/ap/PluginAnnotationProcessor.java @@ -19,7 +19,8 @@ import javax.tools.StandardLocation; import java.io.BufferedWriter; import java.io.IOException; import java.io.Writer; -import java.util.*; +import java.util.Objects; +import java.util.Set; @SupportedAnnotationTypes({"com.velocitypowered.api.plugin.Plugin"}) public class PluginAnnotationProcessor extends AbstractProcessor { diff --git a/api/src/main/java/com/velocitypowered/api/command/Command.java b/api/src/main/java/com/velocitypowered/api/command/Command.java index d1cfd9f49..ecbbcb379 100644 --- a/api/src/main/java/com/velocitypowered/api/command/Command.java +++ b/api/src/main/java/com/velocitypowered/api/command/Command.java @@ -26,4 +26,19 @@ public interface Command { default List suggest(@NonNull CommandSource source, @NonNull String[] currentArgs) { return ImmutableList.of(); } + + /** + * Tests to check if the {@code source} has permission to use this command + * with the provided {@code args}. + * + *

If this method returns false, the handling will be forwarded onto + * the players current server.

+ * + * @param source the source of the command + * @param args the arguments for this command + * @return whether the source has permission + */ + default boolean hasPermission(@NonNull CommandSource source, @NonNull String[] args) { + return true; + } } diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/PluginMessageEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/PluginMessageEvent.java new file mode 100644 index 000000000..b16f933e4 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/connection/PluginMessageEvent.java @@ -0,0 +1,105 @@ +package com.velocitypowered.api.event.connection; + +import com.google.common.base.Preconditions; +import com.google.common.io.ByteArrayDataInput; +import com.google.common.io.ByteStreams; +import com.velocitypowered.api.event.ResultedEvent; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.ChannelMessageSink; +import com.velocitypowered.api.proxy.messages.ChannelMessageSource; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.Arrays; + +/** + * This event is fired when a plugin message is sent to the proxy, either from a client ({@link com.velocitypowered.api.proxy.Player}) + * or a server ({@link com.velocitypowered.api.proxy.ServerConnection}). + */ +public class PluginMessageEvent implements ResultedEvent { + private final ChannelMessageSource source; + private final ChannelMessageSink target; + private final ChannelIdentifier identifier; + private final byte[] data; + private ForwardResult result; + + public PluginMessageEvent(ChannelMessageSource source, ChannelMessageSink target, ChannelIdentifier identifier, byte[] data) { + this.source = Preconditions.checkNotNull(source, "source"); + this.target = Preconditions.checkNotNull(target, "target"); + this.identifier = Preconditions.checkNotNull(identifier, "identifier"); + this.data = Preconditions.checkNotNull(data, "data"); + this.result = ForwardResult.forward(); + } + + @Override + public ForwardResult getResult() { + return result; + } + + @Override + public void setResult(@NonNull ForwardResult result) { + this.result = Preconditions.checkNotNull(result, "result"); + } + + public ChannelMessageSource getSource() { + return source; + } + + public ChannelMessageSink getTarget() { + return target; + } + + public ChannelIdentifier getIdentifier() { + return identifier; + } + + public byte[] getData() { + return Arrays.copyOf(data, data.length); + } + + public ByteArrayDataInput dataAsDataStream() { + return ByteStreams.newDataInput(data); + } + + @Override + public String toString() { + return "PluginMessageEvent{" + + "source=" + source + + ", target=" + target + + ", identifier=" + identifier + + ", data=" + Arrays.toString(data) + + ", result=" + result + + '}'; + } + + /** + * A result determining whether or not to forward this message on. + */ + public static class ForwardResult implements ResultedEvent.Result { + private static final ForwardResult ALLOWED = new ForwardResult(true); + private static final ForwardResult DENIED = new ForwardResult(false); + + private final boolean allowed; + + private ForwardResult(boolean b) { + this.allowed = b; + } + + @Override + public boolean isAllowed() { + return allowed; + } + + @Override + public String toString() { + return allowed ? "forward to sink" : "handled message at proxy"; + } + + public static ForwardResult forward() { + return ALLOWED; + } + + public static ForwardResult handled() { + return DENIED; + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/PostLoginEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/PostLoginEvent.java new file mode 100644 index 000000000..7ecc23bbc --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/event/connection/PostLoginEvent.java @@ -0,0 +1,28 @@ +package com.velocitypowered.api.event.connection; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.Player; + +/** + * This event is fired once the player has been successfully authenticated and + * fully initialized and player will be connected to server after this event + */ +public class PostLoginEvent { + + private final Player player; + + public PostLoginEvent(Player player) { + this.player = Preconditions.checkNotNull(player, "player"); + } + + public Player getPlayer() { + return player; + } + + @Override + public String toString() { + return "PostLoginEvent{" + + "player=" + player + + '}'; + } +} diff --git a/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java b/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java index ef8edfb9f..d5e817f49 100644 --- a/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java +++ b/api/src/main/java/com/velocitypowered/api/event/connection/PreLoginEvent.java @@ -3,12 +3,13 @@ package com.velocitypowered.api.event.connection; import com.google.common.base.Preconditions; import com.velocitypowered.api.event.ResultedEvent; import com.velocitypowered.api.proxy.InboundConnection; - import net.kyori.text.Component; - +import net.kyori.text.serializer.ComponentSerializers; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.Optional; + /** * This event is fired when a player has initiated a connection with the proxy but before the proxy authenticates the * player with Mojang or before the player's proxy connection is fully established (for offline mode). @@ -52,44 +53,59 @@ public class PreLoginEvent implements ResultedEvent reason; + + private PreLoginComponentResult(Result result, @Nullable Component reason) { + this.result = result; + this.reason = Optional.ofNullable(reason); } - private PreLoginComponentResult(@Nullable Component reason) { - super(reason == null, reason); - // Don't care about this - this.onlineMode = false; + @Override + public boolean isAllowed() { + return result != Result.DISALLOWED; + } + + public Optional getReason() { + return reason; } public boolean isOnlineModeAllowed() { - return this.onlineMode; + return result == Result.FORCE_ONLINE; + } + + public boolean isForceOfflineMode() { + return result == Result.FORCE_OFFLINE; } @Override public String toString() { + if (isForceOfflineMode()) { + return "allowed with force offline mode"; + } if (isOnlineModeAllowed()) { return "allowed with online mode"; } - - return super.toString(); + if (isAllowed()) { + return "allowed"; + } + if (reason.isPresent()) { + return "denied: " + ComponentSerializers.PLAIN.serialize(reason.get()); + } + return "denied"; } /** - * Returns a result indicating the connection will be allowed through the proxy. + * Returns a result indicating the connection will be allowed through + * the proxy. * @return the allowed result */ public static PreLoginComponentResult allowed() { @@ -97,23 +113,41 @@ public class PreLoginEvent implements ResultedEvent { private final Player player; - private final ServerInfo server; + private final RegisteredServer server; private final Component originalReason; private final boolean duringLogin; private ServerKickResult result; - public KickedFromServerEvent(Player player, ServerInfo server, Component originalReason, boolean duringLogin, Component fancyReason) { + public KickedFromServerEvent(Player player, RegisteredServer server, Component originalReason, boolean duringLogin, Component fancyReason) { this.player = Preconditions.checkNotNull(player, "player"); this.server = Preconditions.checkNotNull(server, "server"); this.originalReason = Preconditions.checkNotNull(originalReason, "originalReason"); @@ -40,7 +40,7 @@ public class KickedFromServerEvent implements ResultedEvent { private final Player player; + private final RegisteredServer originalServer; private ServerResult result; - public ServerPreConnectEvent(Player player, ServerResult result) { + public ServerPreConnectEvent(Player player, RegisteredServer originalServer) { this.player = Preconditions.checkNotNull(player, "player"); - this.result = Preconditions.checkNotNull(result, "result"); + this.originalServer = Preconditions.checkNotNull(originalServer, "originalServer"); + this.result = ServerResult.allowed(originalServer); } public Player getPlayer() { @@ -35,10 +37,15 @@ public class ServerPreConnectEvent implements ResultedEvent connect(); + CompletableFuture connect(); /** * Initiates the connection to the remote server without waiting for a result. Velocity will use generic error diff --git a/api/src/main/java/com/velocitypowered/api/proxy/Player.java b/api/src/main/java/com/velocitypowered/api/proxy/Player.java index 821ed0587..8e61d2cc5 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -1,13 +1,15 @@ package com.velocitypowered.api.proxy; import com.velocitypowered.api.command.CommandSource; -import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; -import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.api.proxy.player.PlayerSettings; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.MessagePosition; +import com.velocitypowered.api.util.title.Title; import java.util.List; + import net.kyori.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; @@ -65,10 +67,10 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage /** * Creates a new connection request so that the player can connect to another server. - * @param info the server to connect to + * @param server the server to connect to * @return a new connection request */ - ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info); + ConnectionRequestBuilder createConnectionRequest(@NonNull RegisteredServer server); /** * Gets a game profile properties of player @@ -100,4 +102,17 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage * @param reason component with the reason */ void disconnect(Component reason); + + /** + * Sends the specified title to the client. + * @param title the title to send + */ + void sendTitle(Title title); + + /** + * Sends chat input onto the players current server as if they typed it + * into the client chat box. + * @param input the chat input to send + */ + void spoofChatInput(String input); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java index b4c851c58..11bf095ff 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -1,12 +1,14 @@ package com.velocitypowered.api.proxy; -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.messages.ChannelRegistrar; -import com.velocitypowered.api.scheduler.Scheduler; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.api.scheduler.Scheduler; +import net.kyori.text.Component; import java.net.InetSocketAddress; import java.util.Collection; @@ -31,6 +33,12 @@ public interface ProxyServer { */ Optional getPlayer(UUID uuid); + /** + * Broadcasts a message to all players currently online. + * @param component the message to send + */ + void broadcast(Component component); + /** * Retrieves all players currently connected to this proxy. This call may or may not be a snapshot of all players * online. @@ -45,23 +53,24 @@ public interface ProxyServer { int getPlayerCount(); /** - * Retrieves a registered {@link ServerInfo} instance by its name. The search is case-insensitive. + * Retrieves a registered {@link RegisteredServer} instance by its name. The search is case-insensitive. * @param name the name of the server * @return the registered server, which may be empty */ - Optional getServerInfo(String name); + Optional getServer(String name); /** - * Retrieves all {@link ServerInfo}s registered with this proxy. + * Retrieves all {@link RegisteredServer}s registered with this proxy. * @return the servers registered with this proxy */ - Collection getAllServers(); + Collection getAllServers(); /** * Registers a server with this proxy. A server with this name should not already exist. * @param server the server to register + * @return the newly registered server */ - void registerServer(ServerInfo server); + RegisteredServer registerServer(ServerInfo server); /** * Unregisters this server from the proxy. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java b/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java index 5f12fb326..b800131c0 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ServerConnection.java @@ -2,6 +2,7 @@ package com.velocitypowered.api.proxy; import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; /** @@ -12,6 +13,12 @@ public interface ServerConnection extends ChannelMessageSource, ChannelMessageSi * Returns the server that this connection is connected to. * @return the server this connection is connected to */ + RegisteredServer getServer(); + + /** + * Returns the server info for this connection. + * @return the server info for this connection + */ ServerInfo getServerInfo(); /** diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java index 57ebe4ea6..37f02e539 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelMessageSink.java @@ -8,6 +8,7 @@ public interface ChannelMessageSink { * Sends a plugin message to this target. * @param identifier the channel identifier to send the message on * @param data the data to send + * @return whether or not the message could be sent */ - void sendPluginMessage(ChannelIdentifier identifier, byte[] data); + boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data); } diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java index 2d77988b5..84db8799d 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelRegistrar.java @@ -1,16 +1,14 @@ package com.velocitypowered.api.proxy.messages; /** - * Represents an interface to register and unregister {@link MessageHandler} instances for handling plugin messages from - * the client or the server. + * Represents an interface to register and unregister {@link ChannelIdentifier}s for the proxy to listen on. */ public interface ChannelRegistrar { /** - * Registers the specified message handler to listen for plugin messages on the specified channels. - * @param handler the handler to register + * Registers the specified message identifiers to listen on for the * @param identifiers the channel identifiers to register */ - void register(MessageHandler handler, ChannelIdentifier... identifiers); + void register(ChannelIdentifier... identifiers); /** * Unregisters the handler for the specified channel. diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelSide.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelSide.java deleted file mode 100644 index 12256f432..000000000 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/ChannelSide.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.velocitypowered.api.proxy.messages; - -/** - * Represents from "which side" of the proxy the plugin message came from. - */ -public enum ChannelSide { - /** - * The plugin message came from a server that a client was connected to. - */ - FROM_SERVER, - /** - * The plugin message came from the client. - */ - FROM_CLIENT -} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/MessageHandler.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/MessageHandler.java deleted file mode 100644 index 70a7e5fa7..000000000 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/MessageHandler.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.velocitypowered.api.proxy.messages; - -/** - * Represents a handler for handling plugin messages. - */ -public interface MessageHandler { - /** - * Handles an incoming plugin message. - * @param source the source of the plugin message - * @param side from where the plugin message originated - * @param identifier the channel on which the message was sent - * @param data the data inside the plugin message - * @return a {@link ForwardStatus} indicating whether or not to forward this plugin message on - */ - ForwardStatus handle(ChannelMessageSource source, ChannelSide side, ChannelIdentifier identifier, byte[] data); - - enum ForwardStatus { - /** - * Forwards this plugin message on to the client or server, depending on the {@link ChannelSide} it originated - * from. - */ - FORWARD, - /** - * Discard the plugin message and do not forward it on. - */ - HANDLED - } -} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java index b8f689324..1355ed239 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/messages/MinecraftChannelIdentifier.java @@ -10,7 +10,7 @@ import java.util.regex.Pattern; * Represents a Minecraft 1.13+ channel identifier. This class is immutable and safe for multi-threaded use. */ public final class MinecraftChannelIdentifier implements ChannelIdentifier { - private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]+", Pattern.CASE_INSENSITIVE); + private static final Pattern VALID_IDENTIFIER_REGEX = Pattern.compile("[a-z0-9\\-_]+"); private final String namespace; private final String name; diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/RegisteredServer.java b/api/src/main/java/com/velocitypowered/api/proxy/server/RegisteredServer.java new file mode 100644 index 000000000..b7bf9acb5 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/RegisteredServer.java @@ -0,0 +1,30 @@ +package com.velocitypowered.api.proxy.server; + +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.ChannelMessageSink; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; + +/** + * Represents a server that has been registered with the proxy. + */ +public interface RegisteredServer extends ChannelMessageSink { + /** + * Returns the {@link ServerInfo} for this server. + * @return the server info + */ + ServerInfo getServerInfo(); + + /** + * Returns a list of all the players currently connected to this server on this proxy. + * @return the players on this proxy + */ + Collection getPlayersConnected(); + + /** + * Attempts to ping the remote server and return the server list ping result. + * @return the server ping result from the server + */ + CompletableFuture ping(); +} diff --git a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java index 252ad77c3..f4fb7c1ae 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/server/ServerPing.java @@ -16,13 +16,13 @@ public class ServerPing { private final Players players; private final Component description; private final @Nullable Favicon favicon; - private final Modinfo modinfo; + private final ModInfo modinfo; public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon) { - this(version, players, description, favicon, Modinfo.DEFAULT); + this(version, players, description, favicon, ModInfo.DEFAULT); } - public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon, @Nullable Modinfo modinfo) { + public ServerPing(Version version, @Nullable Players players, Component description, @Nullable Favicon favicon, ServerPing.@Nullable ModInfo modinfo) { this.version = Preconditions.checkNotNull(version, "version"); this.players = players; this.description = Preconditions.checkNotNull(description, "description"); @@ -46,7 +46,7 @@ public class ServerPing { return Optional.ofNullable(favicon); } - public Optional getModinfo() { + public Optional getModinfo() { return Optional.ofNullable(modinfo); } @@ -74,6 +74,7 @@ public class ServerPing { builder.favicon = favicon; builder.nullOutModinfo = modinfo == null; if (modinfo != null) { + builder.modType = modinfo.type; builder.mods.addAll(modinfo.modList); } return builder; @@ -91,6 +92,7 @@ public class ServerPing { private int onlinePlayers; private int maximumPlayers; private final List samplePlayers = new ArrayList<>(); + private String modType; private final List mods = new ArrayList<>(); private Component description; private Favicon favicon; @@ -121,6 +123,11 @@ public class ServerPing { return this; } + public Builder modType(String modType) { + this.modType = Preconditions.checkNotNull(modType, "modType"); + return this; + } + public Builder mods(Mod... mods) { this.mods.addAll(Arrays.asList(mods)); return this; @@ -158,7 +165,7 @@ public class ServerPing { public ServerPing build() { return new ServerPing(version, nullOutPlayers ? null : new Players(onlinePlayers, maximumPlayers, samplePlayers), description, favicon, - nullOutModinfo ? null : new Modinfo(mods)); + nullOutModinfo ? null : new ModInfo(modType, mods)); } public Version getVersion() { @@ -185,6 +192,10 @@ public class ServerPing { return favicon; } + public String getModType() { + return modType; + } + public List getMods() { return mods; } @@ -196,6 +207,7 @@ public class ServerPing { ", onlinePlayers=" + onlinePlayers + ", maximumPlayers=" + maximumPlayers + ", samplePlayers=" + samplePlayers + + ", modType=" + modType + ", mods=" + mods + ", description=" + description + ", favicon=" + favicon + @@ -290,15 +302,24 @@ public class ServerPing { } } - public static class Modinfo { - public static final Modinfo DEFAULT = new Modinfo(ImmutableList.of()); + public static class ModInfo { + public static final ModInfo DEFAULT = new ModInfo("FML", ImmutableList.of()); - private final String type = "FML"; + private final String type; private final List modList; - public Modinfo(List modList) { + public ModInfo(String type, List modList) { + this.type = Preconditions.checkNotNull(type, "type"); this.modList = ImmutableList.copyOf(modList); } + + public String getType() { + return type; + } + + public List getMods() { + return modList; + } } public static class Mod { @@ -309,5 +330,13 @@ public class ServerPing { this.id = Preconditions.checkNotNull(id, "id"); this.version = Preconditions.checkNotNull(version, "version"); } + + public String getId() { + return id; + } + + public String getVersion() { + return version; + } } } diff --git a/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java b/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java index fd652d1c6..2854a664c 100644 --- a/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java +++ b/api/src/main/java/com/velocitypowered/api/scheduler/Scheduler.java @@ -24,7 +24,7 @@ public interface Scheduler { * @param unit the unit of time for {@code time} * @return this builder, for chaining */ - TaskBuilder delay(int time, TimeUnit unit); + TaskBuilder delay(long time, TimeUnit unit); /** * Specifies that the task should continue running after waiting for the specified amount, until it is cancelled. @@ -32,7 +32,7 @@ public interface Scheduler { * @param unit the unit of time for {@code time} * @return this builder, for chaining */ - TaskBuilder repeat(int time, TimeUnit unit); + TaskBuilder repeat(long time, TimeUnit unit); /** * Clears the delay on this task. diff --git a/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java b/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java deleted file mode 100644 index a21b97ffd..000000000 --- a/api/src/main/java/com/velocitypowered/api/util/LegacyChatColorUtils.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.velocitypowered.api.util; - -import com.google.common.base.Preconditions; -import org.checkerframework.checker.nullness.qual.NonNull; - -import java.util.regex.Pattern; - -/** - * LegacyChatColorUtils contains utilities for handling legacy Minecraft color codes. Generally, you should prefer - * JSON-based components, but for convenience Velocity provides a limited set of tools to handle Minecraft color codes. - */ -public class LegacyChatColorUtils { - private LegacyChatColorUtils() { - throw new AssertionError(); - } - - /** - * Represents the legacy Minecraft format character, the section symbol. - */ - public static final char FORMAT_CHAR = '\u00a7'; - - /** - * Translates a string with Minecraft color codes prefixed with a different character than the section symbol into - * a string that uses the section symbol. - * @param originalChar the char the color codes are prefixed by - * @param text the text to translate - * @return the translated text - */ - public static String translate(char originalChar, @NonNull String text) { - Preconditions.checkNotNull(text, "text"); - char[] textChars = text.toCharArray(); - int foundSectionIdx = -1; - for (int i = 0; i < textChars.length; i++) { - char textChar = textChars[i]; - if (textChar == originalChar) { - foundSectionIdx = i; - continue; - } - - if (foundSectionIdx >= 0) { - textChar = Character.toLowerCase(textChar); - if ((textChar >= 'a' && textChar <= 'f') || (textChar >= '0' && textChar <= '9') || - (textChar >= 'l' && textChar <= 'o' || textChar == 'r')) { - textChars[foundSectionIdx] = FORMAT_CHAR; - } - foundSectionIdx = -1; - } - } - return new String(textChars); - } - - /** - * A regex that matches all Minecraft color codes and removes them. - */ - private static final Pattern CHAT_COLOR_MATCHER = Pattern.compile("(?i)" + Character.toString(FORMAT_CHAR) + "[0-9A-FL-OR]"); - - /** - * Removes all Minecraft color codes from the string. - * @param text the text to remove color codes from - * @return a new String without Minecraft color codes - */ - public static String removeFormatting(@NonNull String text) { - Preconditions.checkNotNull(text, "text"); - return CHAT_COLOR_MATCHER.matcher(text).replaceAll(""); - } -} diff --git a/api/src/main/java/com/velocitypowered/api/util/title/TextTitle.java b/api/src/main/java/com/velocitypowered/api/util/title/TextTitle.java new file mode 100644 index 000000000..53a84fe45 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/title/TextTitle.java @@ -0,0 +1,236 @@ +package com.velocitypowered.api.util.title; + +import com.google.common.base.Preconditions; +import net.kyori.text.Component; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.Objects; +import java.util.Optional; + +/** + * Represents a "full" title, including all components. This class is immutable. + */ +public class TextTitle implements Title { + private final Component title; + private final Component subtitle; + private final int stay; + private final int fadeIn; + private final int fadeOut; + private final boolean resetBeforeSend; + + private TextTitle(Builder builder) { + this.title = builder.title; + this.subtitle = builder.subtitle; + this.stay = builder.stay; + this.fadeIn = builder.fadeIn; + this.fadeOut = builder.fadeOut; + this.resetBeforeSend = builder.resetBeforeSend; + } + + /** + * Returns the main title this title has, if any. + * @return the main title of this title + */ + public Optional getTitle() { + return Optional.ofNullable(title); + } + + /** + * Returns the subtitle this title has, if any. + * @return the subtitle + */ + public Optional getSubtitle() { + return Optional.ofNullable(subtitle); + } + + /** + * Returns the number of ticks this title will stay up. + * @return how long the title will stay, in ticks + */ + public int getStay() { + return stay; + } + + /** + * Returns the number of ticks over which this title will fade in. + * @return how long the title will fade in, in ticks + */ + public int getFadeIn() { + return fadeIn; + } + + /** + * Returns the number of ticks over which this title will fade out. + * @return how long the title will fade out, in ticks + */ + public int getFadeOut() { + return fadeOut; + } + + /** + * Returns whether or not a reset packet will be sent before this title is sent. By default, unless explicitly + * disabled, this is enabled by default. + * @return whether or not a reset packet will be sent before this title is sent + */ + public boolean isResetBeforeSend() { + return resetBeforeSend; + } + + /** + * Determines whether or not this title has times set on it. If none are set, it will update the previous title + * set on the client. + * @return whether or not this title has times set on it + */ + public boolean areTimesSet() { + return stay != 0 || fadeIn != 0 || fadeOut != 0; + } + + /** + * Creates a new builder from the contents of this title so that it may be changed. + * @return a builder instance with the contents of this title + */ + public Builder toBuilder() { + return new Builder(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TextTitle textTitle = (TextTitle) o; + return stay == textTitle.stay && + fadeIn == textTitle.fadeIn && + fadeOut == textTitle.fadeOut && + resetBeforeSend == textTitle.resetBeforeSend && + Objects.equals(title, textTitle.title) && + Objects.equals(subtitle, textTitle.subtitle); + } + + @Override + public String toString() { + return "TextTitle{" + + "title=" + title + + ", subtitle=" + subtitle + + ", stay=" + stay + + ", fadeIn=" + fadeIn + + ", fadeOut=" + fadeOut + + ", resetBeforeSend=" + resetBeforeSend + + '}'; + } + + @Override + public int hashCode() { + return Objects.hash(title, subtitle, stay, fadeIn, fadeOut, resetBeforeSend); + } + + /** + * Creates a new builder for constructing titles. + * @return a builder for constructing titles + */ + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private @Nullable Component title; + private @Nullable Component subtitle; + private int stay; + private int fadeIn; + private int fadeOut; + private boolean resetBeforeSend = true; + + private Builder() {} + + private Builder(TextTitle copy) { + this.title = copy.title; + this.subtitle = copy.subtitle; + this.stay = copy.stay; + this.fadeIn = copy.fadeIn; + this.fadeOut = copy.fadeOut; + this.resetBeforeSend = copy.resetBeforeSend; + } + + public Builder title(Component title) { + this.title = Preconditions.checkNotNull(title, "title"); + return this; + } + + public Builder clearTitle() { + this.title = null; + return this; + } + + public Builder subtitle(Component subtitle) { + this.subtitle = Preconditions.checkNotNull(subtitle, "subtitle"); + return this; + } + + public Builder clearSubtitle() { + this.subtitle = null; + return this; + } + + public Builder stay(int ticks) { + Preconditions.checkArgument(ticks >= 0, "ticks value %s is negative", ticks); + this.stay = ticks; + return this; + } + + public Builder fadeIn(int ticks) { + Preconditions.checkArgument(ticks >= 0, "ticks value %s is negative", ticks); + this.fadeIn = ticks; + return this; + } + + public Builder fadeOut(int ticks) { + Preconditions.checkArgument(ticks >= 0, "ticks value %s is negative", ticks); + this.fadeOut = ticks; + return this; + } + + public Builder resetBeforeSend(boolean b) { + this.resetBeforeSend = b; + return this; + } + + public Component getTitle() { + return title; + } + + public Component getSubtitle() { + return subtitle; + } + + public int getStay() { + return stay; + } + + public int getFadeIn() { + return fadeIn; + } + + public int getFadeOut() { + return fadeOut; + } + + public boolean isResetBeforeSend() { + return resetBeforeSend; + } + + public TextTitle build() { + return new TextTitle(this); + } + + @Override + public String toString() { + return "Builder{" + + "title=" + title + + ", subtitle=" + subtitle + + ", stay=" + stay + + ", fadeIn=" + fadeIn + + ", fadeOut=" + fadeOut + + ", resetBeforeSend=" + resetBeforeSend + + '}'; + } + } +} diff --git a/api/src/main/java/com/velocitypowered/api/util/title/Title.java b/api/src/main/java/com/velocitypowered/api/util/title/Title.java new file mode 100644 index 000000000..6849e38a4 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/title/Title.java @@ -0,0 +1,7 @@ +package com.velocitypowered.api.util.title; + +/** + * Represents a title that can be sent to a Minecraft client. + */ +public interface Title { +} diff --git a/api/src/main/java/com/velocitypowered/api/util/title/Titles.java b/api/src/main/java/com/velocitypowered/api/util/title/Titles.java new file mode 100644 index 000000000..242a04411 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/title/Titles.java @@ -0,0 +1,50 @@ +package com.velocitypowered.api.util.title; + +/** + * Provides special-purpose titles. + */ +public class Titles { + private Titles() { + throw new AssertionError(); + } + + private static final Title RESET = new Title() { + @Override + public String toString() { + return "reset title"; + } + }; + + private static final Title HIDE = new Title() { + @Override + public String toString() { + return "hide title"; + } + }; + + /** + * Returns a title that, when sent to the client, will cause all title data to be reset and any existing title to be + * hidden. + * @return the reset title + */ + public static Title reset() { + return RESET; + } + + /** + * Returns a title that, when sent to the client, will cause any existing title to be hidden. The title may be + * restored by a {@link TextTitle} with no title or subtitle (only a time). + * @return the hide title + */ + public static Title hide() { + return HIDE; + } + + /** + * Returns a builder for {@link TextTitle}s. + * @return a builder for text titles + */ + public static TextTitle.Builder text() { + return TextTitle.builder(); + } +} diff --git a/api/src/main/java/com/velocitypowered/api/util/title/package-info.java b/api/src/main/java/com/velocitypowered/api/util/title/package-info.java new file mode 100644 index 000000000..467f57ab6 --- /dev/null +++ b/api/src/main/java/com/velocitypowered/api/util/title/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides data structures for creating and manipulating titles. + */ +package com.velocitypowered.api.util.title; \ No newline at end of file diff --git a/api/src/test/java/com/velocitypowered/api/util/LegacyChatColorUtilsTest.java b/api/src/test/java/com/velocitypowered/api/util/LegacyChatColorUtilsTest.java deleted file mode 100644 index b98b7027b..000000000 --- a/api/src/test/java/com/velocitypowered/api/util/LegacyChatColorUtilsTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.velocitypowered.api.util; - -import com.velocitypowered.api.util.LegacyChatColorUtils; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class LegacyChatColorUtilsTest { - private static final String NON_FORMATTED = "Velocity"; - private static final String FORMATTED = "\u00a7cVelocity"; - private static final String FORMATTED_MULTIPLE = "\u00a7c\u00a7lVelocity"; - private static final String FORMATTED_MULTIPLE_VARIED = "\u00a7c\u00a7lVelo\u00a7a\u00a7mcity"; - private static final String INVALID = "\u00a7gVelocity"; - private static final String RAW_SECTION = "\u00a7"; - - @Test - void removeFormattingNonFormatted() { - assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(NON_FORMATTED)); - } - - @Test - void removeFormattingFormatted() { - assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(FORMATTED)); - } - - @Test - void removeFormattingFormattedMultiple() { - assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(FORMATTED_MULTIPLE)); - } - - @Test - void removeFormattingFormattedMultipleVaried() { - assertEquals(NON_FORMATTED, LegacyChatColorUtils.removeFormatting(FORMATTED_MULTIPLE_VARIED)); - } - - @Test - void removeFormattingInvalidFormat() { - assertEquals(INVALID, LegacyChatColorUtils.removeFormatting(INVALID)); - } - - @Test - void removeFormattingRawSection() { - assertEquals(RAW_SECTION, LegacyChatColorUtils.removeFormatting(RAW_SECTION)); - } - - @Test - void translate() { - assertEquals(FORMATTED, LegacyChatColorUtils.translate('&', "&cVelocity")); - } - - @Test - void translateMultiple() { - assertEquals(FORMATTED_MULTIPLE, LegacyChatColorUtils.translate('&', "&c&lVelocity")); - assertEquals(FORMATTED_MULTIPLE_VARIED, LegacyChatColorUtils.translate('&', "&c&lVelo&a&mcity")); - } - - @Test - void translateDifferentChar() { - assertEquals(FORMATTED, LegacyChatColorUtils.translate('$', "$cVelocity")); - assertEquals(FORMATTED_MULTIPLE_VARIED, LegacyChatColorUtils.translate('$', "$c$lVelo$a$mcity")); - } -} \ No newline at end of file diff --git a/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java b/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java index 2f07378c8..4b84516da 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java +++ b/native/src/main/java/com/velocitypowered/natives/util/NativeCodeLoader.java @@ -8,7 +8,7 @@ import java.util.function.Supplier; public class NativeCodeLoader implements Supplier { private final List> variants; - private Variant selected; + private volatile Variant selected; public NativeCodeLoader(List> variants) { this.variants = ImmutableList.copyOf(variants); @@ -16,36 +16,41 @@ public class NativeCodeLoader implements Supplier { @Override public T get() { - if (selected == null) { - selected = select(); - } - return selected.object; + return tryLoad().object; } - private Variant select() { - for (Variant variant : variants) { - T got = variant.get(); - if (got == null) { - continue; - } - return variant; + private Variant tryLoad() { + if (selected != null) { + return selected; + } + + synchronized (this) { + if (selected != null) { + return selected; + } + + for (Variant variant : variants) { + T got = variant.get(); + if (got == null) { + continue; + } + selected = variant; + return selected; + } + throw new IllegalArgumentException("Can't find any suitable variants"); } - throw new IllegalArgumentException("Can't find any suitable variants"); } public String getLoadedVariant() { - if (selected == null) { - selected = select(); - } - return selected.name; + return tryLoad().name; } static class Variant { - private boolean available; + private volatile boolean available; private final Runnable setup; private final String name; private final T object; - private boolean hasBeenSetup = false; + private volatile boolean hasBeenSetup = false; Variant(BooleanSupplier available, Runnable setup, String name, T object) { this.available = available.getAsBoolean(); @@ -54,27 +59,34 @@ public class NativeCodeLoader implements Supplier { this.object = object; } - private void setup() { - if (available && !hasBeenSetup) { - try { - setup.run(); - hasBeenSetup = true; - } catch (Exception e) { - available = false; + public T get() { + if (!available) { + return null; + } + + // Make sure setup happens only once + if (!hasBeenSetup) { + synchronized (this) { + // We change availability if need be below, may as well check it again here for sanity. + if (!available) { + return null; + } + + // Okay, now try the setup if we haven't done so yet. + if (!hasBeenSetup) { + try { + setup.run(); + hasBeenSetup = true; + return object; + } catch (Exception e) { + available = false; + return null; + } + } } } - } - public T get() { - if (!hasBeenSetup) { - setup(); - } - - if (available) { - return object; - } - - return null; + return object; } } diff --git a/native/src/main/java/com/velocitypowered/natives/util/Natives.java b/native/src/main/java/com/velocitypowered/natives/util/Natives.java index 9c135843d..5c22ae752 100644 --- a/native/src/main/java/com/velocitypowered/natives/util/Natives.java +++ b/native/src/main/java/com/velocitypowered/natives/util/Natives.java @@ -3,10 +3,8 @@ package com.velocitypowered.natives.util; import com.google.common.collect.ImmutableList; import com.velocitypowered.natives.compression.JavaVelocityCompressor; import com.velocitypowered.natives.compression.NativeVelocityCompressor; -import com.velocitypowered.natives.compression.VelocityCompressor; import com.velocitypowered.natives.compression.VelocityCompressorFactory; import com.velocitypowered.natives.encryption.JavaVelocityCipher; -import com.velocitypowered.natives.encryption.NativeVelocityCipher; import com.velocitypowered.natives.encryption.VelocityCipherFactory; import java.io.IOException; diff --git a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java index 93136b8df..363c80442 100644 --- a/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java +++ b/native/src/test/java/com/velocitypowered/natives/compression/VelocityCompressorTest.java @@ -9,7 +9,6 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import java.util.Random; -import java.util.function.Supplier; import java.util.zip.DataFormatException; import java.util.zip.Deflater; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java index c628775c4..f2f05f49f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/Velocity.java @@ -1,6 +1,5 @@ package com.velocitypowered.proxy; -import com.velocitypowered.proxy.console.VelocityConsole; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -28,6 +27,6 @@ public class Velocity { double bootTime = (System.currentTimeMillis() - startTime) / 1000d; logger.info("Done ({}s)!", new DecimalFormat("#.##").format(bootTime)); - new VelocityConsole(server).start(); + server.getConsoleCommandSource().start(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index d23709111..4fa54cdfe 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -4,40 +4,43 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; +import com.velocitypowered.api.permission.Tristate; +import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.api.util.Favicon; -import com.velocitypowered.api.plugin.PluginManager; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; -import com.velocitypowered.proxy.network.ConnectionManager; +import com.velocitypowered.api.util.Favicon; import com.velocitypowered.proxy.command.ServerCommand; import com.velocitypowered.proxy.command.ShutdownCommand; import com.velocitypowered.proxy.command.VelocityCommand; -import com.velocitypowered.proxy.config.VelocityConfiguration; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.network.http.NettyHttpClient; import com.velocitypowered.proxy.command.VelocityCommandManager; import com.velocitypowered.proxy.config.AnnotatedConfig; +import com.velocitypowered.proxy.config.VelocityConfiguration; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.console.VelocityConsole; import com.velocitypowered.proxy.messages.VelocityChannelRegistrar; +import com.velocitypowered.proxy.network.ConnectionManager; +import com.velocitypowered.proxy.network.http.NettyHttpClient; import com.velocitypowered.proxy.plugin.VelocityEventManager; -import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.plugin.VelocityPluginManager; +import com.velocitypowered.proxy.protocol.packet.Chat; +import com.velocitypowered.proxy.protocol.util.FaviconSerializer; import com.velocitypowered.proxy.scheduler.VelocityScheduler; +import com.velocitypowered.proxy.server.ServerMap; import com.velocitypowered.proxy.util.AddressUtil; import com.velocitypowered.proxy.util.EncryptionUtils; import com.velocitypowered.proxy.util.Ratelimiter; -import com.velocitypowered.proxy.util.ServerMap; import io.netty.bootstrap.Bootstrap; import net.kyori.text.Component; import net.kyori.text.TextComponent; -import net.kyori.text.serializer.ComponentSerializers; import net.kyori.text.serializer.GsonComponentSerializer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; import java.net.InetSocketAddress; import java.nio.file.Files; @@ -61,7 +64,7 @@ public class VelocityServer implements ProxyServer { private VelocityConfiguration configuration; private NettyHttpClient httpClient; private KeyPair serverKeyPair; - private final ServerMap servers = new ServerMap(); + private final ServerMap servers = new ServerMap(this); private final VelocityCommandManager commandManager = new VelocityCommandManager(); private final AtomicBoolean shutdownInProgress = new AtomicBoolean(false); private boolean shutdown = false; @@ -69,17 +72,7 @@ public class VelocityServer implements ProxyServer { private final Map connectionsByUuid = new ConcurrentHashMap<>(); private final Map connectionsByName = new ConcurrentHashMap<>(); - private final CommandSource consoleCommandSource = new CommandSource() { - @Override - public void sendMessage(Component component) { - logger.info(ComponentSerializers.LEGACY.serialize(component)); - } - - @Override - public boolean hasPermission(String permission) { - return true; - } - }; + private final VelocityConsole console = new VelocityConsole(this); private Ratelimiter ipAttemptLimiter; private VelocityEventManager eventManager; private VelocityScheduler scheduler; @@ -144,6 +137,9 @@ public class VelocityServer implements ProxyServer { // issues) and there is almost no chance ExecutionException will be thrown. } + // init console permissions after plugins are loaded + console.setupPermissions(); + this.cm.bind(configuration.getBind()); if (configuration.isQueryEnabled()) { @@ -252,6 +248,15 @@ public class VelocityServer implements ProxyServer { return Optional.ofNullable(connectionsByUuid.get(uuid)); } + @Override + public void broadcast(Component component) { + Preconditions.checkNotNull(component, "component"); + Chat chat = Chat.createClientbound(component); + for (ConnectedPlayer player : connectionsByUuid.values()) { + player.getConnection().write(chat); + } + } + @Override public Collection getAllPlayers() { return ImmutableList.copyOf(connectionsByUuid.values()); @@ -263,19 +268,19 @@ public class VelocityServer implements ProxyServer { } @Override - public Optional getServerInfo(String name) { + public Optional getServer(String name) { Preconditions.checkNotNull(name, "name"); return servers.getServer(name); } @Override - public Collection getAllServers() { + public Collection getAllServers() { return servers.getAllServers(); } @Override - public void registerServer(ServerInfo server) { - servers.register(server); + public RegisteredServer registerServer(ServerInfo server) { + return servers.register(server); } @Override @@ -284,8 +289,8 @@ public class VelocityServer implements ProxyServer { } @Override - public CommandSource getConsoleCommandSource() { - return consoleCommandSource; + public VelocityConsole getConsoleCommandSource() { + return console; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java index b42a9ed26..b466ae5f7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ServerCommand.java @@ -3,14 +3,17 @@ package com.velocitypowered.proxy.command; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.command.Command; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.permission.Tristate; 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.api.proxy.server.ServerInfo; import net.kyori.text.TextComponent; import net.kyori.text.event.ClickEvent; import net.kyori.text.event.HoverEvent; import net.kyori.text.format.TextColor; +import org.checkerframework.checker.nullness.qual.NonNull; import java.util.List; import java.util.Optional; @@ -34,7 +37,7 @@ public class ServerCommand implements Command { if (args.length == 1) { // Trying to connect to a server. String serverName = args[0]; - Optional toConnect = server.getServerInfo(serverName); + Optional toConnect = server.getServer(serverName); if (!toConnect.isPresent()) { player.sendMessage(TextComponent.of("Server " + serverName + " doesn't exist.", TextColor.RED)); return; @@ -48,17 +51,19 @@ public class ServerCommand implements Command { // Assemble the list of servers as components TextComponent.Builder serverListBuilder = TextComponent.builder("Available servers: ").color(TextColor.YELLOW); - List infos = ImmutableList.copyOf(server.getAllServers()); + List infos = ImmutableList.copyOf(server.getAllServers()); for (int i = 0; i < infos.size(); i++) { - ServerInfo serverInfo = infos.get(i); - TextComponent infoComponent = TextComponent.of(serverInfo.getName()); - if (serverInfo.getName().equals(currentServer)) { + RegisteredServer rs = infos.get(i); + TextComponent infoComponent = TextComponent.of(rs.getServerInfo().getName()); + String playersText = rs.getPlayersConnected().size() + " player(s) online"; + if (rs.getServerInfo().getName().equals(currentServer)) { infoComponent = infoComponent.color(TextColor.GREEN) - .hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Currently connected to this server"))); + .hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + TextComponent.of("Currently connected to this server\n" + playersText))); } else { infoComponent = infoComponent.color(TextColor.GRAY) - .clickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/server " + serverInfo.getName())) - .hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to connect to this server"))); + .clickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/server " + rs.getServerInfo().getName())) + .hoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, TextComponent.of("Click to connect to this server\n" + playersText))); } serverListBuilder.append(infoComponent); if (i != infos.size() - 1) { @@ -74,15 +79,20 @@ public class ServerCommand implements Command { public List suggest(CommandSource source, String[] currentArgs) { if (currentArgs.length == 0) { return server.getAllServers().stream() - .map(ServerInfo::getName) + .map(rs -> rs.getServerInfo().getName()) .collect(Collectors.toList()); } else if (currentArgs.length == 1) { return server.getAllServers().stream() - .map(ServerInfo::getName) + .map(rs -> rs.getServerInfo().getName()) .filter(name -> name.regionMatches(true, 0, currentArgs[0], 0, currentArgs[0].length())) .collect(Collectors.toList()); } else { return ImmutableList.of(); } } + + @Override + public boolean hasPermission(@NonNull CommandSource source, @NonNull String[] args) { + return source.getPermissionValue("velocity.command.server") != Tristate.FALSE; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java index 7f60c81ff..210a9a026 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/ShutdownCommand.java @@ -5,6 +5,7 @@ import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.proxy.VelocityServer; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; +import org.checkerframework.checker.nullness.qual.NonNull; public class ShutdownCommand implements Command { private final VelocityServer server; @@ -21,4 +22,9 @@ public class ShutdownCommand implements Command { } server.shutdown(); } + + @Override + public boolean hasPermission(@NonNull CommandSource source, @NonNull String[] args) { + return source == server.getConsoleCommandSource(); + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java index f3acb9caf..c8b72dbcd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommand.java @@ -2,10 +2,12 @@ package com.velocitypowered.proxy.command; import com.velocitypowered.api.command.Command; import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.proxy.VelocityServer; import net.kyori.text.TextComponent; import net.kyori.text.event.ClickEvent; import net.kyori.text.format.TextColor; +import org.checkerframework.checker.nullness.qual.NonNull; public class VelocityCommand implements Command { @Override @@ -37,4 +39,9 @@ public class VelocityCommand implements Command { source.sendMessage(velocityInfo); source.sendMessage(velocityWebsite); } + + @Override + public boolean hasPermission(@NonNull CommandSource source, @NonNull String[] args) { + return source.getPermissionValue("velocity.command.info") != Tristate.FALSE; + } } 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 38c3759ea..1b952e1dd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -1,10 +1,9 @@ package com.velocitypowered.proxy.command; import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; import com.velocitypowered.api.command.Command; -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.CommandManager; +import com.velocitypowered.api.command.CommandSource; import java.util.*; import java.util.stream.Collectors; @@ -47,6 +46,10 @@ public class VelocityCommandManager implements CommandManager { } try { + if (!command.hasPermission(source, actualArgs)) { + return false; + } + command.execute(source, actualArgs); return true; } catch (Exception e) { @@ -63,23 +66,29 @@ public class VelocityCommandManager implements CommandManager { return Optional.empty(); } - String command = split[0]; + String alias = split[0]; if (split.length == 1) { - return Optional.of(commands.keySet().stream() - .filter(cmd -> cmd.regionMatches(true, 0, command, 0, command.length())) + return Optional.of(commands.entrySet().stream() + .filter(ent -> ent.getKey().regionMatches(true, 0, alias, 0, alias.length())) + .filter(ent -> ent.getValue().hasPermission(source, new String[0])) + .map(ent -> "/" + ent.getKey()) .collect(Collectors.toList())); } String[] actualArgs = Arrays.copyOfRange(split, 1, split.length); - Command executor = commands.get(command); - if (executor == null) { + Command command = commands.get(alias.toLowerCase(Locale.ENGLISH)); + if (command == null) { return Optional.empty(); } try { - return Optional.of(executor.suggest(source, actualArgs)); + if (!command.hasPermission(source, actualArgs)) { + return Optional.empty(); + } + + return Optional.of(command.suggest(source, actualArgs)); } catch (Exception e) { - throw new RuntimeException("Unable to invoke suggestions for command " + command + " for " + source, e); + throw new RuntimeException("Unable to invoke suggestions for command " + alias + " for " + source, e); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/AnnotatedConfig.java b/proxy/src/main/java/com/velocitypowered/proxy/config/AnnotatedConfig.java index 32f9a4880..99a417846 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/AnnotatedConfig.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/AnnotatedConfig.java @@ -1,5 +1,8 @@ package com.velocitypowered.proxy.config; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import java.io.File; import java.io.IOException; import java.lang.annotation.ElementType; @@ -8,18 +11,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; -import java.nio.file.AtomicMoveNotSupportedException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; +import java.nio.file.*; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; /** * Only for simple configs diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java index 470e064f8..303dbcd3f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -5,10 +5,9 @@ import com.google.common.collect.ImmutableMap; import com.moandjiezana.toml.Toml; import com.velocitypowered.api.util.Favicon; import com.velocitypowered.proxy.util.AddressUtil; -import com.velocitypowered.api.util.LegacyChatColorUtils; -import io.netty.buffer.ByteBufUtil; import net.kyori.text.Component; import net.kyori.text.serializer.ComponentSerializers; +import org.apache.logging.log4j.Logger; import java.io.IOException; import java.io.Reader; @@ -17,12 +16,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import org.apache.logging.log4j.Logger; +import java.util.*; public class VelocityConfiguration extends AnnotatedConfig { @@ -273,6 +267,14 @@ public class VelocityConfiguration extends AnnotatedConfig { return announceForge; } + public int getConnectTimeout() { + return advanced.getConnectionTimeout(); + } + + public int getReadTimeout() { + return advanced.getReadTimeout(); + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -420,21 +422,23 @@ public class VelocityConfiguration extends AnnotatedConfig { "Disable by setting to 0"}) @ConfigKey("login-ratelimit") private int loginRatelimit = 3000; + @Comment({"Specify a custom timeout for connection timeouts here. The default is five seconds."}) + @ConfigKey("connection-timeout") + private int connectionTimeout = 5000; + @Comment({"Specify a read timeout for connections here. The default is 30 seconds."}) + @ConfigKey("read-timeout") + private int readTimeout = 30000; private Advanced() { } - private Advanced(int compressionThreshold, int compressionLevel, int loginRatelimit) { - this.compressionThreshold = compressionThreshold; - this.compressionLevel = compressionLevel; - this.loginRatelimit = loginRatelimit; - } - private Advanced(Toml toml) { if (toml != null) { this.compressionThreshold = toml.getLong("compression-threshold", 1024L).intValue(); this.compressionLevel = toml.getLong("compression-level", -1L).intValue(); this.loginRatelimit = toml.getLong("login-ratelimit", 3000L).intValue(); + this.connectionTimeout = toml.getLong("connection-timeout", 5000L).intValue(); + this.readTimeout = toml.getLong("read-timeout", 30000L).intValue(); } } @@ -442,33 +446,31 @@ public class VelocityConfiguration extends AnnotatedConfig { return compressionThreshold; } - public void setCompressionThreshold(int compressionThreshold) { - this.compressionThreshold = compressionThreshold; - } - public int getCompressionLevel() { return compressionLevel; } - public void setCompressionLevel(int compressionLevel) { - this.compressionLevel = compressionLevel; - } - public int getLoginRatelimit() { return loginRatelimit; } - public void setLoginRatelimit(int loginRatelimit) { - this.loginRatelimit = loginRatelimit; + public int getConnectionTimeout() { + return connectionTimeout; + } + + public int getReadTimeout() { + return readTimeout; } @Override public String toString() { - return "Advanced{" - + "compressionThreshold=" + compressionThreshold - + ", compressionLevel=" + compressionLevel - + ", loginRatelimit=" + loginRatelimit - + '}'; + return "Advanced{" + + "compressionThreshold=" + compressionThreshold + + ", compressionLevel=" + compressionLevel + + ", loginRatelimit=" + loginRatelimit + + ", connectionTimeout=" + connectionTimeout + + ", readTimeout=" + readTimeout + + '}'; } } @@ -500,18 +502,10 @@ public class VelocityConfiguration extends AnnotatedConfig { return queryEnabled; } - public void setQueryEnabled(boolean queryEnabled) { - this.queryEnabled = queryEnabled; - } - public int getQueryPort() { return queryPort; } - public void setQueryPort(int queryPort) { - this.queryPort = queryPort; - } - @Override public String toString() { return "Query{" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index b4a68dfbf..6f57fd2ba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -2,13 +2,13 @@ package com.velocitypowered.proxy.connection; import com.google.common.base.Preconditions; import com.velocitypowered.natives.compression.VelocityCompressor; +import com.velocitypowered.natives.encryption.VelocityCipher; import com.velocitypowered.natives.encryption.VelocityCipherFactory; import com.velocitypowered.natives.util.Natives; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.natives.encryption.VelocityCipher; import com.velocitypowered.proxy.protocol.netty.*; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; @@ -21,17 +21,9 @@ import org.apache.logging.log4j.Logger; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; - import java.security.GeneralSecurityException; -import static com.velocitypowered.proxy.network.Connections.CIPHER_DECODER; -import static com.velocitypowered.proxy.network.Connections.CIPHER_ENCODER; -import static com.velocitypowered.proxy.network.Connections.COMPRESSION_DECODER; -import static com.velocitypowered.proxy.network.Connections.COMPRESSION_ENCODER; -import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; -import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; +import static com.velocitypowered.proxy.network.Connections.*; /** * A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft @@ -99,8 +91,6 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (association != null) { logger.error("{}: exception encountered", association, cause); - } else { - logger.error("{} encountered an exception", ctx.channel().remoteAddress(), cause); } ctx.close(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java index b6956336b..b524b31e1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/VelocityConstants.java @@ -8,6 +8,8 @@ public class VelocityConstants { public static final String VELOCITY_IP_FORWARDING_CHANNEL = "velocity:player_info"; public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS"; + public static final String FORGE_LEGACY_CHANNEL = "FML"; + public static final String FORGE_MULTIPART_LEGACY_CHANNEL = "FML|MP"; public static final byte[] FORGE_LEGACY_HANDSHAKE_RESET_DATA = new byte[] { -2, 0 }; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 1a011a2e8..35ef63d92 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -1,15 +1,16 @@ package com.velocitypowered.proxy.connection.backend; +import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.ServerConnectedEvent; -import com.velocitypowered.api.proxy.messages.ChannelSide; -import com.velocitypowered.api.proxy.messages.MessageHandler; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; +import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; @@ -24,7 +25,8 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { - server.getEventManager().fireAndForget(new ServerConnectedEvent(connection.getPlayer(), connection.getServerInfo())); + server.getEventManager().fireAndForget(new ServerConnectedEvent(connection.getPlayer(), connection.getServer())); + connection.getServer().addPlayer(connection.getPlayer()); } @Override @@ -32,7 +34,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { if (!connection.getPlayer().isActive()) { // Connection was left open accidentally. Close it so as to avoid "You logged in from another location" // errors. - connection.getMinecraftConnection().close(); + connection.disconnect(); return; } @@ -40,11 +42,12 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { (ClientPlaySessionHandler) connection.getPlayer().getConnection().getSessionHandler(); if (packet instanceof KeepAlive) { // Forward onto the player - playerHandler.setLastPing(((KeepAlive) packet).getRandomId()); + connection.setLastPingId(((KeepAlive) packet).getRandomId()); connection.getPlayer().getConnection().write(packet); } else if (packet instanceof Disconnect) { Disconnect original = (Disconnect) packet; - connection.getPlayer().handleConnectionException(connection.getServerInfo(), original); + connection.disconnect(); + connection.getPlayer().handleConnectionException(connection.getServer(), original); } else if (packet instanceof JoinGame) { playerHandler.handleBackendJoinGame((JoinGame) packet); } else if (packet instanceof BossBar) { @@ -83,11 +86,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return; } - MessageHandler.ForwardStatus status = server.getChannelRegistrar().handlePluginMessage(connection, - ChannelSide.FROM_SERVER, pm); - if (status == MessageHandler.ForwardStatus.FORWARD) { + ChannelIdentifier id = server.getChannelRegistrar().getFromId(pm.getChannel()); + if (id == null) { connection.getPlayer().getConnection().write(pm); + } else { + PluginMessageEvent event = new PluginMessageEvent(connection, connection.getPlayer(), id, pm.getData()); + server.getEventManager().fire(event) + .thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed()) { + connection.getPlayer().getConnection().write(pm); + } + }, connection.getMinecraftConnection().getChannel().eventLoop()); } + } else if (packet instanceof TabCompleteResponse) { + playerHandler.handleTabCompleteResponse((TabCompleteResponse) packet); } else if (connection.hasCompletedJoin()) { // Just forward the packet on. We don't have anything to handle at this time. connection.getPlayer().getConnection().write(packet); @@ -99,7 +111,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { if (!connection.getPlayer().isActive()) { // Connection was left open accidentally. Close it so as to avoid "You logged in from another location" // errors. - connection.getMinecraftConnection().close(); + connection.disconnect(); return; } @@ -110,7 +122,20 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public void exception(Throwable throwable) { - connection.getPlayer().handleConnectionException(connection.getServerInfo(), throwable); + connection.getPlayer().handleConnectionException(connection.getServer(), throwable); + } + + public VelocityServer getServer() { + return server; + } + + @Override + public void disconnected() { + connection.getServer().removePlayer(connection.getPlayer()); + if (!connection.isGracefulDisconnect()) { + connection.getPlayer().handleConnectionException(connection.getServer(), Disconnect.create( + ConnectionMessages.UNEXPECTED_DISCONNECT)); + } } private boolean canForwardPluginMessage(PluginMessage message) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java index c65f1745e..5fb5341b2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java @@ -1,18 +1,18 @@ package com.velocitypowered.proxy.connection.backend; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; +import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.VelocityConfiguration; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; -import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.*; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import net.kyori.text.TextComponent; @@ -82,12 +82,6 @@ public class LoginSessionHandler implements MinecraftSessionHandler { if (existingConnection == null) { // Strap on the play session handler connection.getPlayer().getConnection().setSessionHandler(new ClientPlaySessionHandler(server, connection.getPlayer())); - - // This is for legacy Forge servers - during first connection the FML handshake will transition to complete regardless - // Thus, we need to ensure that a reset packet is ALWAYS sent on first switch. - // - // The call will handle if the player is not a Forge player appropriately. - connection.getPlayer().getConnection().setCanSendLegacyFMLResetPacket(true); } else { // The previous server connection should become obsolete. // Before we remove it, if the server we are departing is modded, we must always reset the client state. 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 ceb53b9e6..36450f1ea 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 @@ -4,9 +4,14 @@ import com.google.common.base.Preconditions; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; @@ -14,12 +19,11 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; import com.velocitypowered.proxy.protocol.packet.Handshake; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.ServerLogin; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.api.proxy.server.ServerInfo; -import com.velocitypowered.proxy.VelocityServer; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import io.netty.channel.*; +import com.velocitypowered.proxy.server.VelocityRegisteredServer; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; import io.netty.handler.timeout.ReadTimeoutHandler; import io.netty.util.AttributeKey; @@ -27,27 +31,24 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import static com.velocitypowered.proxy.VelocityServer.GSON; -import static com.velocitypowered.proxy.network.Connections.FRAME_DECODER; -import static com.velocitypowered.proxy.network.Connections.FRAME_ENCODER; -import static com.velocitypowered.proxy.network.Connections.HANDLER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_DECODER; -import static com.velocitypowered.proxy.network.Connections.MINECRAFT_ENCODER; -import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; -import static com.velocitypowered.proxy.network.Connections.SERVER_READ_TIMEOUT_SECONDS; +import static com.velocitypowered.proxy.network.Connections.*; public class VelocityServerConnection implements MinecraftConnectionAssociation, ServerConnection { static final AttributeKey> CONNECTION_NOTIFIER = AttributeKey.newInstance("connection-notification-result"); - private final ServerInfo serverInfo; + private final VelocityRegisteredServer registeredServer; private final ConnectedPlayer proxyPlayer; private final VelocityServer server; private MinecraftConnection minecraftConnection; private boolean legacyForge = false; private boolean hasCompletedJoin = false; + private boolean gracefulDisconnect = false; + private long lastPingId; + private long lastPingSent; - public VelocityServerConnection(ServerInfo target, ConnectedPlayer proxyPlayer, VelocityServer server) { - this.serverInfo = target; + public VelocityServerConnection(VelocityRegisteredServer registeredServer, ConnectedPlayer proxyPlayer, VelocityServer server) { + this.registeredServer = registeredServer; this.proxyPlayer = proxyPlayer; this.server = server; } @@ -55,12 +56,11 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, public CompletableFuture connect() { CompletableFuture result = new CompletableFuture<>(); server.initializeGenericBootstrap() - .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { ch.pipeline() - .addLast(READ_TIMEOUT, new ReadTimeoutHandler(SERVER_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS)) .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND)) @@ -73,7 +73,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, ch.pipeline().addLast(HANDLER, connection); } }) - .connect(serverInfo.getAddress()) + .connect(registeredServer.getServerInfo().getAddress()) .addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { @@ -95,7 +95,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake, // separated by \0 (the null byte). In order, you send the original host, the player's IP, their // UUID (undashed), and if you are in online-mode, their login properties (retrieved from Mojang). - return serverInfo.getAddress().getHostString() + "\0" + + return registeredServer.getServerInfo().getAddress().getHostString() + "\0" + proxyPlayer.getRemoteAddress().getHostString() + "\0" + proxyPlayer.getProfile().getId() + "\0" + GSON.toJson(proxyPlayer.getProfile().getProperties()); @@ -113,9 +113,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, } else if (proxyPlayer.getConnection().isLegacyForge()) { handshake.setServerAddress(handshake.getServerAddress() + "\0FML\0"); } else { - handshake.setServerAddress(serverInfo.getAddress().getHostString()); + handshake.setServerAddress(registeredServer.getServerInfo().getAddress().getHostString()); } - handshake.setPort(serverInfo.getAddress().getPort()); + handshake.setPort(registeredServer.getServerInfo().getAddress().getPort()); minecraftConnection.write(handshake); int protocolVersion = proxyPlayer.getConnection().getProtocolVersion(); @@ -127,12 +127,24 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, minecraftConnection.write(login); } + public void writeIfJoined(PluginMessage message) { + if (hasCompletedJoin) { + minecraftConnection.write(message); + } + } + public MinecraftConnection getMinecraftConnection() { return minecraftConnection; } + @Override + public VelocityRegisteredServer getServer() { + return registeredServer; + } + + @Override public ServerInfo getServerInfo() { - return serverInfo; + return registeredServer.getServerInfo(); } @Override @@ -141,23 +153,27 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, } public void disconnect() { - minecraftConnection.close(); - minecraftConnection = null; + if (minecraftConnection != null) { + minecraftConnection.close(); + minecraftConnection = null; + gracefulDisconnect = true; + } } @Override public String toString() { - return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + serverInfo.getName(); + return "[server connection] " + proxyPlayer.getProfile().getName() + " -> " + registeredServer.getServerInfo().getName(); } @Override - public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { Preconditions.checkNotNull(identifier, "identifier"); Preconditions.checkNotNull(data, "data"); PluginMessage message = new PluginMessage(); message.setChannel(identifier.getId()); message.setData(data); minecraftConnection.write(message); + return true; } public boolean isLegacyForge() { @@ -175,4 +191,25 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, public void setHasCompletedJoin(boolean hasCompletedJoin) { this.hasCompletedJoin = hasCompletedJoin; } + + public boolean isGracefulDisconnect() { + return gracefulDisconnect; + } + + public long getLastPingId() { + return lastPingId; + } + + public long getLastPingSent() { + return lastPingSent; + } + + public void setLastPingId(long lastPingId) { + this.lastPingId = lastPingId; + this.lastPingSent = System.currentTimeMillis(); + } + + public void resetLastPingId() { + this.lastPingId = -1; + } } 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 b7cb0f91e..aa27bc1cf 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 @@ -1,14 +1,15 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.api.event.connection.DisconnectEvent; -import com.velocitypowered.api.proxy.messages.ChannelSide; -import com.velocitypowered.api.proxy.messages.MessageHandler; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.*; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.ThrowableUtils; import io.netty.buffer.ByteBuf; @@ -28,12 +29,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private static final int MAX_PLUGIN_CHANNELS = 1024; private final ConnectedPlayer player; - private long lastPingID = -1; - private long lastPingSent = -1; private boolean spawned = false; private final List serverBossBars = new ArrayList<>(); private final Set clientPluginMsgChannels = new HashSet<>(); + private final Queue loginPluginMessages = new ArrayDeque<>(); private final VelocityServer server; + private TabCompleteRequest outstandingTabComplete; public ClientPlaySessionHandler(VelocityServer server, ConnectedPlayer player) { this.player = player; @@ -53,16 +54,22 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void handle(MinecraftPacket packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + if (packet instanceof KeepAlive) { KeepAlive keepAlive = (KeepAlive) packet; - if (keepAlive.getRandomId() != lastPingID) { + if (keepAlive.getRandomId() != serverConnection.getLastPingId()) { // The last keep alive we got was probably from a different server. Let's ignore it, and hope the next // ping is alright. return; } - player.setPing(System.currentTimeMillis() - lastPingSent); - resetPingData(); - player.getConnectedServer().getMinecraftConnection().write(packet); + player.setPing(System.currentTimeMillis() - serverConnection.getLastPingSent()); + serverConnection.getMinecraftConnection().write(packet); + serverConnection.resetLastPingId(); return; } @@ -92,31 +99,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } if (packet instanceof TabCompleteRequest) { - TabCompleteRequest req = (TabCompleteRequest) packet; - int lastSpace = req.getCommand().indexOf(' '); - if (!req.isAssumeCommand() && lastSpace != -1) { - String command = req.getCommand().substring(1); - try { - Optional> offers = server.getCommandManager().offerSuggestions(player, command); - if (offers.isPresent()) { - TabCompleteResponse response = new TabCompleteResponse(); - response.setTransactionId(req.getTransactionId()); - response.setStart(lastSpace); - response.setLength(req.getCommand().length() - lastSpace); - - for (String s : offers.get()) { - response.getOffers().add(new TabCompleteResponse.Offer(s, null)); - } - - player.getConnection().write(response); - } else { - player.getConnectedServer().getMinecraftConnection().write(packet); - } - } catch (Exception e) { - logger.error("Unable to provide tab list completions for " + player.getUsername() + " for command '" + req.getCommand() + "'", e); - } - return; - } + // Record the request so that the outstanding request can be augmented later. + outstandingTabComplete = (TabCompleteRequest) packet; + serverConnection.getMinecraftConnection().write(packet); } if (packet instanceof PluginMessage) { @@ -125,15 +110,21 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } // If we don't want to handle this packet, just forward it on. - if (player.getConnectedServer().hasCompletedJoin()) { - player.getConnectedServer().getMinecraftConnection().write(packet); + if (serverConnection.hasCompletedJoin()) { + serverConnection.getMinecraftConnection().write(packet); } } @Override public void handleUnknown(ByteBuf buf) { - if (player.getConnectedServer().hasCompletedJoin()) { - player.getConnectedServer().getMinecraftConnection().write(buf.retain()); + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + if (serverConnection.hasCompletedJoin()) { + serverConnection.getMinecraftConnection().write(buf.retain()); } } @@ -162,11 +153,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } public void handleBackendJoinGame(JoinGame joinGame) { - resetPingData(); // reset ping data; if (!spawned) { - // nothing special to do here + // Nothing special to do with regards to spawning the player spawned = true; player.getConnection().delayedWrite(joinGame); + + // We have something special to do for legacy Forge servers - during first connection the FML handshake + // will transition to complete regardless. Thus, we need to ensure that a reset packet is ALWAYS sent on + // first switch. + // + // As we know that calling this branch only happens on first join, we set that if we are a Forge + // client that we must reset on the next switch. + // + // The call will handle if the player is not a Forge player appropriately. + player.getConnection().setCanSendLegacyFMLResetPacket(true); } else { // Ah, this is the meat and potatoes of the whole venture! // @@ -210,6 +210,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { channel, toRegister)); } + // If we had plugin messages queued during login/FML handshake, send them now. + PluginMessage pm; + while ((pm = loginPluginMessages.poll()) != null) { + player.getConnectedServer().getMinecraftConnection().delayedWrite(pm); + } + + // Clear any title from the previous server. + player.getConnection().delayedWrite(TitlePacket.resetForProtocolVersion(player.getProtocolVersion())); + // Flush everything player.getConnection().flush(); player.getConnectedServer().getMinecraftConnection().flush(); @@ -234,8 +243,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return serverBossBars; } - public void handleClientPluginMessage(PluginMessage packet) { - if (packet.getChannel().equals("REGISTER") || packet.getChannel().equals("minecraft:register")) { + private void handleClientPluginMessage(PluginMessage packet) { + if (PluginMessageUtil.isMCRegister(packet)) { List actuallyRegistered = new ArrayList<>(); List channels = PluginMessageUtil.getChannels(packet); for (String channel : channels) { @@ -252,31 +261,35 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { PluginMessage newRegisterPacket = PluginMessageUtil.constructChannelsPacket(packet.getChannel(), actuallyRegistered); player.getConnectedServer().getMinecraftConnection().write(newRegisterPacket); } - - return; - } - - if (packet.getChannel().equals("UNREGISTER") || packet.getChannel().equals("minecraft:unregister")) { + } else if (PluginMessageUtil.isMCUnregister(packet)) { List channels = PluginMessageUtil.getChannels(packet); clientPluginMsgChannels.removeAll(channels); - } - - if (PluginMessageUtil.isMCBrand(packet)) { + player.getConnectedServer().getMinecraftConnection().write(packet); + } else if (PluginMessageUtil.isMCBrand(packet)) { player.getConnectedServer().getMinecraftConnection().write(PluginMessageUtil.rewriteMCBrand(packet)); - return; - } - - if (player.getConnectedServer().isLegacyForge() && !player.getConnectedServer().hasCompletedJoin()) { - // Ensure that the messages are forwarded - player.getConnectedServer().getMinecraftConnection().write(packet); - return; - } - - MessageHandler.ForwardStatus status = server.getChannelRegistrar().handlePluginMessage(player, - ChannelSide.FROM_CLIENT, packet); - if (status == MessageHandler.ForwardStatus.FORWARD) { - // We're going to forward on the original packet. - player.getConnectedServer().getMinecraftConnection().write(packet); + } else if (player.getConnectedServer().isLegacyForge() && !player.getConnectedServer().hasCompletedJoin()) { + if (packet.getChannel().equals(VelocityConstants.FORGE_LEGACY_HANDSHAKE_CHANNEL)) { + // Always forward the FML handshake to the remote server. + player.getConnectedServer().getMinecraftConnection().write(packet); + } else { + // The client is trying to send messages too early. This is primarily caused by mods, but it's further + // aggravated by Velocity. To work around these issues, we will queue any non-FML handshake messages to + // be sent once the JoinGame packet has been received by the proxy. + loginPluginMessages.add(packet); + } + } else { + ChannelIdentifier id = server.getChannelRegistrar().getFromId(packet.getChannel()); + if (id == null) { + player.getConnectedServer().getMinecraftConnection().write(packet); + } else { + PluginMessageEvent event = new PluginMessageEvent(player, player.getConnectedServer(), id, packet.getData()); + server.getEventManager().fire(event) + .thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed()) { + player.getConnectedServer().getMinecraftConnection().write(packet); + } + }, player.getConnectedServer().getMinecraftConnection().getChannel().eventLoop()); + } } } @@ -284,13 +297,20 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return clientPluginMsgChannels; } - public void setLastPing(long lastPing) { - this.lastPingID = lastPing; - this.lastPingSent = System.currentTimeMillis(); - } - - private void resetPingData() { - this.lastPingID = -1; - this.lastPingSent = -1; + public void handleTabCompleteResponse(TabCompleteResponse response) { + if (outstandingTabComplete != null) { + if (!outstandingTabComplete.isAssumeCommand()) { + String command = outstandingTabComplete.getCommand().substring(1); + try { + Optional> offers = server.getCommandManager().offerSuggestions(player, command); + offers.ifPresent(strings -> response.getOffers().addAll(strings)); + } catch (Exception e) { + logger.error("Unable to provide tab list completions for {} for command '{}'", player.getUsername(), + command, e); + } + outstandingTabComplete = null; + } + player.getConnection().write(response); + } } } 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 4977c5df9..235915f24 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 @@ -3,6 +3,7 @@ package com.velocitypowered.proxy.connection.client; import com.velocitypowered.api.proxy.player.PlayerSettings; import com.velocitypowered.api.proxy.player.SkinParts; import com.velocitypowered.proxy.protocol.packet.ClientSettings; + import java.util.Locale; public class ClientSettingsWrapper implements PlayerSettings { 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 1fb951dea..cd4c55572 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 @@ -7,28 +7,29 @@ import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent; import com.velocitypowered.api.event.player.ServerPreConnectEvent; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.permission.PermissionProvider; -import com.velocitypowered.api.proxy.player.PlayerSettings; +import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.ConnectionRequestBuilder; +import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.player.PlayerSettings; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.util.GameProfile; import com.velocitypowered.api.util.MessagePosition; -import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.util.title.TextTitle; +import com.velocitypowered.api.util.title.Title; +import com.velocitypowered.api.util.title.Titles; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; -import com.velocitypowered.api.util.GameProfile; -import com.velocitypowered.proxy.protocol.packet.Chat; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; -import com.velocitypowered.proxy.protocol.packet.ClientSettings; -import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.packet.*; +import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.util.ThrowableUtils; -import com.velocitypowered.api.proxy.server.ServerInfo; -import com.velocitypowered.proxy.protocol.packet.Disconnect; -import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; - import net.kyori.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.TranslatableComponent; @@ -143,11 +144,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { byte pos = (byte) position.ordinal(); String json; if (position == MessagePosition.ACTION_BAR) { - // Due to issues with action bar packets, we'll need to convert the text message into a legacy message - // and then inject the legacy text into a component... yuck! - JsonObject object = new JsonObject(); - object.addProperty("text", ComponentSerializers.LEGACY.serialize(component)); - json = VelocityServer.GSON.toJson(object); + if (getProtocolVersion() >= ProtocolConstants.MINECRAFT_1_11) { + // We can use the title packet instead. + TitlePacket pkt = new TitlePacket(); + pkt.setAction(TitlePacket.SET_ACTION_BAR); + pkt.setComponent(ComponentSerializers.JSON.serialize(component)); + connection.write(pkt); + return; + } else { + // Due to issues with action bar packets, we'll need to convert the text message into a legacy message + // and then inject the legacy text into a component... yuck! + JsonObject object = new JsonObject(); + object.addProperty("text", ComponentSerializers.LEGACY.serialize(component)); + json = VelocityServer.GSON.toJson(object); + } } else { json = ComponentSerializers.JSON.serialize(component); } @@ -159,8 +169,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } @Override - public ConnectionRequestBuilder createConnectionRequest(@NonNull ServerInfo info) { - return new ConnectionRequestBuilderImpl(info); + public ConnectionRequestBuilder createConnectionRequest(@NonNull RegisteredServer server) { + return new ConnectionRequestBuilderImpl(server); } @Override @@ -191,56 +201,103 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { connection.closeWith(Disconnect.create(reason)); } + @Override + public void sendTitle(Title title) { + Preconditions.checkNotNull(title, "title"); + + if (title.equals(Titles.reset())) { + connection.write(TitlePacket.resetForProtocolVersion(connection.getProtocolVersion())); + } else if (title.equals(Titles.hide())) { + connection.write(TitlePacket.hideForProtocolVersion(connection.getProtocolVersion())); + } else if (title instanceof TextTitle) { + TextTitle tt = (TextTitle) title; + + if (tt.isResetBeforeSend()) { + connection.delayedWrite(TitlePacket.resetForProtocolVersion(connection.getProtocolVersion())); + } + + if (tt.getTitle().isPresent()) { + TitlePacket titlePkt = new TitlePacket(); + titlePkt.setAction(TitlePacket.SET_TITLE); + titlePkt.setComponent(ComponentSerializers.JSON.serialize(tt.getTitle().get())); + connection.delayedWrite(titlePkt); + } + if (tt.getSubtitle().isPresent()) { + TitlePacket titlePkt = new TitlePacket(); + titlePkt.setAction(TitlePacket.SET_SUBTITLE); + titlePkt.setComponent(ComponentSerializers.JSON.serialize(tt.getSubtitle().get())); + connection.delayedWrite(titlePkt); + } + + if (tt.areTimesSet()) { + TitlePacket timesPkt = TitlePacket.timesForProtocolVersion(connection.getProtocolVersion()); + timesPkt.setFadeIn(tt.getFadeIn()); + timesPkt.setStay(tt.getStay()); + timesPkt.setFadeOut(tt.getFadeOut()); + connection.delayedWrite(timesPkt); + } + connection.flush(); + } else { + throw new IllegalArgumentException("Unknown title class " + title.getClass().getName()); + } + + } + public VelocityServerConnection getConnectedServer() { return connectedServer; } - public void handleConnectionException(ServerInfo info, Throwable throwable) { + public void handleConnectionException(RegisteredServer server, Throwable throwable) { String error = ThrowableUtils.briefDescription(throwable); String userMessage; - if (connectedServer != null && connectedServer.getServerInfo().equals(info)) { - userMessage = "Exception in server " + info.getName(); + if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { + userMessage = "Exception in server " + server.getServerInfo().getName(); } else { - logger.error("{}: unable to connect to server {}", this, info.getName(), throwable); - userMessage = "Exception connecting to server " + info.getName(); + logger.error("{}: unable to connect to server {}", this, server.getServerInfo().getName(), throwable); + userMessage = "Exception connecting to server " + server.getServerInfo().getName(); } - handleConnectionException(info, null, TextComponent.builder() + handleConnectionException(server, null, TextComponent.builder() .content(userMessage + ": ") .color(TextColor.RED) .append(TextComponent.of(error, TextColor.WHITE)) .build()); } - public void handleConnectionException(ServerInfo info, Disconnect disconnect) { + public void handleConnectionException(RegisteredServer server, Disconnect disconnect) { Component disconnectReason = ComponentSerializers.JSON.deserialize(disconnect.getReason()); String plainTextReason = PASS_THRU_TRANSLATE.serialize(disconnectReason); - if (connectedServer != null && connectedServer.getServerInfo().equals(info)) { - logger.error("{}: kicked from server {}: {}", this, info.getName(), plainTextReason); + if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { + logger.error("{}: kicked from server {}: {}", this, server.getServerInfo().getName(), plainTextReason); + handleConnectionException(server, disconnectReason, TextComponent.builder() + .content("Kicked from " + server.getServerInfo().getName() + ": ") + .color(TextColor.RED) + .append(disconnectReason) + .build()); } else { - logger.error("{}: disconnected while connecting to {}: {}", this, info.getName(), plainTextReason); + logger.error("{}: disconnected while connecting to {}: {}", this, server.getServerInfo().getName(), plainTextReason); + handleConnectionException(server, disconnectReason, TextComponent.builder() + .content("Unable to connect to " + server.getServerInfo().getName() + ": ") + .color(TextColor.RED) + .append(disconnectReason) + .build()); } - handleConnectionException(info, disconnectReason, TextComponent.builder() - .content("Unable to connect to " + info.getName() + ": ") - .color(TextColor.RED) - .append(disconnectReason) - .build()); } - private void handleConnectionException(ServerInfo info, @Nullable Component kickReason, Component friendlyReason) { - boolean alreadyConnected = connectedServer != null && connectedServer.getServerInfo().equals(info);; + private void handleConnectionException(RegisteredServer rs, @Nullable Component kickReason, Component friendlyReason) { + boolean alreadyConnected = connectedServer != null && connectedServer.getServerInfo().equals(rs.getServerInfo()); connectionInFlight = null; if (connectedServer == null) { // The player isn't yet connected to a server. - Optional nextServer = getNextServerToTry(); + Optional nextServer = getNextServerToTry(); if (nextServer.isPresent()) { createConnectionRequest(nextServer.get()).fireAndForget(); } else { connection.closeWith(Disconnect.create(friendlyReason)); } - } else if (connectedServer.getServerInfo().equals(info)) { + } else if (connectedServer.getServerInfo().equals(rs.getServerInfo())) { // Already connected to the server being disconnected from. if (kickReason != null) { - server.getEventManager().fire(new KickedFromServerEvent(this, info, kickReason, !alreadyConnected, friendlyReason)) + server.getEventManager().fire(new KickedFromServerEvent(this, rs, kickReason, !alreadyConnected, friendlyReason)) .thenAcceptAsync(event -> { if (event.getResult() instanceof KickedFromServerEvent.DisconnectPlayer) { KickedFromServerEvent.DisconnectPlayer res = (KickedFromServerEvent.DisconnectPlayer) event.getResult(); @@ -257,11 +314,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { connection.closeWith(Disconnect.create(friendlyReason)); } } else { - connection.write(Chat.create(friendlyReason)); + connection.write(Chat.createClientbound(friendlyReason)); } } - Optional getNextServerToTry() { + Optional getNextServerToTry() { List serversToTry = server.getConfiguration().getAttemptConnectionOrder(); if (tryIndex >= serversToTry.size()) { return Optional.empty(); @@ -272,21 +329,25 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return server.getServers().getServer(toTryName); } - private CompletableFuture connect(ConnectionRequestBuilderImpl request) { + private Optional checkServer(RegisteredServer server) { + Preconditions.checkState(server instanceof VelocityRegisteredServer, "Not a valid Velocity server."); if (connectionInFlight != null) { - return CompletableFuture.completedFuture( - ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.CONNECTION_IN_PROGRESS) - ); + return Optional.of(ConnectionRequestBuilder.Status.CONNECTION_IN_PROGRESS); } + if (connectedServer != null && connectedServer.getServer().equals(server)) { + return Optional.of(ConnectionRequestBuilder.Status.ALREADY_CONNECTED); + } + return Optional.empty(); + } - if (connectedServer != null && connectedServer.getServerInfo().equals(request.getServer())) { - return CompletableFuture.completedFuture( - ConnectionRequestResults.plainResult(ConnectionRequestBuilder.Status.ALREADY_CONNECTED) - ); + private CompletableFuture connect(ConnectionRequestBuilderImpl request) { + Optional initialCheck = checkServer(request.getServer()); + if (initialCheck.isPresent()) { + return CompletableFuture.completedFuture(ConnectionRequestResults.plainResult(initialCheck.get())); } // Otherwise, initiate the connection. - ServerPreConnectEvent event = new ServerPreConnectEvent(this, ServerPreConnectEvent.ServerResult.allowed(request.getServer())); + ServerPreConnectEvent event = new ServerPreConnectEvent(this, request.getServer()); return server.getEventManager().fire(event) .thenCompose((newEvent) -> { if (!newEvent.getResult().isAllowed()) { @@ -295,7 +356,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { ); } - return new VelocityServerConnection(newEvent.getResult().getInfo().get(), this, server).connect(); + RegisteredServer rs = newEvent.getResult().getServer().get(); + Optional lastCheck = checkServer(rs); + if (lastCheck.isPresent()) { + return CompletableFuture.completedFuture(ConnectionRequestResults.plainResult(lastCheck.get())); + } + return new VelocityServerConnection((VelocityRegisteredServer) rs, this, server).connect(); }); } @@ -336,30 +402,37 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } @Override - public boolean hasPermission(String permission) { - return permissionFunction.getPermissionSetting(permission).asBoolean(); + public @NonNull Tristate getPermissionValue(@NonNull String permission) { + return permissionFunction.getPermissionValue(permission); } @Override - public void sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { Preconditions.checkNotNull(identifier, "identifier"); Preconditions.checkNotNull(data, "data"); PluginMessage message = new PluginMessage(); message.setChannel(identifier.getId()); message.setData(data); connection.write(message); + return true; + } + + @Override + public void spoofChatInput(String input) { + Preconditions.checkArgument(input.length() <= Chat.MAX_SERVERBOUND_MESSAGE_LENGTH, "input cannot be greater than " + Chat.MAX_SERVERBOUND_MESSAGE_LENGTH + " characters in length"); + connectedServer.getMinecraftConnection().write(Chat.createServerbound(input)); } private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { - private final ServerInfo info; + private final RegisteredServer server; - ConnectionRequestBuilderImpl(ServerInfo info) { - this.info = Preconditions.checkNotNull(info, "info"); + ConnectionRequestBuilderImpl(RegisteredServer server) { + this.server = Preconditions.checkNotNull(server, "info"); } @Override - public ServerInfo getServer() { - return info; + public RegisteredServer getServer() { + return server; } @Override @@ -372,7 +445,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { connect() .whenCompleteAsync((status, throwable) -> { if (throwable != null) { - handleConnectionException(info, throwable); + handleConnectionException(server, throwable); return; } @@ -387,7 +460,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { // Ignored; the plugin probably already handled this. break; case SERVER_DISCONNECTED: - handleConnectionException(info, Disconnect.create(status.getReason().orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR))); + handleConnectionException(server, Disconnect.create(status.getReason().orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR))); break; } }, connection.getChannel().eventLoop()); 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 54899d2a1..6ed3c805c 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 @@ -5,12 +5,12 @@ import com.google.common.collect.ImmutableList; import com.velocitypowered.api.event.connection.ConnectionHandshakeEvent; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.StateRegistry; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java index 64273473a..c25298bef 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialConnectSessionHandler.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.connection.client; -import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; public class InitialConnectSessionHandler implements MinecraftSessionHandler { private final ConnectedPlayer player; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java index ff728178f..116351ebb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/LoginSessionHandler.java @@ -2,22 +2,23 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.base.Preconditions; import com.velocitypowered.api.event.connection.LoginEvent; +import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent; import com.velocitypowered.api.event.connection.PreLoginEvent.PreLoginComponentResult; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.proxy.InboundConnection; -import com.velocitypowered.api.proxy.server.ServerInfo; -import com.velocitypowered.proxy.config.PlayerInfoForwarding; -import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.GameProfile; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.config.PlayerInfoForwarding; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.VelocityConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.*; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.util.EncryptionUtils; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -39,6 +40,7 @@ import java.util.Optional; import java.util.concurrent.ThreadLocalRandom; public class LoginSessionHandler implements MinecraftSessionHandler { + private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); private static final String MOJANG_SERVER_AUTH_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s"; @@ -157,7 +159,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { return; } - if (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed()) { + if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) { // Request encryption. EncryptionRequest request = generateRequest(); this.verify = Arrays.copyOf(request.getVerifyToken(), 4); @@ -218,7 +220,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } private void handleProxyLogin(ConnectedPlayer player) { - Optional toTry = player.getNextServerToTry(); + Optional toTry = player.getNextServerToTry(); if (!toTry.isPresent()) { player.close(TextComponent.of("No available servers", TextColor.RED)); return; @@ -244,7 +246,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { logger.info("{} has connected", player); inbound.setSessionHandler(new InitialConnectSessionHandler(player)); - player.createConnectionRequest(toTry.get()).fireAndForget(); + server.getEventManager().fire(new PostLoginEvent(player)).thenRun(() -> player.createConnectionRequest(toTry.get()).fireAndForget()); } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java index ccc53a8ac..a8b0f3bd0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/StatusSessionHandler.java @@ -4,16 +4,16 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.config.VelocityConfiguration; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.packet.StatusPing; import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.api.proxy.server.ServerPing; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -49,7 +49,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler { new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()), configuration.getMotdComponent(), configuration.getFavicon(), - configuration.isAnnounceForge() ? ServerPing.Modinfo.DEFAULT : null + configuration.isAnnounceForge() ? ServerPing.ModInfo.DEFAULT : null ); ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionMessages.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionMessages.java index b2ff70a41..727750d62 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionMessages.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/util/ConnectionMessages.java @@ -7,6 +7,7 @@ public class ConnectionMessages { public static final TextComponent ALREADY_CONNECTED = TextComponent.of("You are already connected to this server!", TextColor.RED); public static final TextComponent IN_PROGRESS = TextComponent.of("You are already connecting to a server!", TextColor.RED); public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent.of("Internal server connection error"); + public static final TextComponent UNEXPECTED_DISCONNECT = TextComponent.of("Unexpectedly disconnected from server - crash?"); private ConnectionMessages() { throw new AssertionError(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java index fcb1d34f9..c654a2388 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -1,28 +1,57 @@ package com.velocitypowered.proxy.console; +import com.velocitypowered.api.command.CommandSource; +import com.velocitypowered.api.event.permission.PermissionsSetupEvent; +import com.velocitypowered.api.permission.PermissionFunction; +import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.proxy.VelocityServer; +import net.kyori.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; +import net.kyori.text.serializer.ComponentSerializers; import net.minecrell.terminalconsole.SimpleTerminalConsole; -import org.jline.reader.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.jline.reader.Candidate; +import org.jline.reader.LineReader; +import org.jline.reader.LineReaderBuilder; import java.util.List; import java.util.Optional; -public final class VelocityConsole extends SimpleTerminalConsole { +public final class VelocityConsole extends SimpleTerminalConsole implements CommandSource { + private static final Logger logger = LogManager.getLogger(VelocityConsole.class); private final VelocityServer server; + private PermissionFunction permissionFunction = PermissionFunction.ALWAYS_TRUE; public VelocityConsole(VelocityServer server) { this.server = server; } + @Override + public void sendMessage(Component component) { + logger.info(ComponentSerializers.LEGACY.serialize(component)); + } + + @Override + public @NonNull Tristate getPermissionValue(@NonNull String permission) { + return this.permissionFunction.getPermissionValue(permission); + } + + public void setupPermissions() { + PermissionsSetupEvent event = new PermissionsSetupEvent(this, s -> PermissionFunction.ALWAYS_TRUE); + this.server.getEventManager().fire(event).join(); // this is called on startup, we can safely #join + this.permissionFunction = event.createFunction(this); + } + @Override protected LineReader buildReader(LineReaderBuilder builder) { return super.buildReader(builder .appName("Velocity") .completer((reader, parsedLine, list) -> { - Optional> o = server.getCommandManager().offerSuggestions(server.getConsoleCommandSource(), parsedLine.line()); + Optional> o = this.server.getCommandManager().offerSuggestions(this, parsedLine.line()); o.ifPresent(offers -> { for (String offer : offers) { list.add(new Candidate(offer)); @@ -39,8 +68,8 @@ public final class VelocityConsole extends SimpleTerminalConsole { @Override protected void runCommand(String command) { - if (!this.server.getCommandManager().execute(this.server.getConsoleCommandSource(), command)) { - server.getConsoleCommandSource().sendMessage(TextComponent.of("Command not found.", TextColor.RED)); + if (!this.server.getCommandManager().execute(this, command)) { + sendMessage(TextComponent.of("Command not found.", TextColor.RED)); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java index 1fb47cce1..c6feff212 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/messages/VelocityChannelRegistrar.java @@ -2,10 +2,10 @@ package com.velocitypowered.proxy.messages; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.velocitypowered.api.proxy.messages.*; -import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.messages.ChannelRegistrar; +import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import java.util.Collection; import java.util.Map; @@ -13,39 +13,20 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; public class VelocityChannelRegistrar implements ChannelRegistrar { - private static final Logger logger = LogManager.getLogger(VelocityChannelRegistrar.class); - private final Map handlers = new ConcurrentHashMap<>(); private final Map identifierMap = new ConcurrentHashMap<>(); @Override - public void register(MessageHandler handler, ChannelIdentifier... identifiers) { + public void register(ChannelIdentifier... identifiers) { for (ChannelIdentifier identifier : identifiers) { Preconditions.checkArgument(identifier instanceof LegacyChannelIdentifier || identifier instanceof MinecraftChannelIdentifier, "identifier is unknown"); } for (ChannelIdentifier identifier : identifiers) { - handlers.put(identifier.getId(), handler); identifierMap.put(identifier.getId(), identifier); } } - public MessageHandler.ForwardStatus handlePluginMessage(ChannelMessageSource source, ChannelSide side, PluginMessage message) { - MessageHandler handler = handlers.get(message.getChannel()); - ChannelIdentifier identifier = identifierMap.get(message.getChannel()); - if (handler == null || identifier == null) { - return MessageHandler.ForwardStatus.FORWARD; - } - - try { - return handler.handle(source, side, identifier, message.getData()); - } catch (Exception e) { - logger.info("Unable to handle plugin message on channel {} for {}", message.getChannel(), source); - // In case of doubt, do not forward the message on. - return MessageHandler.ForwardStatus.HANDLED; - } - } - @Override public void unregister(ChannelIdentifier... identifiers) { for (ChannelIdentifier identifier : identifiers) { @@ -54,7 +35,6 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { } for (ChannelIdentifier identifier : identifiers) { - handlers.remove(identifier.getId()); identifierMap.remove(identifier.getId()); } } @@ -73,4 +53,8 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { public boolean registered(String id) { return identifierMap.containsKey(id); } + + public ChannelIdentifier getFromId(String id) { + return identifierMap.get(id); + } } 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 cc37d62b2..65301cd77 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -7,21 +7,11 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.netty.GS4QueryHandler; -import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; -import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; -import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.netty.*; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; -import io.netty.channel.epoll.Epoll; -import io.netty.channel.epoll.EpollDatagramChannel; -import io.netty.channel.epoll.EpollEventLoopGroup; -import io.netty.channel.epoll.EpollServerSocketChannel; -import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.epoll.*; import io.netty.channel.kqueue.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.DatagramChannel; @@ -75,7 +65,7 @@ public final class ConnectionManager { @Override protected void initChannel(final Channel ch) { ch.pipeline() - .addLast(READ_TIMEOUT, new ReadTimeoutHandler(CLIENT_READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)) + .addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS)) .addLast(LEGACY_PING_DECODER, new LegacyPingDecoder()) .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) .addLast(LEGACY_PING_ENCODER, LegacyPingEncoder.INSTANCE) @@ -125,7 +115,9 @@ public final class ConnectionManager { public Bootstrap createWorker() { return new Bootstrap() .channel(this.transportType.socketChannelClass) - .group(this.workerGroup); + .group(this.workerGroup) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, server.getConfiguration().getConnectTimeout()); } public void shutdown() { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index 518a3acbe..fb248dc6a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -13,7 +13,4 @@ public interface Connections { String MINECRAFT_DECODER = "minecraft-decoder"; String MINECRAFT_ENCODER = "minecraft-encoder"; String READ_TIMEOUT = "read-timeout"; - - int CLIENT_READ_TIMEOUT_SECONDS = 30; // client -> proxy - int SERVER_READ_TIMEOUT_SECONDS = 30; // proxy -> server } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java b/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java index 24ab295da..f14f10c40 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/http/NettyHttpClient.java @@ -2,7 +2,7 @@ package com.velocitypowered.proxy.network.http; import com.velocitypowered.proxy.VelocityServer; import io.netty.bootstrap.Bootstrap; -import io.netty.channel.*; +import io.netty.channel.Channel; import io.netty.channel.pool.*; import io.netty.handler.codec.http.*; import io.netty.handler.ssl.SslContext; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java index 3f84e6279..b93afb3fd 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java @@ -13,7 +13,10 @@ import com.velocitypowered.proxy.util.concurrency.RecordingThreadFactory; import net.kyori.event.EventSubscriber; import net.kyori.event.PostResult; import net.kyori.event.SimpleEventBus; -import net.kyori.event.method.*; +import net.kyori.event.method.EventExecutor; +import net.kyori.event.method.MethodScanner; +import net.kyori.event.method.MethodSubscriptionAdapter; +import net.kyori.event.method.SimpleMethodSubscriptionAdapter; import net.kyori.event.method.asm.ASMEventExecutorFactory; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -21,8 +24,13 @@ import org.checkerframework.checker.nullness.qual.NonNull; import java.lang.reflect.Method; import java.net.URL; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; public class VelocityEventManager implements EventManager { private static final Logger logger = LogManager.getLogger(VelocityEventManager.class); 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 f4625a900..3c1d5b2c5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.plugin; -import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.plugin.meta.PluginDependency; import com.velocitypowered.proxy.VelocityServer; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java index dc26dec3e..09d1df949 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/JavaPluginLoader.java @@ -2,7 +2,9 @@ package com.velocitypowered.proxy.plugin.loader; import com.google.inject.Guice; import com.google.inject.Injector; -import com.velocitypowered.api.plugin.*; +import com.velocitypowered.api.plugin.InvalidPluginException; +import com.velocitypowered.api.plugin.PluginContainer; +import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.meta.PluginDependency; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.proxy.VelocityServer; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java index cc3ddadc2..66e116eeb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/loader/java/VelocityPluginModule.java @@ -8,7 +8,6 @@ import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.plugin.PluginManager; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; -import com.velocitypowered.proxy.VelocityServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 91fab4eb4..8e6d1fe78 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -8,7 +8,7 @@ import io.netty.util.collection.IntObjectMap; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import java.util.*; +import java.util.Objects; import java.util.function.Supplier; import static com.velocitypowered.proxy.protocol.ProtocolConstants.*; @@ -23,7 +23,7 @@ public enum StateRegistry { }, STATUS { { - SERVERBOUND.register(StatusRequest.class, StatusRequest::new, + SERVERBOUND.register(StatusRequest.class, () -> StatusRequest.INSTANCE, genericMappings(0x00)); SERVERBOUND.register(StatusPing.class, StatusPing::new, genericMappings(0x01)); @@ -40,8 +40,7 @@ public enum StateRegistry { map(0x14, MINECRAFT_1_8, false), map(0x01, MINECRAFT_1_9, false), map(0x02, MINECRAFT_1_12, false), - map(0x01, MINECRAFT_1_12_1, false), - map(0x05, MINECRAFT_1_13, false)); + map(0x01, MINECRAFT_1_12_1, false)); SERVERBOUND.register(Chat.class, Chat::new, map(0x01, MINECRAFT_1_8, false), map(0x02, MINECRAFT_1_9, false), @@ -77,10 +76,9 @@ public enum StateRegistry { map(0x0F, MINECRAFT_1_12, true), map(0x0E, MINECRAFT_1_13, true)); CLIENTBOUND.register(TabCompleteResponse.class, TabCompleteResponse::new, - map(0x3A, MINECRAFT_1_8, true), - map(0x0E, MINECRAFT_1_9, true), - map(0x0E, MINECRAFT_1_12, true), - map(0x10, MINECRAFT_1_13, true)); + map(0x3A, MINECRAFT_1_8, false), + map(0x0E, MINECRAFT_1_9, false), + map(0x0E, MINECRAFT_1_12, false)); CLIENTBOUND.register(PluginMessage.class, PluginMessage::new, map(0x3F, MINECRAFT_1_8, false), map(0x18, MINECRAFT_1_9, false), @@ -114,30 +112,12 @@ public enum StateRegistry { map(0x49, MINECRAFT_1_12, true), map(0x4A, MINECRAFT_1_12_1, true), map(0x4E, MINECRAFT_1_13, true)); - CLIENTBOUND.register(ScoreboardDisplay.class, ScoreboardDisplay::new, - map(0x3D, MINECRAFT_1_8, true), - map(0x38, MINECRAFT_1_9, true), - map(0x3A, MINECRAFT_1_12, true), - map(0x3B, MINECRAFT_1_12_1, true), - map(0x3E, MINECRAFT_1_13, true)); - CLIENTBOUND.register(ScoreboardObjective.class, ScoreboardObjective::new, - map(0x3B, MINECRAFT_1_8, true), - map(0x3F, MINECRAFT_1_9, true), - map(0x41, MINECRAFT_1_12, true), - map(0x42, MINECRAFT_1_12_1, true), - map(0x45, MINECRAFT_1_13, true)); - CLIENTBOUND.register(ScoreboardTeam.class, ScoreboardTeam::new, - map(0x3E, MINECRAFT_1_8, true), - map(0x41, MINECRAFT_1_9, true), - map(0x43, MINECRAFT_1_12, true), - map(0x44, MINECRAFT_1_12_1, true), - map(0x47, MINECRAFT_1_13, true)); - CLIENTBOUND.register(ScoreboardSetScore.class, ScoreboardSetScore::new, - map(0x3C, MINECRAFT_1_8, true), - map(0x42, MINECRAFT_1_9, true), - map(0x44, MINECRAFT_1_12, true), - map(0x45, MINECRAFT_1_12_1, true), - map(0x48, MINECRAFT_1_13, true)); + CLIENTBOUND.register(TitlePacket.class, TitlePacket::new, + map(0x45, MINECRAFT_1_8, true), + map(0x45, MINECRAFT_1_9, true), + map(0x47, MINECRAFT_1_12, true), + map(0x48, MINECRAFT_1_12_1, true), + map(0x4B, MINECRAFT_1_13, true)); } }, LOGIN { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java index df093d78d..2628bc445 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/GS4QueryHandler.java @@ -101,9 +101,6 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler queryResponse.writeByte(QUERY_TYPE_STAT); queryResponse.writeInt(sessionId); - // Fetch information - Collection players = server.getAllPlayers(); - // Start writing the response ResponseWriter responseWriter = new ResponseWriter(queryResponse, queryMessage.readableBytes() == 0); responseWriter.write("hostname", ComponentSerializers.PLAIN.serialize(server.getConfiguration().getMotdComponent())); @@ -114,12 +111,14 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler responseWriter.write("plugins", ""); responseWriter.write("map", "Velocity"); - responseWriter.write("numplayers", players.size()); + responseWriter.write("numplayers", server.getPlayerCount()); responseWriter.write("maxplayers", server.getConfiguration().getShowMaxPlayers()); responseWriter.write("hostport", server.getConfiguration().getBind().getPort()); responseWriter.write("hostip", server.getConfiguration().getBind().getHostString()); - responseWriter.writePlayers(players); + if (!responseWriter.isBasic) { + responseWriter.writePlayers(server.getAllPlayers()); + } break; } @@ -132,6 +131,8 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler ctx.writeAndFlush(responsePacket); } catch (Exception e) { logger.warn("Error while trying to handle a query packet from {}", msg.sender(), e); + // NB: Only need to explicitly release upon exception, writing the response out will decrement the reference + // count. responsePacket.release(); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java index 61893ff44..95b218056 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressDecoder.java @@ -1,8 +1,8 @@ package com.velocitypowered.proxy.protocol.netty; import com.google.common.base.Preconditions; -import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.natives.compression.VelocityCompressor; +import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToMessageDecoder; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressEncoder.java index 802f45121..48ded65a5 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftCompressEncoder.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.protocol.netty; -import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.natives.compression.VelocityCompressor; +import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java index 07ddb287f..4b7e00232 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftDecoder.java @@ -1,7 +1,10 @@ package com.velocitypowered.proxy.protocol.netty; import com.google.common.base.Preconditions; -import com.velocitypowered.proxy.protocol.*; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.CorruptedFrameException; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java index 7a0d61c92..3116c6a69 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftVarintLengthEncoder.java @@ -4,7 +4,6 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.MessageToByteEncoder; import io.netty.handler.codec.MessageToMessageEncoder; import java.util.List; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java index c3de9c6b9..749854506 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Chat.java @@ -1,8 +1,8 @@ package com.velocitypowered.proxy.protocol.packet; import com.google.common.base.Preconditions; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import net.kyori.text.Component; @@ -10,6 +10,8 @@ import net.kyori.text.serializer.ComponentSerializers; public class Chat implements MinecraftPacket { public static final byte CHAT = (byte) 0; + public static final int MAX_SERVERBOUND_MESSAGE_LENGTH = 256; + private String message; private byte type; @@ -61,12 +63,16 @@ public class Chat implements MinecraftPacket { } } - public static Chat create(Component component) { - return create(component, CHAT); + public static Chat createClientbound(Component component) { + return createClientbound(component, CHAT); } - public static Chat create(Component component, byte type) { + public static Chat createClientbound(Component component, byte type) { Preconditions.checkNotNull(component, "component"); return new Chat(ComponentSerializers.JSON.serialize(component), type); } + + public static Chat createServerbound(String message) { + return new Chat(message, CHAT); + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Disconnect.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Disconnect.java index 1420b7981..f6dfd877a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Disconnect.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Disconnect.java @@ -1,8 +1,8 @@ package com.velocitypowered.proxy.protocol.packet; import com.google.common.base.Preconditions; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import net.kyori.text.Component; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Handshake.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Handshake.java index dec9ecb4a..aef59ec2b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Handshake.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/Handshake.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.protocol.packet; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HeaderAndFooter.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HeaderAndFooter.java index 35c75af30..6ee851d65 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HeaderAndFooter.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/HeaderAndFooter.java @@ -2,13 +2,13 @@ package com.velocitypowered.proxy.protocol.packet; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants.Direction; -import static com.velocitypowered.proxy.protocol.ProtocolUtils.writeString; - import io.netty.buffer.ByteBuf; import net.kyori.text.Component; import net.kyori.text.serializer.ComponentSerializer; import net.kyori.text.serializer.ComponentSerializers; +import static com.velocitypowered.proxy.protocol.ProtocolUtils.writeString; + public class HeaderAndFooter implements MinecraftPacket { private static final HeaderAndFooter RESET = new HeaderAndFooter("{\"translate\":\"\"}", "{\"translate\":\"\"}"); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/KeepAlive.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/KeepAlive.java index 638b15d52..1e491b61b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/KeepAlive.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/KeepAlive.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.protocol.packet; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardDisplay.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardDisplay.java deleted file mode 100644 index 8f22abeb5..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardDisplay.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolConstants; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; - -public class ScoreboardDisplay implements MinecraftPacket { - private byte position; - private String displayName; - - public byte getPosition() { - return position; - } - - public void setPosition(byte position) { - this.position = position; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - @Override - public String toString() { - return "ScoreboardDisplay{" + - "position=" + position + - ", displayName='" + displayName + '\'' + - '}'; - } - - @Override - public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - this.position = buf.readByte(); - this.displayName = ProtocolUtils.readString(buf, 16); - } - - @Override - public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - buf.writeByte(position); - ProtocolUtils.writeString(buf, displayName); - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardObjective.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardObjective.java deleted file mode 100644 index 1234e0831..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardObjective.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolConstants; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import com.velocitypowered.proxy.protocol.util.ScoreboardProtocolUtil; -import io.netty.buffer.ByteBuf; -import net.kyori.text.Component; - -public class ScoreboardObjective implements MinecraftPacket { - public static final byte ADD = (byte) 0; - public static final byte REMOVE = (byte) 1; - public static final byte CHANGE = (byte) 2; - private String id; - private byte mode; - private Component displayName; - private ObjectiveMode type; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public byte getMode() { - return mode; - } - - public void setMode(byte mode) { - this.mode = mode; - } - - public Component getDisplayName() { - return displayName; - } - - public void setDisplayName(Component displayName) { - this.displayName = displayName; - } - - public ObjectiveMode getType() { - return type; - } - - public void setType(ObjectiveMode type) { - this.type = type; - } - - @Override - public String toString() { - return "ScoreboardObjective{" + - "id='" + id + '\'' + - ", mode=" + mode + - ", displayName='" + displayName + '\'' + - ", type='" + type + '\'' + - '}'; - } - - @Override - public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - this.id = ProtocolUtils.readString(buf, 16); - this.mode = buf.readByte(); - if (this.mode != REMOVE) { - this.displayName = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion); - if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) { - this.type = ScoreboardProtocolUtil.getMode(ProtocolUtils.readVarInt(buf)); - } else { - this.type = ScoreboardProtocolUtil.getMode(ProtocolUtils.readString(buf)); - } - } - } - - @Override - public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - ProtocolUtils.writeString(buf, id); - buf.writeByte(mode); - if (this.mode != REMOVE) { - ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, displayName); - if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) { - ProtocolUtils.writeVarInt(buf, type.ordinal()); - } else { - ProtocolUtils.writeString(buf, type.name().toLowerCase()); - } - } - } - - public enum ObjectiveMode { - INTEGER, - HEARTS - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardSetScore.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardSetScore.java deleted file mode 100644 index 264d9eec2..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardSetScore.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolConstants; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; - -public class ScoreboardSetScore implements MinecraftPacket { - public static final byte CHANGE = (byte) 0; - public static final byte REMOVE = (byte) 1; - private String entity; - private byte action; - private String objective; - private int value; - - public String getEntity() { - return entity; - } - - public void setEntity(String entity) { - this.entity = entity; - } - - public byte getAction() { - return action; - } - - public void setAction(byte action) { - this.action = action; - } - - public String getObjective() { - return objective; - } - - public void setObjective(String objective) { - this.objective = objective; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - - @Override - public String toString() { - return "ScoreboardSetScore{" + - "entity='" + entity + '\'' + - ", action=" + action + - ", objective='" + objective + '\'' + - ", value=" + value + - '}'; - } - - @Override - public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - this.entity = ProtocolUtils.readString(buf, 40); - this.action = buf.readByte(); - this.objective = ProtocolUtils.readString(buf, 16); - if (this.action != REMOVE) { - value = ProtocolUtils.readVarInt(buf); - } - } - - @Override - public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - ProtocolUtils.writeString(buf, entity); - buf.writeByte(action); - ProtocolUtils.writeString(buf, objective); - if (this.action != REMOVE) { - ProtocolUtils.writeVarInt(buf, value); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardTeam.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardTeam.java deleted file mode 100644 index e6a3d7421..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ScoreboardTeam.java +++ /dev/null @@ -1,215 +0,0 @@ -package com.velocitypowered.proxy.protocol.packet; - -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.ProtocolConstants; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import net.kyori.text.Component; - -import java.util.ArrayList; -import java.util.List; - -public class ScoreboardTeam implements MinecraftPacket { - public static final byte ADD = (byte) 0; - public static final byte REMOVE = (byte) 1; - public static final byte UPDATE = (byte) 2; - public static final byte ADD_PLAYER = (byte) 3; - public static final byte REMOVE_PLAYER = (byte) 4; - private String id; - private byte mode; - - private Component displayName; - private Component prefix; - private Component suffix; - private byte flags; - private String nameTagVisibility; - private String collisionRule; - private int color; - private List entities; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public byte getMode() { - return mode; - } - - public void setMode(byte mode) { - this.mode = mode; - } - - public Component getDisplayName() { - return displayName; - } - - public void setDisplayName(Component displayName) { - this.displayName = displayName; - } - - public Component getPrefix() { - return prefix; - } - - public void setPrefix(Component prefix) { - this.prefix = prefix; - } - - public Component getSuffix() { - return suffix; - } - - public void setSuffix(Component suffix) { - this.suffix = suffix; - } - - public byte getFlags() { - return flags; - } - - public void setFlags(byte flags) { - this.flags = flags; - } - - public String getNameTagVisibility() { - return nameTagVisibility; - } - - public void setNameTagVisibility(String nameTagVisibility) { - this.nameTagVisibility = nameTagVisibility; - } - - public String getCollisionRule() { - return collisionRule; - } - - public void setCollisionRule(String collisionRule) { - this.collisionRule = collisionRule; - } - - public int getColor() { - return color; - } - - public void setColor(int color) { - this.color = color; - } - - public List getEntities() { - return entities; - } - - public void setEntities(List entities) { - this.entities = entities; - } - - @Override - public String toString() { - return "ScoreboardTeam{" + - "id='" + id + '\'' + - ", mode=" + mode + - ", displayName='" + displayName + '\'' + - ", prefix='" + prefix + '\'' + - ", suffix='" + suffix + '\'' + - ", flags=" + flags + - ", nameTagVisibility='" + nameTagVisibility + '\'' + - ", collisionRule='" + collisionRule + '\'' + - ", color=" + color + - ", entities=" + entities + - '}'; - } - - @Override - public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - this.id = ProtocolUtils.readString(buf, 16); - this.mode = buf.readByte(); - - switch (mode) { - case ADD: - case UPDATE: - this.displayName = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion); - if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2) { - this.prefix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion); - this.suffix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion); - } - this.flags = buf.readByte(); - this.nameTagVisibility = ProtocolUtils.readString(buf, 32); - if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) { - this.collisionRule = ProtocolUtils.readString(buf, 32); - } - this.color = protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2 ? buf.readByte() : - ProtocolUtils.readVarInt(buf); - if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) { - this.prefix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion); - this.suffix = ProtocolUtils.readScoreboardTextComponent(buf, protocolVersion); - } - if (mode == ADD) { - this.entities = readEntities(buf); - } - break; - case REMOVE: // remove - break; - case ADD_PLAYER: // add player - case REMOVE_PLAYER: // remove player - this.entities = readEntities(buf); - break; - } - } - - @Override - public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - ProtocolUtils.writeString(buf, id); - buf.writeByte(mode); - switch (mode) { - case ADD: - case UPDATE: - ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, displayName); - if (protocolVersion <= ProtocolConstants.MINECRAFT_1_12_2) { - ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, prefix); - ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, suffix); - } - buf.writeByte(flags); - ProtocolUtils.writeString(buf, nameTagVisibility); - if (protocolVersion >= ProtocolConstants.MINECRAFT_1_9) { - ProtocolUtils.writeString(buf, collisionRule); - } - if (protocolVersion >= ProtocolConstants.MINECRAFT_1_13) { - ProtocolUtils.writeVarInt(buf, color); - ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, prefix); - ProtocolUtils.writeScoreboardTextComponent(buf, protocolVersion, suffix); - } else { - buf.writeByte(color); - } - if (mode == ADD) { - writeEntities(buf, entities); - } - break; - case REMOVE: - break; - case ADD_PLAYER: - case REMOVE_PLAYER: - writeEntities(buf, entities); - break; - } - } - - private static List readEntities(ByteBuf buf) { - List entities = new ArrayList<>(); - int size = ProtocolUtils.readVarInt(buf); - for (int i = 0; i < size; i++) { - entities.add(ProtocolUtils.readString(buf, 40)); - } - return entities; - } - - private static void writeEntities(ByteBuf buf, List entities) { - ProtocolUtils.writeVarInt(buf, entities.size()); - for (String entity : entities) { - ProtocolUtils.writeString(buf, entity); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java index 459afe338..a9cb0ac68 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.protocol.packet; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusRequest.java index c53276e5a..33e1e1411 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusRequest.java @@ -1,10 +1,16 @@ package com.velocitypowered.proxy.protocol.packet; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import io.netty.buffer.ByteBuf; public class StatusRequest implements MinecraftPacket { + public static final StatusRequest INSTANCE = new StatusRequest(); + + private StatusRequest() { + + } + @Override public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { @@ -14,4 +20,9 @@ public class StatusRequest implements MinecraftPacket { public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { } + + @Override + public String toString() { + return "StatusRequest"; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusResponse.java index 75a052258..7c397fa83 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/StatusResponse.java @@ -1,7 +1,7 @@ package com.velocitypowered.proxy.protocol.packet; -import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java index 10c14b400..a3636f85c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteRequest.java @@ -9,20 +9,11 @@ import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_1 import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_9; public class TabCompleteRequest implements MinecraftPacket { - private int transactionId; private String command; private boolean assumeCommand; private boolean hasPosition; private long position; - public int getTransactionId() { - return transactionId; - } - - public void setTransactionId(int transactionId) { - this.transactionId = transactionId; - } - public String getCommand() { return command; } @@ -58,8 +49,7 @@ public class TabCompleteRequest implements MinecraftPacket { @Override public String toString() { return "TabCompleteRequest{" + - "transactionId=" + transactionId + - ", command='" + command + '\'' + + "command='" + command + '\'' + ", assumeCommand=" + assumeCommand + ", hasPosition=" + hasPosition + ", position=" + position + @@ -68,35 +58,25 @@ public class TabCompleteRequest implements MinecraftPacket { @Override public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - if (protocolVersion >= MINECRAFT_1_13) { - this.transactionId = ProtocolUtils.readVarInt(buf); - this.command = ProtocolUtils.readString(buf); - } else { - this.command = ProtocolUtils.readString(buf); - if (protocolVersion >= MINECRAFT_1_9) { - this.assumeCommand = buf.readBoolean(); - } - this.hasPosition = buf.readBoolean(); - if (hasPosition) { - this.position = buf.readLong(); - } + this.command = ProtocolUtils.readString(buf); + if (protocolVersion >= MINECRAFT_1_9) { + this.assumeCommand = buf.readBoolean(); + } + this.hasPosition = buf.readBoolean(); + if (hasPosition) { + this.position = buf.readLong(); } } @Override public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - if (protocolVersion >= MINECRAFT_1_13) { - ProtocolUtils.writeVarInt(buf, transactionId); - ProtocolUtils.writeString(buf, command); - } else { - ProtocolUtils.writeString(buf, command); - if (protocolVersion >= MINECRAFT_1_9) { - buf.writeBoolean(assumeCommand); - } - buf.writeBoolean(hasPosition); - if (hasPosition) { - buf.writeLong(position); - } + ProtocolUtils.writeString(buf, command); + if (protocolVersion >= MINECRAFT_1_9) { + buf.writeBoolean(assumeCommand); + } + buf.writeBoolean(hasPosition); + if (hasPosition) { + buf.writeLong(position); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java index f5464360c..0a528acd8 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TabCompleteResponse.java @@ -4,125 +4,37 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolConstants; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; -import net.kyori.text.Component; -import net.kyori.text.serializer.ComponentSerializers; -import org.checkerframework.checker.nullness.qual.Nullable; import java.util.ArrayList; import java.util.List; -import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_13; - public class TabCompleteResponse implements MinecraftPacket { - private int transactionId; - private int start; - private int length; - private final List offers = new ArrayList<>(); + private final List offers = new ArrayList<>(); - public int getTransactionId() { - return transactionId; - } - - public void setTransactionId(int transactionId) { - this.transactionId = transactionId; - } - - public int getStart() { - return start; - } - - public void setStart(int start) { - this.start = start; - } - - public int getLength() { - return length; - } - - public void setLength(int length) { - this.length = length; - } - - public List getOffers() { + public List getOffers() { return offers; } @Override public String toString() { return "TabCompleteResponse{" + - "transactionId=" + transactionId + - ", start=" + start + - ", length=" + length + - ", offers=" + offers + + "offers=" + offers + '}'; } @Override public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - if (protocolVersion >= MINECRAFT_1_13) { - this.transactionId = ProtocolUtils.readVarInt(buf); - this.start = ProtocolUtils.readVarInt(buf); - this.length = ProtocolUtils.readVarInt(buf); - int offersAvailable = ProtocolUtils.readVarInt(buf); - for (int i = 0; i < offersAvailable; i++) { - String entry = ProtocolUtils.readString(buf); - Component component = buf.readBoolean() ? ComponentSerializers.JSON.deserialize(ProtocolUtils.readString(buf)) : - null; - offers.add(new Offer(entry, component)); - } - } else { - int offersAvailable = ProtocolUtils.readVarInt(buf); - for (int i = 0; i < offersAvailable; i++) { - offers.add(new Offer(ProtocolUtils.readString(buf), null)); - } + int offersAvailable = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < offersAvailable; i++) { + offers.add(ProtocolUtils.readString(buf)); } } @Override public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { - if (protocolVersion >= MINECRAFT_1_13) { - ProtocolUtils.writeVarInt(buf, transactionId); - ProtocolUtils.writeVarInt(buf, start); - ProtocolUtils.writeVarInt(buf, length); - ProtocolUtils.writeVarInt(buf, offers.size()); - for (Offer offer : offers) { - ProtocolUtils.writeString(buf, offer.entry); - buf.writeBoolean(offer.tooltip != null); - if (offer.tooltip != null) { - ProtocolUtils.writeString(buf, ComponentSerializers.JSON.serialize(offer.tooltip)); - } - } - } else { - ProtocolUtils.writeVarInt(buf, offers.size()); - for (Offer offer : offers) { - ProtocolUtils.writeString(buf, offer.entry); - } - } - } - - public static class Offer { - private final String entry; - private final Component tooltip; - - public Offer(String entry, @Nullable Component tooltip) { - this.entry = entry; - this.tooltip = tooltip; - } - - public String getEntry() { - return entry; - } - - public @Nullable Component getTooltip() { - return tooltip; - } - - @Override - public String toString() { - return "Offer{" + - "entry='" + entry + '\'' + - ", tooltip=" + tooltip + - '}'; + ProtocolUtils.writeVarInt(buf, offers.size()); + for (String offer : offers) { + ProtocolUtils.writeString(buf, offer); } } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TitlePacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TitlePacket.java new file mode 100644 index 000000000..de1398060 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/TitlePacket.java @@ -0,0 +1,137 @@ +package com.velocitypowered.proxy.protocol.packet; + +import com.google.common.base.Preconditions; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class TitlePacket implements MinecraftPacket { + public static final int SET_TITLE = 0; + public static final int SET_SUBTITLE = 1; + public static final int SET_ACTION_BAR = 2; + public static final int SET_TIMES = 3; + public static final int SET_TIMES_OLD = 2; + public static final int HIDE = 4; + public static final int HIDE_OLD = 3; + public static final int RESET = 5; + public static final int RESET_OLD = 4; + + private int action; + private String component; + private int fadeIn; + private int stay; + private int fadeOut; + + @Override + public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + throw new UnsupportedOperationException(); // encode only + } + + @Override + public void encode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) { + ProtocolUtils.writeVarInt(buf, action); + if (protocolVersion >= ProtocolConstants.MINECRAFT_1_11) { + // 1.11+ shifted the action enum by 1 to handle the action bar + switch (action) { + case SET_TITLE: + case SET_SUBTITLE: + case SET_ACTION_BAR: + ProtocolUtils.writeString(buf, component); + break; + case SET_TIMES: + buf.writeInt(fadeIn); + buf.writeInt(stay); + buf.writeInt(fadeOut); + break; + case HIDE: + case RESET: + break; + } + } else { + switch (action) { + case SET_TITLE: + case SET_SUBTITLE: + ProtocolUtils.writeString(buf, component); + break; + case SET_TIMES_OLD: + buf.writeInt(fadeIn); + buf.writeInt(stay); + buf.writeInt(fadeOut); + break; + case HIDE_OLD: + case RESET_OLD: + break; + } + } + } + + public int getAction() { + return action; + } + + public void setAction(int action) { + this.action = action; + } + + public String getComponent() { + return component; + } + + public void setComponent(String component) { + this.component = component; + } + + public int getFadeIn() { + return fadeIn; + } + + public void setFadeIn(int fadeIn) { + this.fadeIn = fadeIn; + } + + public int getStay() { + return stay; + } + + public void setStay(int stay) { + this.stay = stay; + } + + public int getFadeOut() { + return fadeOut; + } + + public void setFadeOut(int fadeOut) { + this.fadeOut = fadeOut; + } + + public static TitlePacket hideForProtocolVersion(int protocolVersion) { + TitlePacket packet = new TitlePacket(); + packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.HIDE : TitlePacket.HIDE_OLD); + return packet; + } + + public static TitlePacket resetForProtocolVersion(int protocolVersion) { + TitlePacket packet = new TitlePacket(); + packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.RESET : TitlePacket.RESET_OLD); + return packet; + } + + public static TitlePacket timesForProtocolVersion(int protocolVersion) { + TitlePacket packet = new TitlePacket(); + packet.setAction(protocolVersion >= ProtocolConstants.MINECRAFT_1_11 ? TitlePacket.SET_TIMES : TitlePacket.SET_TIMES_OLD); + return packet; + } + + @Override + public String toString() { + return "TitlePacket{" + + "action=" + action + + ", component='" + component + '\'' + + ", fadeIn=" + fadeIn + + ", stay=" + stay + + ", fadeOut=" + fadeOut + + '}'; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java index 075058486..1ffb37dc9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/PluginMessageUtil.java @@ -5,7 +5,6 @@ import com.google.common.collect.ImmutableList; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import java.nio.charset.StandardCharsets; @@ -20,13 +19,20 @@ public enum PluginMessageUtil { return message.getChannel().equals("MC|Brand") || message.getChannel().equals("minecraft:brand"); } + public static boolean isMCRegister(PluginMessage message) { + Preconditions.checkNotNull(message, "message"); + return message.getChannel().equals("REGISTER") || message.getChannel().equals("minecraft:register"); + } + + public static boolean isMCUnregister(PluginMessage message) { + Preconditions.checkNotNull(message, "message"); + return message.getChannel().equals("UNREGISTER") || message.getChannel().equals("minecraft:unregister"); + } + public static List getChannels(PluginMessage message) { Preconditions.checkNotNull(message, "message"); - Preconditions.checkArgument(message.getChannel().equals("REGISTER") || - message.getChannel().equals("UNREGISTER") || - message.getChannel().equals("minecraft:register") || - message.getChannel().equals("minecraft:unregister"), - "Unknown channel type " + message.getChannel()); + Preconditions.checkArgument(isMCRegister(message) || isMCUnregister(message),"Unknown channel type %s", + message.getChannel()); String channels = new String(message.getData(), StandardCharsets.UTF_8); return ImmutableList.copyOf(channels.split("\0")); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ScoreboardProtocolUtil.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ScoreboardProtocolUtil.java deleted file mode 100644 index 0cc15a61b..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ScoreboardProtocolUtil.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.velocitypowered.proxy.protocol.util; - -import com.velocitypowered.proxy.protocol.packet.ScoreboardObjective; - -public class ScoreboardProtocolUtil { - private ScoreboardProtocolUtil() { - throw new AssertionError(); - } - - public static ScoreboardObjective.ObjectiveMode getMode(String mode) { - return ScoreboardObjective.ObjectiveMode.valueOf(mode.toUpperCase()); - } - - public static ScoreboardObjective.ObjectiveMode getMode(int enumVal) { - switch (enumVal) { - case 0: - return ScoreboardObjective.ObjectiveMode.INTEGER; - case 1: - return ScoreboardObjective.ObjectiveMode.HEARTS; - default: - throw new IllegalStateException("Unknown mode " + enumVal); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java index 6e10807e3..d37abc0e3 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/scheduler/VelocityScheduler.java @@ -16,7 +16,6 @@ import java.util.Collection; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicReference; public class VelocityScheduler implements Scheduler { private final PluginManager pluginManager; @@ -66,13 +65,13 @@ public class VelocityScheduler implements Scheduler { } @Override - public TaskBuilder delay(int time, TimeUnit unit) { + public TaskBuilder delay(long time, TimeUnit unit) { this.delay = unit.toMillis(time); return this; } @Override - public TaskBuilder repeat(int time, TimeUnit unit) { + public TaskBuilder repeat(long time, TimeUnit unit) { this.repeat = unit.toMillis(time); return this; } @@ -91,62 +90,9 @@ public class VelocityScheduler implements Scheduler { @Override public ScheduledTask schedule() { - if (delay == 0 && repeat == 0) { - // A special purpose, simplified implementation - VelocityImmediatelyScheduledTask task = new VelocityImmediatelyScheduledTask(plugin, runnable); - tasksByPlugin.put(plugin, task); - taskService.execute(task); - return task; - } else { - VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat); - tasksByPlugin.put(plugin, task); - return task; - } - } - } - - private class VelocityImmediatelyScheduledTask implements ScheduledTask, Runnable { - private final Object plugin; - private final Runnable runnable; - private final AtomicReference status; - private Thread taskThread; - - private VelocityImmediatelyScheduledTask(Object plugin, Runnable runnable) { - this.plugin = plugin; - this.runnable = runnable; - this.status = new AtomicReference<>(TaskStatus.SCHEDULED); - } - - @Override - public Object plugin() { - return plugin; - } - - @Override - public TaskStatus status() { - return status.get(); - } - - @Override - public void cancel() { - if (status.compareAndSet(TaskStatus.SCHEDULED, TaskStatus.CANCELLED)) { - if (taskThread != null) { - taskThread.interrupt(); - } - } - } - - @Override - public void run() { - taskThread = Thread.currentThread(); - try { - runnable.run(); - } catch (Exception e) { - Log.logger.error("Exception in task {} by plugin {}", runnable, plugin); - } - status.compareAndSet(TaskStatus.SCHEDULED, TaskStatus.FINISHED); - taskThread = null; - tasksByPlugin.remove(plugin, this); + VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat); + tasksByPlugin.put(plugin, task); + return task; } } @@ -154,6 +100,7 @@ public class VelocityScheduler implements Scheduler { private final Object plugin; private final Runnable runnable; private ScheduledFuture future; + private volatile Thread currentTaskThread; private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) { this.plugin = plugin; @@ -191,6 +138,12 @@ public class VelocityScheduler implements Scheduler { public void cancel() { if (future != null) { future.cancel(false); + + Thread cur = currentTaskThread; + if (cur != null) { + cur.interrupt(); + } + onFinish(); } } @@ -198,6 +151,7 @@ public class VelocityScheduler implements Scheduler { @Override public void run() { taskService.execute(() -> { + currentTaskThread = Thread.currentThread(); try { runnable.run(); } catch (Exception e) { @@ -208,6 +162,7 @@ public class VelocityScheduler implements Scheduler { Log.logger.error("Exception in task {} by plugin {}", runnable, plugin); } } + currentTaskThread = null; }); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java b/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java new file mode 100644 index 000000000..2216387e7 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/ServerMap.java @@ -0,0 +1,49 @@ +package com.velocitypowered.proxy.server; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.proxy.VelocityServer; + +import java.util.Collection; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class ServerMap { + private final VelocityServer server; + private final Map servers = new ConcurrentHashMap<>(); + + public ServerMap(VelocityServer server) { + this.server = server; + } + + public Optional getServer(String name) { + Preconditions.checkNotNull(name, "server"); + String lowerName = name.toLowerCase(Locale.US); + return Optional.ofNullable(servers.get(lowerName)); + } + + public Collection getAllServers() { + return ImmutableList.copyOf(servers.values()); + } + + public RegisteredServer register(ServerInfo serverInfo) { + Preconditions.checkNotNull(serverInfo, "serverInfo"); + String lowerName = serverInfo.getName().toLowerCase(Locale.US); + VelocityRegisteredServer rs = new VelocityRegisteredServer(server, serverInfo); + Preconditions.checkArgument(servers.putIfAbsent(lowerName, rs) == null, "Server with name %s already registered", serverInfo.getName()); + return rs; + } + + public void unregister(ServerInfo serverInfo) { + Preconditions.checkNotNull(serverInfo, "serverInfo"); + String lowerName = serverInfo.getName().toLowerCase(Locale.US); + RegisteredServer rs = servers.get(lowerName); + Preconditions.checkArgument(rs != null, "Server with name %s is not registered!", serverInfo.getName()); + Preconditions.checkArgument(rs.getServerInfo().equals(serverInfo), "Trying to remove server %s with differing information", serverInfo.getName()); + Preconditions.checkState(servers.remove(lowerName, rs), "Server with name %s replaced whilst unregistering", serverInfo.getName()); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java new file mode 100644 index 000000000..7abd379f0 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -0,0 +1,112 @@ +package com.velocitypowered.proxy.server; + +import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.ServerConnection; +import com.velocitypowered.api.proxy.messages.ChannelIdentifier; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.api.proxy.server.ServerPing; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; +import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.server.ping.PingSessionHandler; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelInitializer; +import io.netty.handler.timeout.ReadTimeoutHandler; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import static com.velocitypowered.proxy.network.Connections.*; + +public class VelocityRegisteredServer implements RegisteredServer { + private final VelocityServer server; + private final ServerInfo serverInfo; + private final Set players = ConcurrentHashMap.newKeySet(); + + public VelocityRegisteredServer(VelocityServer server, ServerInfo serverInfo) { + this.server = server; + this.serverInfo = serverInfo; + } + + @Override + public ServerInfo getServerInfo() { + return serverInfo; + } + + @Override + public Collection getPlayersConnected() { + return ImmutableList.copyOf(players); + } + + @Override + public CompletableFuture ping() { + CompletableFuture pingFuture = new CompletableFuture<>(); + server.initializeGenericBootstrap() + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline() + .addLast(READ_TIMEOUT, new ReadTimeoutHandler(server.getConfiguration().getReadTimeout(), TimeUnit.SECONDS)) + .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolConstants.Direction.CLIENTBOUND)) + .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolConstants.Direction.SERVERBOUND)); + + MinecraftConnection connection = new MinecraftConnection(ch, server); + connection.setState(StateRegistry.HANDSHAKE); + ch.pipeline().addLast(HANDLER, connection); + } + }) + .connect(serverInfo.getAddress()) + .addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (future.isSuccess()) { + MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); + conn.setSessionHandler(new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn)); + } else { + pingFuture.completeExceptionally(future.cause()); + } + } + }); + return pingFuture; + } + + public void addPlayer(ConnectedPlayer player) { + players.add(player); + } + + public void removePlayer(ConnectedPlayer player) { + players.remove(player); + } + + @Override + public boolean sendPluginMessage(ChannelIdentifier identifier, byte[] data) { + for (ConnectedPlayer player : players) { + if (player.getConnectedServer() != null && player.getConnectedServer().getServerInfo().equals(serverInfo)) { + ServerConnection connection = player.getConnectedServer(); + return connection.sendPluginMessage(identifier, data); + } + } + + return false; + } + + @Override + public String toString() { + return "registered server: " + serverInfo; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/ping/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/ping/PingSessionHandler.java new file mode 100644 index 000000000..0bce28b74 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/ping/PingSessionHandler.java @@ -0,0 +1,68 @@ +package com.velocitypowered.proxy.server.ping; + +import com.google.common.base.Preconditions; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.proxy.server.ServerPing; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolConstants; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.Handshake; +import com.velocitypowered.proxy.protocol.packet.StatusRequest; +import com.velocitypowered.proxy.protocol.packet.StatusResponse; + +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public class PingSessionHandler implements MinecraftSessionHandler { + private final CompletableFuture result; + private final RegisteredServer server; + private final MinecraftConnection connection; + private boolean completed = false; + + public PingSessionHandler(CompletableFuture result, RegisteredServer server, MinecraftConnection connection) { + this.result = result; + this.server = server; + this.connection = connection; + } + + @Override + public void activated() { + Handshake handshake = new Handshake(); + handshake.setNextStatus(StateRegistry.STATUS_ID); + handshake.setServerAddress(server.getServerInfo().getAddress().getHostString()); + handshake.setPort(server.getServerInfo().getAddress().getPort()); + handshake.setProtocolVersion(ProtocolConstants.MINIMUM_GENERIC_VERSION); + connection.write(handshake); + + connection.setState(StateRegistry.STATUS); + connection.write(StatusRequest.INSTANCE); + } + + @Override + public void handle(MinecraftPacket packet) { + Preconditions.checkState(packet instanceof StatusResponse, "Did not get status response back from connection"); + + // All good! + completed = true; + connection.close(); + + ServerPing ping = VelocityServer.GSON.fromJson(((StatusResponse) packet).getStatus(), ServerPing.class); + result.complete(ping); + } + + @Override + public void disconnected() { + if (!completed) { + result.completeExceptionally(new IOException("Unexpectedly disconnected from remote server")); + } + } + + @Override + public void exception(Throwable throwable) { + completed = true; + result.completeExceptionally(throwable); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/ServerMap.java b/proxy/src/main/java/com/velocitypowered/proxy/util/ServerMap.java deleted file mode 100644 index 75784ef0b..000000000 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/ServerMap.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.velocitypowered.proxy.util; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.velocitypowered.api.proxy.server.ServerInfo; - -import java.util.*; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class ServerMap { - private final Map servers = new HashMap<>(); - private final ReadWriteLock lock = new ReentrantReadWriteLock(); - - public Optional getServer(String server) { - Preconditions.checkNotNull(server, "server"); - String lowerName = server.toLowerCase(Locale.US); - lock.readLock().lock(); - try { - return Optional.ofNullable(servers.get(lowerName)); - } finally { - lock.readLock().unlock(); - } - } - - public Collection getAllServers() { - lock.readLock().lock(); - try { - return ImmutableList.copyOf(servers.values()); - } finally { - lock.readLock().unlock(); - } - } - - public void register(ServerInfo server) { - Preconditions.checkNotNull(server, "server"); - String lowerName = server.getName().toLowerCase(Locale.US); - lock.writeLock().lock(); - try { - Preconditions.checkArgument(servers.putIfAbsent(lowerName, server) == null, "Server with name %s already registered", server.getName()); - } finally { - lock.writeLock().unlock(); - } - } - - public void unregister(ServerInfo server) { - Preconditions.checkNotNull(server, "server"); - String lowerName = server.getName().toLowerCase(Locale.US); - lock.writeLock().lock(); - try { - Preconditions.checkArgument(servers.remove(lowerName, server), "Server with this name is not registered!"); - } finally { - lock.writeLock().unlock(); - } - } -} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/RecordingThreadFactory.java b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/RecordingThreadFactory.java index ae289bb1c..1de4b165d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/RecordingThreadFactory.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/concurrency/RecordingThreadFactory.java @@ -6,7 +6,6 @@ import com.google.common.collect.MapMaker; import java.util.Collections; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadFactory; /** diff --git a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java index dac88e563..cc227f471 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/protocol/PacketRegistryTest.java @@ -3,9 +3,7 @@ package com.velocitypowered.proxy.protocol; import com.velocitypowered.proxy.protocol.packet.Handshake; import org.junit.jupiter.api.Test; -import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12; -import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_1; -import static com.velocitypowered.proxy.protocol.ProtocolConstants.MINECRAFT_1_12_2; +import static com.velocitypowered.proxy.protocol.ProtocolConstants.*; import static org.junit.jupiter.api.Assertions.*; class PacketRegistryTest { diff --git a/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java b/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java index d6d6896ee..7be0008c8 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/scheduler/VelocitySchedulerTest.java @@ -9,7 +9,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class VelocitySchedulerTest { // TODO: The timings here will be inaccurate on slow systems. Need to find a testing-friendly replacement for Thread.sleep() diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java index b9f9ff204..ba97fc493 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/RatelimiterTest.java @@ -7,7 +7,8 @@ import java.net.InetAddress; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; class RatelimiterTest { diff --git a/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java b/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java index e332ed85f..4105fdac7 100644 --- a/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java +++ b/proxy/src/test/java/com/velocitypowered/proxy/util/ServerMapTest.java @@ -1,31 +1,34 @@ package com.velocitypowered.proxy.util; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerInfo; +import com.velocitypowered.proxy.server.ServerMap; import org.junit.jupiter.api.Test; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Optional; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class ServerMapTest { private static final InetSocketAddress TEST_ADDRESS = new InetSocketAddress(InetAddress.getLoopbackAddress(), 25565); @Test void respectsCaseInsensitivity() { - ServerMap map = new ServerMap(); + ServerMap map = new ServerMap(null); ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS); - map.register(info); + RegisteredServer connection = map.register(info); - assertEquals(Optional.of(info), map.getServer("TestServer")); - assertEquals(Optional.of(info), map.getServer("testserver")); - assertEquals(Optional.of(info), map.getServer("TESTSERVER")); + assertEquals(Optional.of(connection), map.getServer("TestServer")); + assertEquals(Optional.of(connection), map.getServer("testserver")); + assertEquals(Optional.of(connection), map.getServer("TESTSERVER")); } @Test void rejectsRepeatedRegisterAttempts() { - ServerMap map = new ServerMap(); + ServerMap map = new ServerMap(null); ServerInfo info = new ServerInfo("TestServer", TEST_ADDRESS); map.register(info);