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 f571c4517..db3ef1ed5 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 @@ -182,6 +182,19 @@ public final class ServerPing { return this; } + /** + * Uses the modified {@code mods} list in the response. + * @param mods the mods list to use + * @return this build, for chaining + */ + public Builder mods(ModInfo mods) { + Preconditions.checkNotNull(mods, "mods"); + this.modType = mods.getType(); + this.mods.clear(); + this.mods.addAll(mods.getMods()); + return this; + } + public Builder clearMods() { this.mods.clear(); return this; diff --git a/proxy/build.gradle b/proxy/build.gradle index 9c3f151b1..9c5bf26fb 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -66,6 +66,8 @@ dependencies { compile 'com.mojang:brigadier:1.0.15' compile 'org.asynchttpclient:async-http-client:2.10.1' + + compile 'com.spotify:completable-futures:0.3.2' testCompile "org.junit.jupiter:junit-jupiter-api:${junitVersion}" testCompile "org.junit.jupiter:junit-jupiter-engine:${junitVersion}" diff --git a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java index c078ecfb9..5b032e180 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/VelocityServer.java @@ -77,6 +77,7 @@ import org.asynchttpclient.AsyncHttpClient; import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.RequiresNonNull; public class VelocityServer implements ProxyServer { @@ -263,11 +264,7 @@ public class VelocityServer implements ProxyServer { logger.info("Loaded {} plugins", pluginManager.getPlugins().size()); } - public Bootstrap createBootstrap() { - return this.cm.createWorker(); - } - - public Bootstrap createBootstrap(EventLoopGroup group) { + public Bootstrap createBootstrap(@Nullable EventLoopGroup group) { return this.cm.createWorker(group); } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java new file mode 100644 index 000000000..89ed3ad8e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/PingPassthroughMode.java @@ -0,0 +1,7 @@ +package com.velocitypowered.proxy.config; + +public enum PingPassthroughMode { + DISABLED, + MODS, + ALL +} 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 812d66154..7891d778c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/config/VelocityConfiguration.java @@ -72,8 +72,13 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi @ConfigKey("forwarding-secret") private byte[] forwardingSecret = generateRandomString(12).getBytes(StandardCharsets.UTF_8); - @Comment({"Announce whether or not your server supports Forge. If you run a modded server, we", - "suggest turning this on."}) + @Comment({ + "Announce whether or not your server supports Forge. If you run a modded server, we", + "suggest turning this on.", + "", + "If your network runs one modpack consistently, consider using ping-passthrough = \"mods\"", + "instead for a nicer display in the server list." + }) @ConfigKey("announce-forge") private boolean announceForge = false; @@ -82,6 +87,20 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi @ConfigKey("kick-existing-players") private boolean onlineModeKickExistingPlayers = false; + @Comment({ + "Should Velocity pass server list ping requests to a backend server?", + "Available options:", + "- \"disabled\": No pass-through will be done. The velocity.toml and server-icon.png", + " will determine the initial server list ping response.", + "- \"mods\": Passes only the mod list from your backend server into the response.", + " This is the recommended replacement for announce-forge = true. If no backend", + " servers can be contacted, Velocity will not display any mod information.", + "- \"all\": Passes everything from the backend server into the response. The Velocity", + " configuration is used if no servers could be contacted." + }) + @ConfigKey("ping-passthrough") + private PingPassthroughMode pingPassthrough; + @Table("[servers]") private final Servers servers; @@ -114,8 +133,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi private VelocityConfiguration(String bind, String motd, int showMaxPlayers, boolean onlineMode, boolean announceForge, PlayerInfoForwarding playerInfoForwardingMode, byte[] forwardingSecret, - boolean onlineModeKickExistingPlayers, Servers servers, ForcedHosts forcedHosts, - Advanced advanced, Query query, Metrics metrics) { + boolean onlineModeKickExistingPlayers, PingPassthroughMode pingPassthrough, Servers servers, + ForcedHosts forcedHosts, Advanced advanced, Query query, Metrics metrics) { this.bind = bind; this.motd = motd; this.showMaxPlayers = showMaxPlayers; @@ -124,6 +143,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi this.playerInfoForwardingMode = playerInfoForwardingMode; this.forwardingSecret = forwardingSecret; this.onlineModeKickExistingPlayers = onlineModeKickExistingPlayers; + this.pingPassthrough = pingPassthrough; this.servers = servers; this.forcedHosts = forcedHosts; this.advanced = advanced; @@ -380,6 +400,10 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi return metrics; } + public PingPassthroughMode getPingPassthrough() { + return pingPassthrough; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) @@ -427,6 +451,8 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi String forwardingModeName = toml.getString("player-info-forwarding-mode", "MODERN") .toUpperCase(Locale.US); + String passThroughName = toml.getString("ping-passthrough", "DISABLED") + .toUpperCase(Locale.US); return new VelocityConfiguration( toml.getString("bind", "0.0.0.0:25577"), @@ -437,6 +463,7 @@ public class VelocityConfiguration extends AnnotatedConfig implements ProxyConfi PlayerInfoForwarding.valueOf(forwardingModeName), forwardingSecret, toml.getBoolean("kick-existing-players", false), + PingPassthroughMode.valueOf(passThroughName), servers, forcedHosts, advanced, 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 0916d4aea..68403374c 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 @@ -1,12 +1,15 @@ package com.velocitypowered.proxy.connection.client; import com.google.common.collect.ImmutableList; +import com.spotify.futures.CompletableFutures; import com.velocitypowered.api.event.proxy.ProxyPingEvent; import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.InboundConnection; +import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.ServerPing; import com.velocitypowered.api.util.ModInfo; import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.config.PingPassthroughMode; import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; @@ -15,7 +18,13 @@ import com.velocitypowered.proxy.protocol.packet.LegacyPing; import com.velocitypowered.proxy.protocol.packet.StatusPing; import com.velocitypowered.proxy.protocol.packet.StatusRequest; import com.velocitypowered.proxy.protocol.packet.StatusResponse; +import com.velocitypowered.proxy.server.VelocityRegisteredServer; import io.netty.buffer.ByteBuf; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; public class StatusSessionHandler implements MinecraftSessionHandler { @@ -30,12 +39,10 @@ public class StatusSessionHandler implements MinecraftSessionHandler { this.inboundWrapper = inboundWrapper; } - private ServerPing createInitialPing() { + private ServerPing constructLocalPing(ProtocolVersion version) { VelocityConfiguration configuration = server.getConfiguration(); - ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion()) - ? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION; return new ServerPing( - new ServerPing.Version(shownVersion.getProtocol(), + new ServerPing.Version(version.getProtocol(), "Velocity " + ProtocolVersion.SUPPORTED_VERSION_STRING), new ServerPing.Players(server.getPlayerCount(), configuration.getShowMaxPlayers(), ImmutableList.of()), @@ -45,12 +52,69 @@ public class StatusSessionHandler implements MinecraftSessionHandler { ); } + private CompletableFuture createInitialPing() { + VelocityConfiguration configuration = server.getConfiguration(); + ProtocolVersion shownVersion = ProtocolVersion.isSupported(connection.getProtocolVersion()) + ? connection.getProtocolVersion() : ProtocolVersion.MAXIMUM_VERSION; + + PingPassthroughMode passthrough = configuration.getPingPassthrough(); + if (passthrough == PingPassthroughMode.DISABLED) { + return CompletableFuture.completedFuture(constructLocalPing(shownVersion)); + } else { + return attemptPingPassthrough(configuration.getPingPassthrough(), + configuration.getAttemptConnectionOrder(), shownVersion); + } + } + + private CompletableFuture attemptPingPassthrough(PingPassthroughMode mode, + List servers, ProtocolVersion pingingVersion) { + ServerPing fallback = constructLocalPing(pingingVersion); + List> pings = new ArrayList<>(); + for (String s : servers) { + Optional rs = server.getServer(s); + if (!rs.isPresent()) { + continue; + } + VelocityRegisteredServer vrs = (VelocityRegisteredServer) rs.get(); + pings.add(vrs.ping(connection.eventLoop(), pingingVersion)); + } + if (pings.isEmpty()) { + return CompletableFuture.completedFuture(fallback); + } + + CompletableFuture> pingResponses = CompletableFutures.successfulAsList(pings, + (ex) -> fallback); + switch (mode) { + case ALL: + return pingResponses.thenApply(responses -> { + // Find the first non-fallback + return responses.stream() + .filter(ping -> ping != fallback) + .findFirst() + .orElse(fallback); + }); + case MODS: + return pingResponses.thenApply(responses -> { + // Find the first non-fallback that contains a non-empty mod list + Optional modInfo = responses.stream() + .filter(ping -> ping != fallback) + .map(ServerPing::getModinfo) + .flatMap(o -> o.map(Stream::of).orElseGet(Stream::empty)) + .findFirst(); + return modInfo.map(mi -> fallback.asBuilder().mods(mi).build()).orElse(fallback); + }); + default: + // Not possible, but covered for completeness. + return CompletableFuture.completedFuture(fallback); + } + } + @Override public boolean handle(LegacyPing packet) { - ServerPing initialPing = createInitialPing(); - ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing); - server.getEventManager().fire(event) - .thenRunAsync(() -> { + createInitialPing() + .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inboundWrapper, + ping))) + .thenAcceptAsync(event -> { connection.closeWith(LegacyDisconnect.fromServerPing(event.getPing(), packet.getVersion())); }, connection.eventLoop()); @@ -65,11 +129,11 @@ public class StatusSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(StatusRequest packet) { - ServerPing initialPing = createInitialPing(); - ProxyPingEvent event = new ProxyPingEvent(inboundWrapper, initialPing); - server.getEventManager().fire(event) - .thenRunAsync( - () -> { + createInitialPing() + .thenCompose(ping -> server.getEventManager().fire(new ProxyPingEvent(inboundWrapper, + ping))) + .thenAcceptAsync( + (event) -> { StringBuilder json = new StringBuilder(); VelocityServer.GSON.toJson(event.getPing(), json); connection.write(new StatusResponse(json)); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java index 208ce766e..9fba8e657 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ConnectionManager.java @@ -145,10 +145,6 @@ public final class ConnectionManager { }); } - public Bootstrap createWorker() { - return this.createWorker(null); - } - /** * Creates a TCP {@link Bootstrap} using Velocity's event loops. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java index 48875b988..93094cc36 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -18,13 +18,15 @@ public class PingSessionHandler implements MinecraftSessionHandler { private final CompletableFuture result; private final RegisteredServer server; private final MinecraftConnection connection; + private final ProtocolVersion version; private boolean completed = false; PingSessionHandler(CompletableFuture result, RegisteredServer server, - MinecraftConnection connection) { + MinecraftConnection connection, ProtocolVersion version) { this.result = result; this.server = server; this.connection = connection; + this.version = version; } @Override @@ -33,7 +35,7 @@ public class PingSessionHandler implements MinecraftSessionHandler { handshake.setNextStatus(StateRegistry.STATUS_ID); handshake.setServerAddress(server.getServerInfo().getAddress().getHostString()); handshake.setPort(server.getServerInfo().getAddress().getPort()); - handshake.setProtocolVersion(ProtocolVersion.MINIMUM_VERSION); + handshake.setProtocolVersion(version); connection.delayedWrite(handshake); connection.setState(StateRegistry.STATUS); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java index 6261d5614..cf6694cb4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -9,6 +9,7 @@ import static com.velocitypowered.proxy.network.Connections.READ_TIMEOUT; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; @@ -27,6 +28,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoop; import io.netty.handler.timeout.ReadTimeoutHandler; import java.util.Collection; import java.util.Set; @@ -58,11 +60,22 @@ public class VelocityRegisteredServer implements RegisteredServer { @Override public CompletableFuture ping() { + return ping(null, ProtocolVersion.UNKNOWN); + } + + /** + * Pings the specified server using the specified event {@code loop}, claiming to be + * {@code version}. + * @param loop the event loop to use + * @param version the version to report + * @return the server list ping response + */ + public CompletableFuture ping(@Nullable EventLoop loop, ProtocolVersion version) { if (server == null) { throw new IllegalStateException("No Velocity proxy instance available"); } CompletableFuture pingFuture = new CompletableFuture<>(); - server.createBootstrap() + server.createBootstrap(loop) .handler(new ChannelInitializer() { @Override protected void initChannel(Channel ch) throws Exception { @@ -86,8 +99,8 @@ public class VelocityRegisteredServer implements RegisteredServer { public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); - conn.setSessionHandler( - new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn)); + conn.setSessionHandler(new PingSessionHandler( + pingFuture, VelocityRegisteredServer.this, conn, version)); } else { pingFuture.completeExceptionally(future.cause()); }