diff --git a/api/build.gradle b/api/build.gradle index 4a275b55f..4a7fd9130 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -5,6 +5,7 @@ plugins { } apply from: '../gradle/checkstyle.gradle' +apply from: '../gradle/publish.gradle' apply plugin: 'com.github.johnrengelman.shadow' sourceSets { @@ -30,7 +31,7 @@ dependencies { api "net.kyori:adventure-text-serializer-gson:${adventureVersion}" api "net.kyori:adventure-text-serializer-legacy:${adventureVersion}" api "net.kyori:adventure-text-serializer-plain:${adventureVersion}" - api "net.kyori:adventure-text-serializer-legacy-text3:${adventureVersion}" + api "net.kyori:adventure-text-serializer-legacy-text3:${adventurePlatformVersion}" api "org.slf4j:slf4j-api:${slf4jVersion}" api 'com.google.inject:guice:4.2.3' @@ -78,8 +79,8 @@ javadoc { 'http://www.slf4j.org/apidocs/', 'https://google.github.io/guava/releases/25.1-jre/api/docs/', 'https://google.github.io/guice/api-docs/4.2/javadoc/', - 'https://jd.kyori.net/text-api/3.0.0/', - 'https://docs.oracle.com/javase/8/docs/api/' + 'https://docs.oracle.com/javase/8/docs/api/', + 'https://jd.adventure.kyori.net/api/4.0.0/' ) // Disable the crazy super-strict doclint tool in Java 8 diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java index b688adc63..afe9356d5 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandManager.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandManager.java @@ -46,9 +46,7 @@ public interface CommandManager { * @param command the command to register * @param otherAliases additional aliases * @throws IllegalArgumentException if one of the given aliases is already registered - * @deprecated Prefer {@link #register(CommandMeta, Command)} instead. */ - @Deprecated void register(String alias, Command command, String... otherAliases); /** @@ -122,4 +120,12 @@ public interface CommandManager { * Can be completed exceptionally if an exception is thrown during execution. */ CompletableFuture executeImmediatelyAsync(CommandSource source, String cmdLine); + + /** + * Returns whether the given alias is registered on this manager. + * + * @param alias the command alias to check + * @return {@code true} if the alias is registered + */ + boolean hasCommand(String alias); } diff --git a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java index d2862d404..2402976cd 100644 --- a/api/src/main/java/com/velocitypowered/api/command/CommandSource.java +++ b/api/src/main/java/com/velocitypowered/api/command/CommandSource.java @@ -1,7 +1,10 @@ package com.velocitypowered.api.command; import com.velocitypowered.api.permission.PermissionSubject; -import com.velocitypowered.api.proxy.ProxyAudience; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.audience.MessageType; +import net.kyori.adventure.identity.Identified; +import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.legacytext3.LegacyText3ComponentSerializer; import org.checkerframework.checker.nullness.qual.NonNull; @@ -9,19 +12,21 @@ import org.checkerframework.checker.nullness.qual.NonNull; /** * Represents something that can be used to run a {@link Command}. */ -public interface CommandSource extends PermissionSubject, ProxyAudience { +public interface CommandSource extends Audience, PermissionSubject { /** * Sends the specified {@code component} to the invoker. * * @param component the text component to send - * @deprecated Use {@link #sendMessage(Component)} instead + * @deprecated Use {@link #sendMessage(Identified, Component)} + * or {@link #sendMessage(Identity, Component)} instead */ @Deprecated void sendMessage(net.kyori.text.Component component); @Override - default void sendMessage(@NonNull Component message) { + default void sendMessage(@NonNull Identity identity, @NonNull Component message, + @NonNull MessageType type) { this.sendMessage(LegacyText3ComponentSerializer.get().serialize(message)); } } diff --git a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java index 27c40d11f..ce5130078 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -40,9 +40,13 @@ public enum ProtocolVersion { MINECRAFT_1_16(735, "1.16"), MINECRAFT_1_16_1(736, "1.16.1"), MINECRAFT_1_16_2(751, "1.16.2"), - MINECRAFT_1_16_3(753, "1.16.3"); + MINECRAFT_1_16_3(753, "1.16.3"), + MINECRAFT_1_16_4(754, "1.16.4"); + + private static final int SNAPSHOT_BIT = 30; private final int protocol; + private final int snapshotProtocol; private final String name; /** @@ -68,7 +72,13 @@ public enum ProtocolVersion { static { Map versions = new HashMap<>(); for (ProtocolVersion version : values()) { - versions.put(version.protocol, version); + // For versions where the snapshot is compatible with the prior release version, Mojang will + // default to that. Follow that behavior since there is precedent (all the Minecraft 1.8 + // minor releases use the same protocol version). + versions.putIfAbsent(version.protocol, version); + if (version.snapshotProtocol != -1) { + versions.put(version.snapshotProtocol, version); + } } ID_TO_PROTOCOL_CONSTANT = ImmutableMap.copyOf(versions); @@ -92,6 +102,16 @@ public enum ProtocolVersion { } ProtocolVersion(int protocol, String name) { + this(protocol, -1, name); + } + + ProtocolVersion(int protocol, int snapshotProtocol, String name) { + if (snapshotProtocol != -1) { + this.snapshotProtocol = (1 << SNAPSHOT_BIT) | snapshotProtocol; + } else { + this.snapshotProtocol = -1; + } + this.protocol = protocol; this.name = name; } 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 dce8a5c4b..598cb07a6 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/Player.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/Player.java @@ -14,13 +14,15 @@ import com.velocitypowered.api.util.title.Title; import java.util.List; import java.util.Optional; import java.util.UUID; +import net.kyori.adventure.identity.Identified; +import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; /** * Represents a player who is connected to the proxy. */ -public interface Player extends CommandSource, InboundConnection, ChannelMessageSource, - ChannelMessageSink { +public interface Player extends CommandSource, Identified, InboundConnection, + ChannelMessageSource, ChannelMessageSink { /** * Returns the player's current username. @@ -76,7 +78,8 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage * Sends a chat message to the player's client. * * @param component the chat message to send - * @deprecated Use {@link #sendMessage(net.kyori.adventure.text.Component)} + * @deprecated Use {@link #sendMessage(Identified, Component)} + * or {@link #sendMessage(Identity, Component)} instead */ @Deprecated @Override @@ -89,8 +92,9 @@ public interface Player extends CommandSource, InboundConnection, ChannelMessage * * @param component the chat message to send * @param position the position for the message - * @deprecated Use @deprecated Use {@link #sendMessage(net.kyori.adventure.text.Component)} or - * {@link #sendActionBar(net.kyori.adventure.text.Component)} + * @deprecated Use @deprecated Use {@link #sendMessage(Identified, Component)} or + * {@link #sendMessage(Identity, Component)} for chat messages, or + * {@link #sendActionBar(net.kyori.adventure.text.Component)} for action bar messages */ @Deprecated void sendMessage(net.kyori.text.Component component, MessagePosition position); diff --git a/api/src/main/java/com/velocitypowered/api/proxy/ProxyAudience.java b/api/src/main/java/com/velocitypowered/api/proxy/ProxyAudience.java deleted file mode 100644 index 0b06b8f9d..000000000 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyAudience.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.velocitypowered.api.proxy; - -import net.kyori.adventure.audience.Audience; -import net.kyori.adventure.audience.MessageType; -import net.kyori.adventure.text.Component; -import org.checkerframework.checker.nullness.qual.NonNull; - -/** - * Indicates an {@link Audience} that is on the proxy. This interface contains no-op default methods - * that are used to bridge compatibility issues with the new adventure API. This interface will go - * away in Velocity 2.0.0. - * - * @deprecated Only used to handle compatibility problems, will go away in Velocity 2.0.0 - */ -@Deprecated -public interface ProxyAudience extends Audience { - - @Override - void sendMessage(@NonNull Component message); - - @Override - default void sendMessage(@NonNull Component message, @NonNull MessageType type) { - sendMessage(message); - } -} 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 d9ed1e4c8..f0b3876a0 100644 --- a/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java +++ b/api/src/main/java/com/velocitypowered/api/proxy/ProxyServer.java @@ -18,6 +18,9 @@ import java.util.Collection; import java.util.Optional; import java.util.UUID; import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.identity.Identified; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import org.checkerframework.checker.nullness.qual.NonNull; /** @@ -26,7 +29,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; public interface ProxyServer extends Audience { /** - * Shuts down the proxy, kicking players with the specified {@param reason}. + * Shuts down the proxy, kicking players with the specified {@code reason}. * * @param reason message to kick online players with */ @@ -58,7 +61,8 @@ public interface ProxyServer extends Audience { * Broadcasts a message to all players currently online. * * @param component the message to send - * @deprecated Use {@link #sendMessage(net.kyori.adventure.text.Component)} instead + * @deprecated Use {@link #sendMessage(Identified, Component)} + * or {@link #sendMessage(Identity, Component)} instead */ @Deprecated void broadcast(net.kyori.text.Component component); 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 f1979d491..dcb44f7ff 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 @@ -133,8 +133,9 @@ public final class ServerPing { /** * Returns a copy of this {@link ServerPing} instance as a builder so that it can be modified. - * It is guaranteed that {@code ping.asBuilder().ping().equals(ping)}: that is, if no other - * changes are made to the returned builder, the built instance will equal the original instance. + * It is guaranteed that {@code ping.asBuilder().build().equals(ping)} is true: that is, if no + * other changes are made to the returned builder, the built instance will equal the original + * instance. * * @return a copy of this instance as a {@link 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 index bedf52abb..e77c9e981 100644 --- 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 @@ -1,6 +1,6 @@ /** * Provides data structures for creating and manipulating titles. * - * @deprecated Replaced with {@link net.kyori.adventure.title} + * @deprecated Replaced with {@link net.kyori.adventure.title.Title} */ package com.velocitypowered.api.util.title; \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4d6810e9c..47f223b28 100644 --- a/build.gradle +++ b/build.gradle @@ -21,13 +21,14 @@ allprojects { ext { // dependency versions textVersion = '3.0.4' - adventureVersion = '4.0.0-SNAPSHOT' - junitVersion = '5.3.0-M1' - slf4jVersion = '1.7.25' + adventureVersion = '4.1.1' + adventurePlatformVersion = '4.0.0-SNAPSHOT' + junitVersion = '5.7.0' + slf4jVersion = '1.7.30' log4jVersion = '2.13.3' - nettyVersion = '4.1.51.Final' + nettyVersion = '4.1.52.Final' guavaVersion = '25.1-jre' - checkerFrameworkVersion = '2.7.0' + checkerFrameworkVersion = '3.6.1' configurateVersion = '3.7.1' getCurrentShortRevision = { diff --git a/gradle/publish.gradle b/gradle/publish.gradle new file mode 100644 index 000000000..cb94e0aa1 --- /dev/null +++ b/gradle/publish.gradle @@ -0,0 +1,16 @@ +publishing { + repositories { + maven { + credentials { + username System.getenv("NEXUS_USERNAME") + password System.getenv("NEXUS_PASSWORD") + } + + name = 'velocity-nexus' + def base = 'https://nexus.velocitypowered.com/repository/velocity-artifacts' + def releasesRepoUrl = "$base-releases/" + def snapshotsRepoUrl = "$base-snapshots/" + url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 01be9d5c3..71796f880 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip diff --git a/native/build.gradle b/native/build.gradle index 72ed54609..ed7cf0aa3 100644 --- a/native/build.gradle +++ b/native/build.gradle @@ -1,9 +1,11 @@ plugins { id 'java-library' id 'checkstyle' + id 'maven-publish' } apply from: '../gradle/checkstyle.gradle' +apply from: '../gradle/publish.gradle' dependencies { implementation "com.google.guava:guava:${guavaVersion}" @@ -12,4 +14,12 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}" testImplementation "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + } + } } \ No newline at end of file diff --git a/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java b/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java index ab3e79108..ac48b357b 100644 --- a/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java +++ b/native/src/main/java/com/velocitypowered/natives/compression/LibdeflateVelocityCompressor.java @@ -27,8 +27,6 @@ public class LibdeflateVelocityCompressor implements VelocityCompressor { public void inflate(ByteBuf source, ByteBuf destination, int uncompressedSize) throws DataFormatException { ensureNotDisposed(); - source.memoryAddress(); - destination.memoryAddress(); // libdeflate recommends we work with a known uncompressed size - so we work strictly within // those parameters. If the uncompressed size doesn't match the compressed size, then we will diff --git a/proxy/build.gradle b/proxy/build.gradle index 88175336f..e7eb88a84 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -56,16 +56,16 @@ dependencies { implementation 'net.sf.jopt-simple:jopt-simple:5.0.4' // command-line options implementation 'net.minecrell:terminalconsoleappender:1.2.0' - runtimeOnly 'org.jline:jline-terminal-jansi:3.12.1' // Needed for JLine + runtimeOnly 'org.jline:jline-terminal-jansi:3.16.0' // Needed for JLine runtimeOnly 'com.lmax:disruptor:3.4.2' // Async loggers - implementation 'it.unimi.dsi:fastutil:8.2.3' + implementation 'it.unimi.dsi:fastutil:8.4.1' implementation 'net.kyori:event-method-asm:4.0.0-SNAPSHOT' implementation 'net.kyori:adventure-nbt:4.0.0-SNAPSHOT' - implementation 'org.asynchttpclient:async-http-client:2.10.4' + implementation 'org.asynchttpclient:async-http-client:2.12.1' - implementation 'com.spotify:completable-futures:0.3.2' + implementation 'com.spotify:completable-futures:0.3.3' implementation 'com.electronwill.night-config:toml:3.6.3' diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index a641acfb1..863a481c7 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -344,14 +344,14 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { player.createConnectionRequest(next.get()).connectWithIndication() .whenComplete((success, ex) -> { if (ex != null || success == null || !success) { - player.disconnect(TextComponent.of("Your server has been changed, but we could " + player.disconnect(Component.text("Your server has been changed, but we could " + "not move you to any fallback servers.")); } latch.countDown(); }); } else { latch.countDown(); - player.disconnect(TextComponent.of("Your server has been changed, but we could " + player.disconnect(Component.text("Your server has been changed, but we could " + "not move you to any fallback servers.")); } } @@ -425,8 +425,11 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { .toArray((IntFunction[]>) CompletableFuture[]::new)); playersTeardownFuture.get(10, TimeUnit.SECONDS); - } catch (TimeoutException | ExecutionException e) { + } catch (TimeoutException e) { timedOut = true; + } catch (ExecutionException e) { + timedOut = true; + logger.error("Exception while tearing down player connections", e); } eventManager.fireShutdownEvent(); @@ -456,8 +459,12 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { } }; - Thread thread = new Thread(shutdownProcess); - thread.start(); + if (explicitExit) { + Thread thread = new Thread(shutdownProcess); + thread.start(); + } else { + shutdownProcess.run(); + } } /** @@ -466,7 +473,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { * @param explicitExit whether the user explicitly shut down the proxy */ public void shutdown(boolean explicitExit) { - shutdown(explicitExit, TextComponent.of("Proxy shutting down.")); + shutdown(explicitExit, Component.text("Proxy shutting down.")); } @Override @@ -520,7 +527,7 @@ public class VelocityServer implements ProxyServer, ForwardingAudience { } else { ConnectedPlayer existing = connectionsByUuid.get(connection.getUniqueId()); if (existing != null) { - existing.disconnect(TranslatableComponent.of("multiplayer.disconnect.duplicate_login")); + existing.disconnect(Component.translatable("multiplayer.disconnect.duplicate_login")); } // We can now replace the entries as needed. 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 cd5f034d7..92232a380 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/VelocityCommandManager.java @@ -24,6 +24,8 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.concurrent.CompletableFuture; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -161,7 +163,7 @@ public class VelocityCommandManager implements CommandManager { boolean isSyntaxError = !e.getType().equals( CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand()); if (isSyntaxError) { - source.sendMessage(TextComponent.of(e.getMessage(), NamedTextColor.RED)); + source.sendMessage(Identity.nil(), Component.text(e.getMessage(), NamedTextColor.RED)); // This is, of course, a lie, but the API will need to change... return true; } else { @@ -227,6 +229,7 @@ public class VelocityCommandManager implements CommandManager { * @param alias the command alias to check * @return {@code true} if the alias is registered */ + @Override public boolean hasCommand(final String alias) { Preconditions.checkNotNull(alias, "alias"); return dispatcher.getRoot().getChild(alias.toLowerCase(Locale.ENGLISH)) != null; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java index 207ee028f..ca1677a90 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/GlistCommand.java @@ -17,6 +17,8 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.server.RegisteredServer; import java.util.List; import java.util.Optional; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -58,10 +60,11 @@ public class GlistCommand { private int totalCount(final CommandContext context) { final CommandSource source = context.getSource(); sendTotalProxyCount(source); - source.sendMessage( - TextComponent.builder("To view all players on servers, use ", NamedTextColor.YELLOW) - .append("/glist all", NamedTextColor.DARK_AQUA) - .append(".", NamedTextColor.YELLOW) + source.sendMessage(Identity.nil(), + Component.text().content("To view all players on servers, use ") + .color(NamedTextColor.YELLOW) + .append(Component.text("/glist all", NamedTextColor.DARK_AQUA)) + .append(Component.text(".", NamedTextColor.YELLOW)) .build()); return 1; } @@ -77,8 +80,8 @@ public class GlistCommand { } else { Optional registeredServer = server.getServer(serverName); if (!registeredServer.isPresent()) { - source.sendMessage( - TextComponent.of("Server " + serverName + " doesn't exist.", NamedTextColor.RED)); + source.sendMessage(Identity.nil(), + Component.text("Server " + serverName + " doesn't exist.", NamedTextColor.RED)); return -1; } sendServerPlayers(source, registeredServer.get(), false); @@ -87,9 +90,10 @@ public class GlistCommand { } private void sendTotalProxyCount(CommandSource target) { - target.sendMessage(TextComponent.builder("There are ", NamedTextColor.YELLOW) - .append(Integer.toString(server.getAllPlayers().size()), NamedTextColor.GREEN) - .append(" player(s) online.", NamedTextColor.YELLOW) + target.sendMessage(Identity.nil(), Component.text() + .content("There are ").color(NamedTextColor.YELLOW) + .append(Component.text(server.getAllPlayers().size(), NamedTextColor.GREEN)) + .append(Component.text(" player(s) online.", NamedTextColor.YELLOW)) .build()); } @@ -99,22 +103,22 @@ public class GlistCommand { return; } - TextComponent.Builder builder = TextComponent.builder() - .append(TextComponent.of("[" + server.getServerInfo().getName() + "] ", + TextComponent.Builder builder = Component.text() + .append(Component.text("[" + server.getServerInfo().getName() + "] ", NamedTextColor.DARK_AQUA)) - .append("(" + onServer.size() + ")", NamedTextColor.GRAY) - .append(": ") + .append(Component.text("(" + onServer.size() + ")", NamedTextColor.GRAY)) + .append(Component.text(": ")) .resetStyle(); for (int i = 0; i < onServer.size(); i++) { Player player = onServer.get(i); - builder.append(player.getUsername()); + builder.append(Component.text(player.getUsername())); if (i + 1 < onServer.size()) { - builder.append(", "); + builder.append(Component.text(", ")); } } - target.sendMessage(builder.build()); + target.sendMessage(Identity.nil(), builder.build()); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java index afc9551ed..145f914b4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/ServerCommand.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.format.NamedTextColor; @@ -34,7 +36,7 @@ public class ServerCommand implements SimpleCommand { final String[] args = invocation.arguments(); if (!(source instanceof Player)) { - source.sendMessage(TextComponent.of("Only players may run this command.", + source.sendMessage(Identity.nil(), Component.text("Only players may run this command.", NamedTextColor.RED)); return; } @@ -45,8 +47,8 @@ public class ServerCommand implements SimpleCommand { String serverName = args[0]; Optional toConnect = server.getServer(serverName); if (!toConnect.isPresent()) { - player.sendMessage( - TextComponent.of("Server " + serverName + " doesn't exist.", NamedTextColor.RED)); + player.sendMessage(Identity.nil(), + Component.text("Server " + serverName + " doesn't exist.", NamedTextColor.RED)); return; } @@ -59,45 +61,45 @@ public class ServerCommand implements SimpleCommand { private void outputServerInformation(Player executor) { String currentServer = executor.getCurrentServer().map(ServerConnection::getServerInfo) .map(ServerInfo::getName).orElse(""); - executor.sendMessage(TextComponent.of("You are currently connected to " + currentServer + ".", - NamedTextColor.YELLOW)); + executor.sendMessage(Identity.nil(), Component.text( + "You are currently connected to " + currentServer + ".", NamedTextColor.YELLOW)); List servers = BuiltinCommandUtil.sortedServerList(server); if (servers.size() > MAX_SERVERS_TO_LIST) { - executor.sendMessage(TextComponent.of( + executor.sendMessage(Identity.nil(), Component.text( "Too many servers to list. Tab-complete to show all servers.", NamedTextColor.RED)); return; } // Assemble the list of servers as components - TextComponent.Builder serverListBuilder = TextComponent.builder("Available servers: ") + TextComponent.Builder serverListBuilder = Component.text().content("Available servers: ") .color(NamedTextColor.YELLOW); for (int i = 0; i < servers.size(); i++) { RegisteredServer rs = servers.get(i); serverListBuilder.append(formatServerComponent(currentServer, rs)); if (i != servers.size() - 1) { - serverListBuilder.append(TextComponent.of(", ", NamedTextColor.GRAY)); + serverListBuilder.append(Component.text(", ", NamedTextColor.GRAY)); } } - executor.sendMessage(serverListBuilder.build()); + executor.sendMessage(Identity.nil(), serverListBuilder.build()); } private TextComponent formatServerComponent(String currentPlayerServer, RegisteredServer server) { ServerInfo serverInfo = server.getServerInfo(); - TextComponent serverTextComponent = TextComponent.of(serverInfo.getName()); + TextComponent serverTextComponent = Component.text(serverInfo.getName()); String playersText = server.getPlayersConnected().size() + " player(s) online"; if (serverInfo.getName().equals(currentPlayerServer)) { serverTextComponent = serverTextComponent.color(NamedTextColor.GREEN) .hoverEvent( - showText(TextComponent.of("Currently connected to this server\n" + playersText)) + showText(Component.text("Currently connected to this server\n" + playersText)) ); } else { serverTextComponent = serverTextComponent.color(NamedTextColor.GRAY) .clickEvent(ClickEvent.runCommand("/server " + serverInfo.getName())) .hoverEvent( - showText(TextComponent.of("Click to connect to this server\n" + playersText)) + showText(Component.text("Click to connect to this server\n" + playersText)) ); } return serverTextComponent; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java index 1a0466f58..3218960b9 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/command/builtin/VelocityCommand.java @@ -3,19 +3,37 @@ package com.velocitypowered.proxy.command.builtin; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; + +import com.google.common.collect.ImmutableSet; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSyntaxException; import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.command.SimpleCommand; import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.PluginDescription; import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.util.ProxyVersion; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.util.InformationUtils; + +import java.net.ConnectException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; + +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEvent; @@ -23,6 +41,10 @@ import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextDecoration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.BoundRequestBuilder; +import org.asynchttpclient.ListenableFuture; +import org.asynchttpclient.Response; import org.checkerframework.checker.nullness.qual.NonNull; public class VelocityCommand implements SimpleCommand { @@ -50,6 +72,7 @@ public class VelocityCommand implements SimpleCommand { .put("version", new Info(server)) .put("plugins", new Plugins(server)) .put("reload", new Reload(server)) + .put("dump", new Dump(server)) .build(); } @@ -59,7 +82,7 @@ public class VelocityCommand implements SimpleCommand { .map(Map.Entry::getKey) .collect(Collectors.joining("|")); String commandText = "/velocity <" + availableCommands + ">"; - source.sendMessage(TextComponent.of(commandText, NamedTextColor.RED)); + source.sendMessage(Identity.nil(), Component.text(commandText, NamedTextColor.RED)); } @Override @@ -142,15 +165,16 @@ public class VelocityCommand implements SimpleCommand { public void execute(CommandSource source, String @NonNull [] args) { try { if (server.reloadConfiguration()) { - source.sendMessage(TextComponent.of("Configuration reloaded.", NamedTextColor.GREEN)); + source.sendMessage(Identity.nil(), Component.text( + "Configuration reloaded.", NamedTextColor.GREEN)); } else { - source.sendMessage(TextComponent.of( + source.sendMessage(Identity.nil(), Component.text( "Unable to reload your configuration. Check the console for more details.", NamedTextColor.RED)); } } catch (Exception e) { logger.error("Unable to reload configuration", e); - source.sendMessage(TextComponent.of( + source.sendMessage(Identity.nil(), Component.text( "Unable to reload your configuration. Check the console for more details.", NamedTextColor.RED)); } @@ -173,39 +197,39 @@ public class VelocityCommand implements SimpleCommand { @Override public void execute(CommandSource source, String @NonNull [] args) { if (args.length != 0) { - source.sendMessage(TextComponent.of("/velocity version", NamedTextColor.RED)); + source.sendMessage(Identity.nil(), Component.text("/velocity version", NamedTextColor.RED)); return; } ProxyVersion version = server.getVersion(); - TextComponent velocity = TextComponent.builder(version.getName() + " ") + TextComponent velocity = Component.text().content(version.getName() + " ") .decoration(TextDecoration.BOLD, true) .color(NamedTextColor.DARK_AQUA) - .append(TextComponent.of(version.getVersion()).decoration(TextDecoration.BOLD, false)) + .append(Component.text(version.getVersion()).decoration(TextDecoration.BOLD, false)) .build(); - TextComponent copyright = TextComponent - .of("Copyright 2018-2020 " + version.getVendor() + ". " + version.getName() + TextComponent copyright = Component + .text("Copyright 2018-2020 " + version.getVendor() + ". " + version.getName() + " is freely licensed under the terms of the MIT License."); - source.sendMessage(velocity); - source.sendMessage(copyright); + source.sendMessage(Identity.nil(), velocity); + source.sendMessage(Identity.nil(), copyright); if (version.getName().equals("Velocity")) { - TextComponent velocityWebsite = TextComponent.builder() + TextComponent velocityWebsite = Component.text() .content("Visit the ") - .append(TextComponent.builder("Velocity website") + .append(Component.text().content("Velocity website") .color(NamedTextColor.GREEN) .clickEvent( ClickEvent.openUrl("https://www.velocitypowered.com")) .build()) - .append(TextComponent.of(" or the ")) - .append(TextComponent.builder("Velocity GitHub") + .append(Component.text(" or the ")) + .append(Component.text().content("Velocity GitHub") .color(NamedTextColor.GREEN) .clickEvent(ClickEvent.openUrl( "https://github.com/VelocityPowered/Velocity")) .build()) .build(); - source.sendMessage(velocityWebsite); + source.sendMessage(Identity.nil(), velocityWebsite); } } @@ -226,7 +250,7 @@ public class VelocityCommand implements SimpleCommand { @Override public void execute(CommandSource source, String @NonNull [] args) { if (args.length != 0) { - source.sendMessage(TextComponent.of("/velocity plugins", NamedTextColor.RED)); + source.sendMessage(Identity.nil(), Component.text("/velocity plugins", NamedTextColor.RED)); return; } @@ -234,49 +258,50 @@ public class VelocityCommand implements SimpleCommand { int pluginCount = plugins.size(); if (pluginCount == 0) { - source.sendMessage(TextComponent.of("No plugins installed.", NamedTextColor.YELLOW)); + source.sendMessage(Identity.nil(), Component.text( + "No plugins installed.", NamedTextColor.YELLOW)); return; } - TextComponent.Builder output = TextComponent.builder("Plugins: ") + TextComponent.Builder output = Component.text().content("Plugins: ") .color(NamedTextColor.YELLOW); for (int i = 0; i < pluginCount; i++) { PluginContainer plugin = plugins.get(i); output.append(componentForPlugin(plugin.getDescription())); if (i + 1 < pluginCount) { - output.append(TextComponent.of(", ")); + output.append(Component.text(", ")); } } - source.sendMessage(output.build()); + source.sendMessage(Identity.nil(), output.build()); } private TextComponent componentForPlugin(PluginDescription description) { String pluginInfo = description.getName().orElse(description.getId()) + description.getVersion().map(v -> " " + v).orElse(""); - TextComponent.Builder hoverText = TextComponent.builder(pluginInfo); + TextComponent.Builder hoverText = Component.text().content(pluginInfo); description.getUrl().ifPresent(url -> { - hoverText.append(TextComponent.newline()); - hoverText.append(TextComponent.of("Website: " + url)); + hoverText.append(Component.newline()); + hoverText.append(Component.text("Website: " + url)); }); if (!description.getAuthors().isEmpty()) { - hoverText.append(TextComponent.newline()); + hoverText.append(Component.newline()); if (description.getAuthors().size() == 1) { - hoverText.append(TextComponent.of("Author: " + description.getAuthors().get(0))); + hoverText.append(Component.text("Author: " + description.getAuthors().get(0))); } else { - hoverText.append(TextComponent.of("Authors: " + Joiner.on(", ") + hoverText.append(Component.text("Authors: " + Joiner.on(", ") .join(description.getAuthors()))); } } description.getDescription().ifPresent(pdesc -> { - hoverText.append(TextComponent.newline()); - hoverText.append(TextComponent.newline()); - hoverText.append(TextComponent.of(pdesc)); + hoverText.append(Component.newline()); + hoverText.append(Component.newline()); + hoverText.append(Component.text(pdesc)); }); - return TextComponent.of(description.getId(), NamedTextColor.GRAY) + return Component.text(description.getId(), NamedTextColor.GRAY) .hoverEvent(HoverEvent.showText(hoverText.build())); } @@ -285,4 +310,140 @@ public class VelocityCommand implements SimpleCommand { return source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE; } } + + private static class Dump implements SubCommand { + + private static final Logger logger = LogManager.getLogger(Dump.class); + private final ProxyServer server; + + private Dump(ProxyServer server) { + this.server = server; + } + + @Override + public void execute(CommandSource source, String @NonNull [] args) { + if (args.length != 0) { + source.sendMessage(Identity.nil(), Component.text("/velocity dump", NamedTextColor.RED)); + return; + } + + Collection allServers = ImmutableSet.copyOf(server.getAllServers()); + JsonObject servers = new JsonObject(); + for (RegisteredServer iter : allServers) { + servers.add(iter.getServerInfo().getName(), + InformationUtils.collectServerInfo(iter)); + } + JsonArray connectOrder = new JsonArray(); + List attemptedConnectionOrder = ImmutableList.copyOf( + server.getConfiguration().getAttemptConnectionOrder()); + for (int i = 0; i < attemptedConnectionOrder.size(); i++) { + connectOrder.add(attemptedConnectionOrder.get(i)); + } + + JsonObject proxyConfig = InformationUtils.collectProxyConfig(server.getConfiguration()); + proxyConfig.add("servers", servers); + proxyConfig.add("connectOrder", connectOrder); + proxyConfig.add("forcedHosts", + InformationUtils.collectForcedHosts(server.getConfiguration())); + + JsonObject dump = new JsonObject(); + dump.add("versionInfo", InformationUtils.collectProxyInfo(server.getVersion())); + dump.add("platform", InformationUtils.collectEnvironmentInfo()); + dump.add("config", proxyConfig); + dump.add("plugins", InformationUtils.collectPluginInfo(server)); + + source.sendMessage(Component.text().content("Uploading gathered information...").build()); + AsyncHttpClient httpClient = ((VelocityServer) server).getAsyncHttpClient(); + + BoundRequestBuilder request = + httpClient.preparePost("https://dump.velocitypowered.com/documents"); + request.setHeader("Content-Type", "text/plain"); + request.addHeader("User-Agent", server.getVersion().getName() + "/" + + server.getVersion().getVersion()); + request.setBody( + InformationUtils.toHumanReadableString(dump).getBytes(StandardCharsets.UTF_8)); + + ListenableFuture future = request.execute(); + future.addListener(() -> { + try { + Response response = future.get(); + if (response.getStatusCode() != 200) { + source.sendMessage(Component.text() + .content("An error occurred while communicating with the Velocity servers. " + + "The servers may be temporarily unavailable or there is an issue " + + "with your network settings. You can find more information in the " + + "log or console of your Velocity server.") + .color(NamedTextColor.RED).build()); + logger.error("Invalid status code while POST-ing Velocity dump: " + + response.getStatusCode()); + logger.error("Headers: \n--------------BEGIN HEADERS--------------\n" + + response.getHeaders().toString() + + "\n---------------END HEADERS---------------"); + return; + } + JsonObject key = InformationUtils.parseString( + response.getResponseBody(StandardCharsets.UTF_8)); + if (!key.has("key")) { + throw new JsonSyntaxException("Missing Dump-Url-response"); + } + String url = "https://dump.velocitypowered.com/" + + key.get("key").getAsString() + ".json"; + source.sendMessage(Component.text() + .content("Created an anonymised report containing useful information about " + + "this proxy. If a developer requested it, you may share the " + + "following link with them:") + .append(Component.newline()) + .append(Component.text(">> " + url) + .color(NamedTextColor.GREEN) + .clickEvent(ClickEvent.openUrl(url))) + .append(Component.newline()) + .append(Component.text("Note: This link is only valid for a few days") + .color(NamedTextColor.GRAY) + ).build()); + } catch (InterruptedException e) { + source.sendMessage(Component.text() + .content("Could not complete the request, the command was interrupted." + + "Please refer to the proxy-log or console for more information.") + .color(NamedTextColor.RED).build()); + logger.error("Failed to complete dump command, " + + "the executor was interrupted: " + e.getMessage()); + e.printStackTrace(); + } catch (ExecutionException e) { + TextComponent.Builder message = Component.text() + .content("An error occurred while attempting to upload the gathered " + + "information to the Velocity servers.") + .append(Component.newline()) + .color(NamedTextColor.RED); + if (e.getCause() instanceof UnknownHostException + || e.getCause() instanceof ConnectException) { + message.append(Component.text( + "Likely cause: Invalid system DNS settings or no internet connection")); + } + source.sendMessage(message + .append(Component.newline() + .append(Component.text( + "Error details can be found in the proxy log / console")) + ).build()); + + logger.error("Failed to complete dump command, " + + "the executor encountered an Exception: " + e.getCause().getMessage()); + e.getCause().printStackTrace(); + } catch (JsonParseException e) { + source.sendMessage(Component.text() + .content("An error occurred on the Velocity-servers and the dump could not " + + "be completed. Please contact the Velocity staff about this problem. " + + "If you do, provide the details about this error from the Velocity " + + "console or server log.") + .color(NamedTextColor.RED).build()); + logger.error("Invalid response from the Velocity servers: " + e.getMessage()); + e.printStackTrace(); + } + }, MoreExecutors.directExecutor()); + } + + @Override + public boolean hasPermission(final CommandSource source, final String @NonNull [] args) { + return source.getPermissionValue("velocity.command.plugins") == Tristate.TRUE; + } + } } 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 a3f7537f2..5660b913e 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -7,6 +7,7 @@ import com.electronwill.nightconfig.toml.TomlFormat; import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gson.annotations.Expose; import com.velocitypowered.api.proxy.config.ProxyConfig; import com.velocitypowered.api.util.Favicon; import com.velocitypowered.proxy.util.AddressUtil; @@ -42,20 +43,20 @@ public class VelocityConfiguration implements ProxyConfig { private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class); - private String bind = "0.0.0.0:25577"; - private String motd = "&3A Velocity Server"; - private int showMaxPlayers = 500; - private boolean onlineMode = true; - private boolean preventClientProxyConnections = false; - private PlayerInfoForwarding playerInfoForwardingMode = PlayerInfoForwarding.NONE; + @Expose private String bind = "0.0.0.0:25577"; + @Expose private String motd = "&3A Velocity Server"; + @Expose private int showMaxPlayers = 500; + @Expose private boolean onlineMode = true; + @Expose private boolean preventClientProxyConnections = false; + @Expose private PlayerInfoForwarding playerInfoForwardingMode = PlayerInfoForwarding.NONE; private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8); - private boolean announceForge = false; - private boolean onlineModeKickExistingPlayers = false; - private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; + @Expose private boolean announceForge = false; + @Expose private boolean onlineModeKickExistingPlayers = false; + @Expose private PingPassthroughMode pingPassthrough = PingPassthroughMode.DISABLED; private final Servers servers; private final ForcedHosts forcedHosts; - private final Advanced advanced; - private final Query query; + @Expose private final Advanced advanced; + @Expose private final Query query; private final Metrics metrics; private final Messages messages; private net.kyori.adventure.text.@MonotonicNonNull Component motdAsComponent; @@ -622,18 +623,18 @@ public class VelocityConfiguration implements ProxyConfig { private static class Advanced { - private int compressionThreshold = 256; - private int compressionLevel = -1; - private int loginRatelimit = 3000; - private int connectionTimeout = 5000; - private int readTimeout = 30000; - private boolean proxyProtocol = false; - private boolean tcpFastOpen = false; - private boolean bungeePluginMessageChannel = true; - private boolean showPingRequests = false; - private boolean failoverOnUnexpectedServerDisconnect = true; - private boolean announceProxyCommands = true; - private boolean logCommandExecutions = false; + @Expose private int compressionThreshold = 256; + @Expose private int compressionLevel = -1; + @Expose private int loginRatelimit = 3000; + @Expose private int connectionTimeout = 5000; + @Expose private int readTimeout = 30000; + @Expose private boolean proxyProtocol = false; + @Expose private boolean tcpFastOpen = false; + @Expose private boolean bungeePluginMessageChannel = true; + @Expose private boolean showPingRequests = false; + @Expose private boolean failoverOnUnexpectedServerDisconnect = true; + @Expose private boolean announceProxyCommands = true; + @Expose private boolean logCommandExecutions = false; private Advanced() { } @@ -725,10 +726,10 @@ public class VelocityConfiguration implements ProxyConfig { private static class Query { - private boolean queryEnabled = false; - private int queryPort = 25577; - private String queryMap = "Velocity"; - private boolean showPlugins = false; + @Expose private boolean queryEnabled = false; + @Expose private int queryPort = 25577; + @Expose private String queryMap = "Velocity"; + @Expose private boolean showPlugins = false; private Query() { } 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 bbba322d9..abd1f7b91 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 @@ -149,7 +149,11 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { Unpooled.wrappedBuffer(copy)); playerConnection.write(copied); } - }, playerConnection.eventLoop()); + }, playerConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling plugin message {}", packet, ex); + return null; + }); return true; } @@ -186,7 +190,11 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { server.getEventManager().fire( new PlayerAvailableCommandsEvent(serverConn.getPlayer(), rootNode)) - .thenAcceptAsync(event -> playerConnection.write(commands), playerConnection.eventLoop()); + .thenAcceptAsync(event -> playerConnection.write(commands), playerConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling available commands for {}", playerConnection, ex); + return null; + }); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java index 9cfc0f371..e8896cf63 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BungeeCordMessageResponder.java @@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.util.Optional; import java.util.StringJoiner; +import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.ComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -155,9 +156,10 @@ class BungeeCordMessageResponder { Component messageComponent = serializer.deserialize(message); if (target.equals("ALL")) { - proxy.sendMessage(messageComponent); + proxy.sendMessage(Identity.nil(), messageComponent); } else { - proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent)); + proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(), + messageComponent)); } } 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 15980b863..80e6bd7ce 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 @@ -20,18 +20,20 @@ import com.velocitypowered.proxy.protocol.packet.SetCompression; import com.velocitypowered.proxy.util.except.QuietRuntimeException; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import java.net.InetSocketAddress; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.concurrent.CompletableFuture; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; public class LoginSessionHandler implements MinecraftSessionHandler { - private static final TextComponent MODERN_IP_FORWARDING_FAILURE = TextComponent - .of("Your server did not send a forwarding request to the proxy. Is it set up correctly?"); + private static final TextComponent MODERN_IP_FORWARDING_FAILURE = Component + .text("Your server did not send a forwarding request to the proxy. Is it set up correctly?"); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -57,7 +59,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { if (configuration.getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && packet .getChannel().equals(VelocityConstants.VELOCITY_IP_FORWARDING_CHANNEL)) { ByteBuf forwardingData = createForwardingData(configuration.getForwardingSecret(), - serverConn.getPlayer().getRemoteAddress().getHostString(), + cleanRemoteAddress(serverConn.getPlayer().getRemoteAddress()), serverConn.getPlayer().getGameProfile()); LoginPluginResponse response = new LoginPluginResponse(packet.getId(), true, forwardingData); mc.write(response); @@ -126,6 +128,16 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } } + private static String cleanRemoteAddress(InetSocketAddress address) { + String addressString = address.getAddress().getHostAddress(); + int ipv6ScopeIdx = addressString.indexOf('%'); + if (ipv6ScopeIdx == -1) { + return addressString; + } else { + return addressString.substring(0, ipv6ScopeIdx); + } + } + private static ByteBuf createForwardingData(byte[] hmacSecret, String address, GameProfile profile) { ByteBuf forwarded = Unpooled.buffer(2048); 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 e44f83ce6..956772000 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,6 +1,7 @@ package com.velocitypowered.proxy.connection.client; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_13; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_16; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.proxy.protocol.util.PluginMessageUtil.constructChannelsPacket; @@ -43,7 +44,8 @@ import java.util.Optional; import java.util.Queue; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -137,8 +139,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { .exceptionally(e -> { logger.info("Exception occurred while running command for {}", player.getUsername(), e); - player.sendMessage( - TextComponent.of("An error occurred while running this command.", + player.sendMessage(Identity.nil(), + Component.text("An error occurred while running this command.", NamedTextColor.RED)); return null; }); @@ -155,7 +157,11 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { smc.write(packet); } } - }, smc.eventLoop()); + }, smc.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling player chat for {}", player, ex); + return null; + }); } return true; } @@ -223,7 +229,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { Unpooled.wrappedBuffer(copy)); backendConn.write(message); } - }, backendConn.eventLoop()); + }, backendConn.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling plugin message packet for {}", + player, ex); + return null; + }); } } } @@ -328,20 +339,18 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // Most notably, by having the client accept the join game packet, we can work around the need // to perform entity ID rewrites, eliminating potential issues from rewriting packets and // improving compatibility with mods. - player.getConnection().delayedWrite(joinGame); - // Since 1.16 this dynamic changed: - // We don't need to send two dimension swiches anymore! - if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_16) < 0) { - int tempDim = joinGame.getDimension() == 0 ? -1 : 0; - player.getConnection().delayedWrite( - new Respawn(tempDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), - joinGame.getGamemode(), joinGame.getLevelType(), - false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), - joinGame.getCurrentDimensionData())); + + int sentOldDim = joinGame.getDimension(); + if (player.getProtocolVersion().compareTo(MINECRAFT_1_16) < 0) { + // Before Minecraft 1.16, we could not switch to the same dimension without sending an + // additional respawn. On older versions of Minecraft this forces the client to perform + // garbage collection which adds additional latency. + joinGame.setDimension(joinGame.getDimension() == 0 ? -1 : 0); } + player.getConnection().delayedWrite(joinGame); player.getConnection().delayedWrite( - new Respawn(joinGame.getDimension(), joinGame.getPartialHashedSeed(), + new Respawn(sentOldDim, joinGame.getPartialHashedSeed(), joinGame.getDifficulty(), joinGame.getGamemode(), joinGame.getLevelType(), false, joinGame.getDimensionInfo(), joinGame.getPreviousGamemode(), joinGame.getCurrentDimensionData())); @@ -423,7 +432,12 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { resp.getOffers().addAll(offers); player.getConnection().write(resp); } - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling command tab completion for player {} executing {}", + player, command, ex); + return null; + }); return true; // Sorry, handler; we're just gonna have to lie to you here. } @@ -475,7 +489,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getUsername(), command, e); } - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()) + .exceptionally((ex) -> { + logger.error( + "Exception while finishing command tab completion, with request {} and response {}", + request, response, ex); + return null; + }); } private void finishRegularTabComplete(TabCompleteRequest request, TabCompleteResponse response) { @@ -490,7 +510,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { response.getOffers().add(new Offer(s)); } player.getConnection().write(response); - }, player.getConnection().eventLoop()); + }, player.getConnection().eventLoop()) + .exceptionally((ex) -> { + logger.error( + "Exception while finishing regular tab completion, with request {} and response{}", + request, response, ex); + return null; + }); } private CompletableFuture processCommandExecuteResult(String originalCommand, 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 c97e492c4..f635babf5 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 @@ -68,8 +68,8 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ThreadLocalRandom; import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.bossbar.BossBar; +import net.kyori.adventure.identity.Identity; import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -90,6 +90,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class); + private final Identity identity = new IdentityImpl(); /** * The actual Minecraft connection. This is actually a wrapper object around the Netty channel. */ @@ -128,6 +129,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { this.onlineMode = onlineMode; } + @Override + public @NonNull Identity identity() { + return this.identity; + } + @Override public String getUsername() { return profile.getName(); @@ -258,27 +264,29 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } @Override - public void sendMessage(net.kyori.adventure.text.@NonNull Component message) { - connection.write(Chat.createClientbound(message, this.getProtocolVersion())); + public void sendMessage(@NonNull Identity identity, @NonNull Component message) { + connection.write(Chat.createClientbound(identity, message, this.getProtocolVersion())); } @Override - public void sendMessage(@NonNull Component message, @NonNull MessageType type) { + public void sendMessage(@NonNull Identity identity, @NonNull Component message, + @NonNull MessageType type) { Preconditions.checkNotNull(message, "message"); Preconditions.checkNotNull(type, "type"); - Chat packet = Chat.createClientbound(message, this.getProtocolVersion()); + Chat packet = Chat.createClientbound(identity, message, this.getProtocolVersion()); packet.setType(type == MessageType.CHAT ? Chat.CHAT_TYPE : Chat.SYSTEM_TYPE); connection.write(packet); } @Override public void sendActionBar(net.kyori.adventure.text.@NonNull Component message) { - if (getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0) { - // We can use the title packet instead. + ProtocolVersion playerVersion = getProtocolVersion(); + if (playerVersion.compareTo(ProtocolVersion.MINECRAFT_1_11) >= 0) { + // Use the title packet instead. TitlePacket pkt = new TitlePacket(); pkt.setAction(TitlePacket.SET_ACTION_BAR); - pkt.setComponent(ProtocolUtils.getJsonChatSerializer(this.getProtocolVersion()) + pkt.setComponent(ProtocolUtils.getJsonChatSerializer(playerVersion) .serialize(message)); connection.write(pkt); } else { @@ -288,7 +296,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { object.addProperty("text", LegacyComponentSerializer.legacySection().serialize(message)); Chat chat = new Chat(); chat.setMessage(object.toString()); - chat.setType((byte) 1); + chat.setType(Chat.GAME_INFO_TYPE); connection.write(chat); } } @@ -311,9 +319,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { TitlePacket timesPkt = TitlePacket.timesForProtocolVersion(this.getProtocolVersion()); net.kyori.adventure.title.Title.Times times = title.times(); if (times != null) { - timesPkt.setFadeIn((int) DurationUtils.convertDurationToTicks(times.fadeIn())); - timesPkt.setStay((int) DurationUtils.convertDurationToTicks(times.stay())); - timesPkt.setFadeOut((int) DurationUtils.convertDurationToTicks(times.fadeOut())); + timesPkt.setFadeIn((int) DurationUtils.toTicks(times.fadeIn())); + timesPkt.setStay((int) DurationUtils.toTicks(times.stay())); + timesPkt.setFadeOut((int) DurationUtils.toTicks(times.fadeOut())); } connection.delayedWrite(timesPkt); @@ -504,7 +512,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { userMessage = "Unable to connect to " + server.getServerInfo().getName() + ". Try again " + "later."; } - handleConnectionException(server, null, TextComponent.of(userMessage, + handleConnectionException(server, null, Component.text(userMessage, NamedTextColor.RED), safe); } @@ -527,7 +535,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { if (connectedServer != null && connectedServer.getServerInfo().equals(server.getServerInfo())) { logger.error("{}: kicked from server {}: {}", this, server.getServerInfo().getName(), plainTextReason); - handleConnectionException(server, disconnectReason, TextComponent.builder() + handleConnectionException(server, disconnectReason, Component.text() .append(messages.getKickPrefix(server.getServerInfo().getName())) .color(NamedTextColor.RED) .append(disconnectReason) @@ -535,7 +543,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } else { logger.error("{}: disconnected while connecting to {}: {}", this, server.getServerInfo().getName(), plainTextReason); - handleConnectionException(server, disconnectReason, TextComponent.builder() + handleConnectionException(server, disconnectReason, Component.text() .append(messages.getDisconnectPrefix(server.getServerInfo().getName())) .color(NamedTextColor.RED) .append(disconnectReason) @@ -623,7 +631,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { getProtocolVersion()), ((Impl) status).isSafe()); break; case SUCCESS: - sendMessage(server.getConfiguration().getMessages() + sendMessage(Identity.nil(), server.getConfiguration().getMessages() .getMovedToNewServerPrefix().append(friendlyReason)); break; default: @@ -634,7 +642,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } else if (event.getResult() instanceof Notify) { Notify res = (Notify) event.getResult(); if (event.kickedDuringServerConnect() && previouslyConnected) { - sendMessage(res.getMessageComponent()); + sendMessage(Identity.nil(), res.getMessageComponent()); } else { disconnect(res.getMessageComponent()); } @@ -749,7 +757,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { } DisconnectEvent event = new DisconnectEvent(this, status); - server.getEventManager().fire(event).thenRun(() -> this.teardownFuture.complete(null)); + server.getEventManager().fire(event).whenComplete((val, ex) -> { + if (ex == null) { + this.teardownFuture.complete(null); + } else { + this.teardownFuture.completeExceptionally(ex); + } + }); } public CompletableFuture getTeardownFuture() { @@ -871,6 +885,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { return minecraftOrFmlMessage || knownChannels.contains(message.getChannel()); } + private class IdentityImpl implements Identity { + @Override + public @NonNull UUID uuid() { + return ConnectedPlayer.this.getUniqueId(); + } + } + private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private final RegisteredServer toConnect; @@ -967,10 +988,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player { switch (status.getStatus()) { case ALREADY_CONNECTED: - sendMessage(ConnectionMessages.ALREADY_CONNECTED); + sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); break; case CONNECTION_IN_PROGRESS: - sendMessage(ConnectionMessages.IN_PROGRESS); + sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); break; case CONNECTION_CANCELLED: // Ignored; the plugin probably already handled this. 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 83a25d4e8..632181582 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 @@ -22,6 +22,7 @@ import io.netty.buffer.ByteBuf; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Optional; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.format.NamedTextColor; @@ -54,7 +55,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyHandshake packet) { connection.closeWith(LegacyDisconnect - .from(TextComponent.of("Your client is old, please upgrade!", NamedTextColor.RED))); + .from(Component.text("Your client is old, please upgrade!", NamedTextColor.RED))); return true; } @@ -100,13 +101,13 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { private void handleLogin(Handshake handshake, InitialInboundConnection ic) { if (!ProtocolVersion.isSupported(handshake.getProtocolVersion())) { - ic.disconnectQuietly(TranslatableComponent.of("multiplayer.disconnect.outdated_client")); + ic.disconnectQuietly(Component.translatable("multiplayer.disconnect.outdated_client")); return; } InetAddress address = ((InetSocketAddress) connection.getRemoteAddress()).getAddress(); if (!server.getIpAttemptLimiter().attempt(address)) { - ic.disconnectQuietly(TextComponent.of("You are logging in too fast, try again later.")); + ic.disconnectQuietly(Component.text("You are logging in too fast, try again later.")); return; } @@ -116,7 +117,7 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { // and lower, otherwise IP information will never get forwarded. if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && handshake.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { - ic.disconnectQuietly(TextComponent.of("This server is only compatible with 1.13 and above.")); + ic.disconnectQuietly(Component.text("This server is only compatible with 1.13 and above.")); return; } 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 826baec66..fbfca874f 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 @@ -183,7 +183,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { } else { initializePlayer(GameProfile.forOfflinePlayer(login.getUsername()), false); } - }, mcConnection.eventLoop()); + }, mcConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception in pre-login stage", ex); + return null; + }); } private EncryptionRequest generateEncryptionRequest() { @@ -202,6 +206,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler { server.getConfiguration().getPlayerInfoForwardingMode()); GameProfileRequestEvent profileRequestEvent = new GameProfileRequestEvent(inbound, profile, onlineMode); + final GameProfile finalProfile = profile; server.getEventManager().fire(profileRequestEvent).thenCompose(profileEvent -> { if (mcConnection.isClosed()) { @@ -229,6 +234,9 @@ public class LoginSessionHandler implements MinecraftSessionHandler { completeLoginProtocolPhaseAndInitialize(player); } }, mcConnection.eventLoop()); + }).exceptionally((ex) -> { + logger.error("Exception during connection of {}", finalProfile, ex); + return null; }); } @@ -274,7 +282,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { server.getEventManager().fire(new PostLoginEvent(player)) .thenRun(() -> connectToInitialServer(player)); } - }, mcConnection.eventLoop()); + }, mcConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while completing login initialisation phase for {}", player, ex); + return null; + }); } private void connectToInitialServer(ConnectedPlayer player) { @@ -291,7 +303,11 @@ public class LoginSessionHandler implements MinecraftSessionHandler { return; } player.createConnectionRequest(toTry.get()).fireAndForget(); - }, mcConnection.eventLoop()); + }, mcConnection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); } @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 cffc71377..2b44c654b 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 @@ -163,7 +163,11 @@ public class StatusSessionHandler implements MinecraftSessionHandler { .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inbound, ping))) .thenAcceptAsync(event -> connection.closeWith( LegacyDisconnect.fromServerPing(event.getPing(), packet.getVersion())), - connection.eventLoop()); + connection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling legacy ping {}", packet, ex); + return null; + }); return true; } @@ -189,7 +193,11 @@ public class StatusSessionHandler implements MinecraftSessionHandler { .toJson(event.getPing(), json); connection.write(new StatusResponse(json)); }, - connection.eventLoop()); + connection.eventLoop()) + .exceptionally((ex) -> { + logger.error("Exception while handling status request {}", packet, ex); + return null; + }); return true; } 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 86f248297..50c9f3e88 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 @@ -1,16 +1,17 @@ package com.velocitypowered.proxy.connection.util; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; public class ConnectionMessages { - public static final TextComponent ALREADY_CONNECTED = TextComponent - .of("You are already connected to this server!", NamedTextColor.RED); - public static final TextComponent IN_PROGRESS = TextComponent - .of("You are already connecting to a server!", NamedTextColor.RED); - public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = TextComponent - .of("An internal server connection error occurred.", NamedTextColor.RED); + public static final TextComponent ALREADY_CONNECTED = Component + .text("You are already connected to this server!", NamedTextColor.RED); + public static final TextComponent IN_PROGRESS = Component + .text("You are already connecting to a server!", NamedTextColor.RED); + public static final TextComponent INTERNAL_SERVER_CONNECTION_ERROR = Component + .text("An internal server connection error occurred.", NamedTextColor.RED); 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 931e68c82..4273549ed 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/console/VelocityConsole.java @@ -8,6 +8,8 @@ import com.velocitypowered.api.permission.Tristate; import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.proxy.VelocityServer; import java.util.List; +import net.kyori.adventure.identity.Identity; +import net.kyori.adventure.text.Component; import net.kyori.text.TextComponent; import net.kyori.text.format.TextColor; import net.minecrell.terminalconsole.SimpleTerminalConsole; @@ -38,7 +40,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons } @Override - public void sendMessage(net.kyori.adventure.text.@NonNull Component message) { + public void sendMessage(@NonNull Identity identity, @NonNull Component message) { logger.info(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection() .serialize(message)); } 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 8d9b40bbf..5d0d58551 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityEventManager.java @@ -115,12 +115,10 @@ public class VelocityEventManager implements EventManager { return CompletableFuture.completedFuture(event); } - CompletableFuture eventFuture = new CompletableFuture<>(); - service.execute(() -> { + return CompletableFuture.supplyAsync(() -> { fireEvent(event); - eventFuture.complete(event); - }); - return eventFuture; + return event; + }, service); } @Override 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 1a2301c38..232e8c254 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/plugin/VelocityPluginManager.java @@ -41,7 +41,7 @@ public class VelocityPluginManager implements PluginManager { private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class); - private final Map plugins = new HashMap<>(); + private final Map plugins = new LinkedHashMap<>(); private final Map pluginInstances = new IdentityHashMap<>(); private final VelocityServer server; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java index 2fa1e759a..a7e0bd437 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -271,10 +271,6 @@ public enum ProtocolUtils { * @param stringArray the array to write */ public static void writeStringArray(ByteBuf buf, String[] stringArray) { - if (stringArray == null) { - writeVarInt(buf, 0); - return; - } writeVarInt(buf, stringArray.length); for (String s : stringArray) { writeString(buf, s); @@ -292,7 +288,7 @@ public enum ProtocolUtils { writeString(buf, property.getName()); writeString(buf, property.getValue()); String signature = property.getSignature(); - if (signature != null) { + if (signature != null && !signature.isEmpty()) { buf.writeBoolean(true); writeString(buf, signature); } else { 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 4ea37de84..ffe2a24b4 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 @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer; +import org.apache.logging.log4j.LogManager; public class GS4QueryHandler extends SimpleChannelInboundHandler { @@ -128,7 +129,7 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler // Build initial query response QueryResponse response = createInitialResponse(); - boolean isBasic = queryMessage.isReadable(); + boolean isBasic = !queryMessage.isReadable(); // Call event and write response server.getEventManager() @@ -162,7 +163,12 @@ public class GS4QueryHandler extends SimpleChannelInboundHandler // Send the response DatagramPacket responsePacket = new DatagramPacket(queryResponse, msg.sender()); ctx.writeAndFlush(responsePacket, ctx.voidPromise()); - }, ctx.channel().eventLoop()); + }, ctx.channel().eventLoop()) + .exceptionally((ex) -> { + LogManager.getLogger(getClass()).error( + "Exception while writing GS4 response for query from {}", senderAddress, ex); + return null; + }); break; } default: 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 a42d2650b..fd64ea600 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 @@ -6,6 +6,7 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; +import net.kyori.adventure.identity.Identity; import org.checkerframework.checker.nullness.qual.Nullable; import java.util.UUID; @@ -14,6 +15,7 @@ public class Chat implements MinecraftPacket { public static final byte CHAT_TYPE = (byte) 0; public static final byte SYSTEM_TYPE = (byte) 1; + public static final byte GAME_INFO_TYPE = (byte) 2; public static final int MAX_SERVERBOUND_MESSAGE_LENGTH = 256; public static final UUID EMPTY_SENDER = new UUID(0, 0); @@ -109,9 +111,9 @@ public class Chat implements MinecraftPacket { .serialize(component), type, sender); } - public static Chat createClientbound(net.kyori.adventure.text.Component component, - ProtocolVersion version) { - return createClientbound(component, CHAT_TYPE, EMPTY_SENDER, version); + public static Chat createClientbound(Identity identity, + net.kyori.adventure.text.Component component, ProtocolVersion version) { + return createClientbound(component, CHAT_TYPE, identity.uuid(), version); } public static Chat createClientbound(net.kyori.adventure.text.Component component, byte type, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java index ff8149332..644c1e3c6 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/ByteBufDataOutput.java @@ -10,7 +10,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; /** - * A {@link DataOutput} equivalent to {@link ByteBufDataInput}. + * A {@link ByteArrayDataOutput} equivalent to {@link ByteBufDataInput}. */ public class ByteBufDataOutput extends OutputStream implements ByteArrayDataOutput { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java index 02af541a6..dbf27b039 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/util/VelocityLegacyHoverEventSerializer.java @@ -31,7 +31,7 @@ public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSeria } private static Key legacyIdToFakeKey(byte id) { - return Key.of("velocity", "legacy_hover/id_" + id); + return Key.key("velocity", "legacy_hover/id_" + id); } @Override @@ -45,7 +45,7 @@ public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSeria if (idIfString.isEmpty()) { key = legacyIdToFakeKey(item.getByte("id")); } else { - key = Key.of(idIfString); + key = Key.key(idIfString); } byte count = item.getByte("Count", (byte) 1); @@ -62,10 +62,10 @@ public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSeria try { name = componentDecoder.decode(item.getString("name")); } catch (Exception e) { - name = TextComponent.of(item.getString("name")); + name = Component.text(item.getString("name")); } - return ShowEntity.of(Key.of(item.getString("type")), + return ShowEntity.of(Key.key(item.getString("type")), UUID.fromString(item.getString("id")), name); } @@ -89,7 +89,7 @@ public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSeria builder.put("tag", TagStringIO.get().asCompound(nbt.string())); } - return TextComponent.of(TagStringIO.get().asString(builder.build())); + return Component.text(TagStringIO.get().asString(builder.build())); } @Override @@ -102,6 +102,6 @@ public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSeria if (name != null) { tag.putString("name", componentEncoder.encode(name)); } - return TextComponent.of(TagStringIO.get().asString(tag.build())); + return Component.text(TagStringIO.get().asString(tag.build())); } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java index 5d753c155..1bad01978 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/BrigadierUtils.java @@ -1,10 +1,8 @@ package com.velocitypowered.proxy.util; -import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.mojang.brigadier.Command; import com.mojang.brigadier.arguments.StringArgumentType; -import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; @@ -13,7 +11,6 @@ import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.velocitypowered.api.command.CommandSource; import java.util.Locale; -import org.checkerframework.checker.nullness.qual.Nullable; /** * Provides utilities for working with Brigadier commands. diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/DurationUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/DurationUtils.java index 4742613e9..a588ed222 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/DurationUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/DurationUtils.java @@ -18,7 +18,7 @@ public final class DurationUtils { * @param duration the duration to convert into Minecraft ticks * @return the duration represented as the number of Minecraft ticks */ - public static long convertDurationToTicks(Duration duration) { + public static long toTicks(Duration duration) { return duration.toMillis() / ONE_TICK_IN_MILLISECONDS; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java b/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java new file mode 100644 index 000000000..89f24ab9c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/InformationUtils.java @@ -0,0 +1,240 @@ +package com.velocitypowered.proxy.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +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.api.proxy.config.ProxyConfig; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import com.velocitypowered.api.util.ProxyVersion; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; + +public enum InformationUtils { + ; + + /** + * Retrieves a {@link JsonArray} containing basic information about all + * running plugins on the {@link ProxyServer} instance. + * + * @param proxy the proxy instance to retrieve from + * @return {@link JsonArray} containing zero or more {@link JsonObject} + */ + public static JsonArray collectPluginInfo(ProxyServer proxy) { + List allPlugins = ImmutableList.copyOf( + proxy.getPluginManager().getPlugins()); + JsonArray plugins = new JsonArray(); + + for (PluginContainer plugin : allPlugins) { + PluginDescription desc = plugin.getDescription(); + JsonObject current = new JsonObject(); + current.addProperty("id", desc.getId()); + if (desc.getName().isPresent()) { + current.addProperty("name", desc.getName().get()); + } + if (desc.getVersion().isPresent()) { + current.addProperty("version", desc.getVersion().get()); + } + if (!desc.getAuthors().isEmpty()) { + JsonArray authorsArray = new JsonArray(); + for (String author : desc.getAuthors()) { + authorsArray.add(author); + } + current.add("authors", authorsArray); + } + if (desc.getDescription().isPresent()) { + current.addProperty("description", desc.getDescription().get()); + } + if (desc.getUrl().isPresent()) { + current.addProperty("url", desc.getUrl().get()); + } + if (!desc.getDependencies().isEmpty()) { + JsonArray dependencies = new JsonArray(); + for (PluginDependency dependency : desc.getDependencies()) { + dependencies.add(dependency.getId()); + } + current.add("dependencies", dependencies); + } + plugins.add(current); + } + return plugins; + } + + /** + * Creates a {@link JsonObject} containing information about the + * current environment the project is run under. + * + * @return {@link JsonObject} containing environment info + */ + public static JsonObject collectEnvironmentInfo() { + JsonObject envInfo = new JsonObject(); + envInfo.addProperty("operatingSystemType", System.getProperty("os.name")); + envInfo.addProperty("operatingSystemVersion", System.getProperty("os.version")); + envInfo.addProperty("operatingSystemArchitecture", System.getProperty("os.arch")); + envInfo.addProperty("javaVersion", System.getProperty("java.version")); + envInfo.addProperty("javaVendor", System.getProperty("java.vendor")); + return envInfo; + } + + /** + * Creates a {@link JsonObject} containing information about the + * forced hosts of the {@link ProxyConfig} instance. + * + * @return {@link JsonArray} containing forced hosts + */ + public static JsonObject collectForcedHosts(ProxyConfig config) { + JsonObject forcedHosts = new JsonObject(); + Map> allForcedHosts = ImmutableMap.copyOf( + config.getForcedHosts()); + for (Map.Entry> entry : allForcedHosts.entrySet()) { + JsonArray host = new JsonArray(); + for (int i = 0; i < entry.getValue().size(); i++) { + host.add(entry.getValue().get(i)); + } + forcedHosts.add(entry.getKey(), host); + } + return forcedHosts; + } + + /** + * Anonymises or redacts a given {@link InetAddress} + * public address bits. + * + * @param address The address to redact + * @return {@link String} address with public parts redacted + */ + public static String anonymizeInetAddress(InetAddress address) { + if (address instanceof Inet4Address) { + Inet4Address v4 = (Inet4Address) address; + if (v4.isAnyLocalAddress() || v4.isLoopbackAddress() + || v4.isLinkLocalAddress() + || v4.isSiteLocalAddress()) { + return address.getHostAddress(); + } else { + byte[] addr = v4.getAddress(); + return (addr[0] & 0xff) + "." + (addr[1] & 0xff) + ".XXX.XXX"; + } + } else if (address instanceof Inet6Address) { + Inet6Address v6 = (Inet6Address) address; + if (v6.isAnyLocalAddress() || v6.isLoopbackAddress() + || v6.isSiteLocalAddress() + || v6.isSiteLocalAddress()) { + return address.getHostAddress(); + } else { + String[] bits = v6.getHostAddress().split(":"); + String ret = ""; + boolean flag = false; + for (int iter = 0; iter < bits.length; iter++) { + if (flag) { + ret += ":X"; + continue; + } + if (!bits[iter].equals("0")) { + if (iter == 0) { + ret = bits[iter]; + } else { + ret = "::" + bits[iter]; + } + flag = true; + } + } + return ret; + } + } else { + return address.getHostAddress(); + } + } + + /** + * Creates a {@link JsonObject} containing most relevant + * information of the {@link RegisteredServer} for diagnosis. + * + * @param server the server to evaluate + * @return {@link JsonObject} containing server and diagnostic info + */ + public static JsonObject collectServerInfo(RegisteredServer server) { + JsonObject info = new JsonObject(); + info.addProperty("currentPlayers", server.getPlayersConnected().size()); + InetSocketAddress iaddr = server.getServerInfo().getAddress(); + if (iaddr.isUnresolved()) { + // Greetings form Netty 4aa10db9 + info.addProperty("host", iaddr.getHostString()); + } else { + info.addProperty("host", anonymizeInetAddress(iaddr.getAddress())); + } + info.addProperty("port", iaddr.getPort()); + return info; + } + + /** + * Creates a {@link JsonObject} containing information about the + * current environment the project is run under. + * + * @param version the proxy instance to retrieve from + * @return {@link JsonObject} containing environment info + */ + public static JsonObject collectProxyInfo(ProxyVersion version) { + return (JsonObject) serializeObject(version, false); + } + + /** + * Creates a {@link JsonObject} containing most relevant + * information of the {@link ProxyConfig} for diagnosis. + * + * @param config the config instance to retrieve from + * @return {@link JsonObject} containing select config values + */ + public static JsonObject collectProxyConfig(ProxyConfig config) { + return (JsonObject) serializeObject(config, true); + } + + /** + * Creates a human-readable String from a {@link JsonElement}. + * + * @param json the {@link JsonElement} object + * @return the human-readable String + */ + public static String toHumanReadableString(JsonElement json) { + return GSON_WITHOUT_EXCLUDES.toJson(json); + } + + /** + * Creates a {@link JsonObject} from a String. + * + * @param toParse the String to parse + * @return {@link JsonObject} object + */ + public static JsonObject parseString(String toParse) { + return GSON_WITHOUT_EXCLUDES.fromJson(toParse, JsonObject.class); + } + + private static JsonElement serializeObject(Object toSerialize, boolean withExcludes) { + return JsonParser.parseString( + withExcludes ? GSON_WITH_EXCLUDES.toJson(toSerialize) : + GSON_WITHOUT_EXCLUDES.toJson(toSerialize)); + } + + private static final Gson GSON_WITH_EXCLUDES = new GsonBuilder() + .setPrettyPrinting() + .excludeFieldsWithoutExposeAnnotation() + .create(); + + private static final Gson GSON_WITHOUT_EXCLUDES = new GsonBuilder() + .setPrettyPrinting() + .create(); + + +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java index bb7c383bf..b07e4815f 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/util/VelocityChannelRegistrar.java @@ -21,8 +21,7 @@ public class VelocityChannelRegistrar implements ChannelRegistrar { public void register(ChannelIdentifier... identifiers) { for (ChannelIdentifier identifier : identifiers) { Preconditions.checkArgument(identifier instanceof LegacyChannelIdentifier - || identifier instanceof MinecraftChannelIdentifier, - "identifier is unknown"); + || identifier instanceof MinecraftChannelIdentifier, "identifier is unknown"); } for (ChannelIdentifier identifier : identifiers) {