diff --git a/.gitignore b/.gitignore index f90c93da9..97eb7c9e0 100644 --- a/.gitignore +++ b/.gitignore @@ -83,6 +83,8 @@ gradle-app.setting logs/ /velocity.toml /forwarding.secret +forwarding.secret +velocity.toml server-icon.png /bin/ run/ 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 110b58767..ef7024efe 100644 --- a/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java +++ b/api/src/main/java/com/velocitypowered/api/network/ProtocolVersion.java @@ -62,7 +62,8 @@ public enum ProtocolVersion { MINECRAFT_1_19_1(760, "1.19.1", "1.19.2"), MINECRAFT_1_19_3(761, "1.19.3"), MINECRAFT_1_19_4(762, "1.19.4"), - MINECRAFT_1_20(763, "1.20", "1.20.1"); + MINECRAFT_1_20(763, "1.20", "1.20.1"), + MINECRAFT_1_20_2(764, "1.20.2"); private static final int SNAPSHOT_BIT = 30; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java index 9145ff133..bb0c4db72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftConnection.java @@ -36,6 +36,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.connection.client.InitialLoginSessionHandler; import com.velocitypowered.proxy.connection.client.StatusSessionHandler; +import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.VelocityConnectionEvent; @@ -46,6 +47,7 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder; +import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler; import com.velocitypowered.proxy.util.except.QuietDecoderException; import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; @@ -60,6 +62,9 @@ import io.netty.util.ReferenceCountUtil; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -78,7 +83,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { private final Channel channel; private SocketAddress remoteAddress; private StateRegistry state; - private @Nullable MinecraftSessionHandler sessionHandler; + private Map sessionHandlers; + private @Nullable MinecraftSessionHandler activeSessionHandler; private ProtocolVersion protocolVersion; private @Nullable MinecraftConnectionAssociation association; public final VelocityServer server; @@ -96,12 +102,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.remoteAddress = channel.remoteAddress(); this.server = server; this.state = StateRegistry.HANDSHAKE; + + this.sessionHandlers = new HashMap<>(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.connected(); + if (activeSessionHandler != null) { + activeSessionHandler.connected(); } if (association != null && server.getConfiguration().isLogPlayerConnections()) { @@ -111,12 +119,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.disconnected(); + if (activeSessionHandler != null) { + activeSessionHandler.disconnected(); } if (association != null && !knownDisconnect - && !(sessionHandler instanceof StatusSessionHandler) + && !(activeSessionHandler instanceof StatusSessionHandler) && server.getConfiguration().isLogPlayerConnections()) { logger.info("{} has disconnected", association); } @@ -125,12 +133,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { try { - if (sessionHandler == null) { + if (activeSessionHandler == null) { // No session handler available, do nothing return; } - if (sessionHandler.beforeHandle()) { + if (activeSessionHandler.beforeHandle()) { return; } @@ -140,15 +148,15 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (msg instanceof MinecraftPacket) { MinecraftPacket pkt = (MinecraftPacket) msg; - if (!pkt.handle(sessionHandler)) { - sessionHandler.handleGeneric((MinecraftPacket) msg); + if (!pkt.handle(activeSessionHandler)) { + activeSessionHandler.handleGeneric((MinecraftPacket) msg); } } else if (msg instanceof HAProxyMessage) { HAProxyMessage proxyMessage = (HAProxyMessage) msg; this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(), proxyMessage.sourcePort()); } else if (msg instanceof ByteBuf) { - sessionHandler.handleUnknown((ByteBuf) msg); + activeSessionHandler.handleUnknown((ByteBuf) msg); } } finally { ReferenceCountUtil.release(msg); @@ -157,20 +165,21 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.readCompleted(); + if (activeSessionHandler != null) { + activeSessionHandler.readCompleted(); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (ctx.channel().isActive()) { - if (sessionHandler != null) { + if (activeSessionHandler != null) { try { - sessionHandler.exception(cause); + activeSessionHandler.exception(cause); } catch (Exception ex) { logger.error("{}: exception handling exception in {}", - (association != null ? association : channel.remoteAddress()), sessionHandler, cause); + (association != null ? association : channel.remoteAddress()), activeSessionHandler, + cause); } } @@ -178,13 +187,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { if (cause instanceof ReadTimeoutException) { logger.error("{}: read timed out", association); } else { - boolean frontlineHandler = sessionHandler instanceof InitialLoginSessionHandler - || sessionHandler instanceof HandshakeSessionHandler - || sessionHandler instanceof StatusSessionHandler; + boolean frontlineHandler = activeSessionHandler instanceof InitialLoginSessionHandler + || activeSessionHandler instanceof HandshakeSessionHandler + || activeSessionHandler instanceof StatusSessionHandler; boolean isQuietDecoderException = cause instanceof QuietDecoderException; boolean willLog = !isQuietDecoderException && !frontlineHandler; if (willLog) { - logger.error("{}: exception encountered in {}", association, sessionHandler, cause); + logger.error("{}: exception encountered in {}", association, activeSessionHandler, + cause); } else { knownDisconnect = true; } @@ -197,8 +207,8 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { - if (sessionHandler != null) { - sessionHandler.writabilityChanged(); + if (activeSessionHandler != null) { + activeSessionHandler.writabilityChanged(); } } @@ -323,7 +333,7 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } /** - * Determines whether or not the channel should continue reading data automaticaly. + * Determines whether or not the channel should continue reading data automatically. * * @param autoReading whether or not we should read data automatically */ @@ -341,10 +351,12 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } } + // Ideally only used by the state switch + /** - * Changes the state of the Minecraft connection. + * Sets the new state for the connection. * - * @param state the new state + * @param state the state to use */ public void setState(StateRegistry state) { ensureInEventLoop(); @@ -352,6 +364,25 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { this.state = state; this.channel.pipeline().get(MinecraftEncoder.class).setState(state); this.channel.pipeline().get(MinecraftDecoder.class).setState(state); + + if (state == StateRegistry.CONFIG) { + // Activate the play packet queue + addPlayPacketQueueHandler(); + } else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) { + // Remove the queue + this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE); + } + } + + /** + * Adds the play packet queue handler. + */ + public void addPlayPacketQueueHandler() { + if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) { + this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE, + new PlayPacketQueueHandler(this.protocolVersion, + channel.pipeline().get(MinecraftEncoder.class).getDirection())); + } } public ProtocolVersion getProtocolVersion() { @@ -382,32 +413,81 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { } } - public @Nullable MinecraftSessionHandler getSessionHandler() { - return sessionHandler; + public @Nullable MinecraftSessionHandler getActiveSessionHandler() { + return activeSessionHandler; + } + + public @Nullable MinecraftSessionHandler getSessionHandlerForRegistry(StateRegistry registry) { + return this.sessionHandlers.getOrDefault(registry, null); } /** * Sets the session handler for this connection. * + * @param registry the registry of the handler * @param sessionHandler the handler to use */ - public void setSessionHandler(MinecraftSessionHandler sessionHandler) { + public void setActiveSessionHandler(StateRegistry registry, + MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); ensureInEventLoop(); - if (this.sessionHandler != null) { - this.sessionHandler.deactivated(); + if (this.activeSessionHandler != null) { + this.activeSessionHandler.deactivated(); } - this.sessionHandler = sessionHandler; + this.sessionHandlers.put(registry, sessionHandler); + this.activeSessionHandler = sessionHandler; + setState(registry); sessionHandler.activated(); } + /** + * Switches the active session handler to the respective registry one. + * + * @param registry the registry of the handler + * @return true if successful and handler is present + */ + public boolean setActiveSessionHandler(StateRegistry registry) { + Preconditions.checkNotNull(registry); + ensureInEventLoop(); + + MinecraftSessionHandler handler = getSessionHandlerForRegistry(registry); + if (handler != null) { + boolean flag = true; + if (this.activeSessionHandler != null + && (flag = !Objects.equals(handler, this.activeSessionHandler))) { + this.activeSessionHandler.deactivated(); + } + this.activeSessionHandler = handler; + setState(registry); + if (flag) { + handler.activated(); + } + } + return handler != null; + } + + /** + * Adds a secondary session handler for this connection. + * + * @param registry the registry of the handler + * @param sessionHandler the handler to use + */ + public void addSessionHandler(StateRegistry registry, MinecraftSessionHandler sessionHandler) { + Preconditions.checkNotNull(registry); + Preconditions.checkArgument(registry != state, "Handler would overwrite handler"); + ensureInEventLoop(); + + this.sessionHandlers.put(registry, sessionHandler); + } + private void ensureOpen() { Preconditions.checkState(!isClosed(), "Connection is closed."); } /** - * Sets the compression threshold on the connection. You are responsible for sending - * {@link com.velocitypowered.proxy.protocol.packet.SetCompression} beforehand. + * Sets the compression threshold on the connection. You are responsible for sending {@link + * com.velocitypowered.proxy.protocol.packet.SetCompression} beforehand. * * @param threshold the compression threshold to use */ @@ -497,5 +577,4 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter { public void setType(ConnectionType connectionType) { this.connectionType = connectionType; } - } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index ef4c362f4..ea01eb008 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -31,8 +31,10 @@ import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyHandshake; import com.velocitypowered.proxy.protocol.packet.LegacyPing; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; +import com.velocitypowered.proxy.protocol.packet.PingIdentify; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; @@ -55,6 +57,11 @@ import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.ActiveFeatures; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -279,4 +286,32 @@ public interface MinecraftSessionHandler { default boolean handle(UpsertPlayerInfo packet) { return false; } + + default boolean handle(LoginAcknowledged packet) { + return false; + } + + default boolean handle(ActiveFeatures packet) { + return false; + } + + default boolean handle(FinishedUpdate packet) { + return false; + } + + default boolean handle(RegistrySync packet) { + return false; + } + + default boolean handle(TagsUpdate packet) { + return false; + } + + default boolean handle(StartUpdate packet) { + return false; + } + + default boolean handle(PingIdentify pingIdentify) { + return false; + } } 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 ff777f5f9..ffa0c8391 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 @@ -39,8 +39,11 @@ import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.AvailableCommands; import com.velocitypowered.proxy.protocol.packet.BossBar; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; @@ -51,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; import com.velocitypowered.proxy.protocol.packet.ServerData; import com.velocitypowered.proxy.protocol.packet.TabCompleteResponse; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -68,10 +72,10 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); private static final Logger logger = LogManager.getLogger(BackendPlaySessionHandler.class); - private static final boolean BACKPRESSURE_LOG = Boolean - .getBoolean("velocity.log-server-backpressure"); - private static final int MAXIMUM_PACKETS_TO_FLUSH = Integer - .getInteger("velocity.max-packets-per-flush", 8192); + private static final boolean BACKPRESSURE_LOG = + Boolean.getBoolean("velocity.log-server-backpressure"); + private static final int MAXIMUM_PACKETS_TO_FLUSH = + Integer.getInteger("velocity.max-packets-per-flush", 8192); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -86,7 +90,7 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { this.serverConn = serverConn; this.playerConnection = serverConn.getPlayer().getConnection(); - MinecraftSessionHandler psh = playerConnection.getSessionHandler(); + MinecraftSessionHandler psh = playerConnection.getActiveSessionHandler(); if (!(psh instanceof ClientPlaySessionHandler)) { throw new IllegalStateException( "Initializing BackendPlaySessionHandler with no backing client play session handler!"); @@ -119,12 +123,28 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { return false; } + @Override + public boolean handle(StartUpdate packet) { + MinecraftConnection smc = serverConn.ensureConnected(); + smc.setAutoReading(false); + // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.CONFIG); + serverConn.getPlayer().switchToConfigState(); + return true; + } + @Override public boolean handle(KeepAlive packet) { serverConn.getPendingPings().put(packet.getRandomId(), System.currentTimeMillis()); return false; // forwards on } + @Override + public boolean handle(ClientSettings packet) { + serverConn.ensureConnected().write(packet); + return true; + } + @Override public boolean handle(Disconnect packet) { serverConn.disconnect(); @@ -221,20 +241,16 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { } byte[] copy = ByteBufUtil.getBytes(packet.content()); - PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, - copy); - server.getEventManager().fire(event) - .thenAcceptAsync(pme -> { - if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { - PluginMessage copied = new PluginMessage(packet.getChannel(), - Unpooled.wrappedBuffer(copy)); - playerConnection.write(copied); - } - }, playerConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while handling plugin message {}", packet, ex); - return null; - }); + PluginMessageEvent event = new PluginMessageEvent(serverConn, serverConn.getPlayer(), id, copy); + server.getEventManager().fire(event).thenAcceptAsync(pme -> { + if (pme.getResult().isAllowed() && !playerConnection.isClosed()) { + PluginMessage copied = new PluginMessage(packet.getChannel(), Unpooled.wrappedBuffer(copy)); + playerConnection.write(copied); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while handling plugin message {}", packet, ex); + return null; + }); return true; } @@ -283,18 +299,13 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ServerData packet) { - server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()) - .thenComposeAsync( - ping -> server.getEventManager() - .fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), - playerConnection.eventLoop() - ) - .thenAcceptAsync(pingEvent -> - this.playerConnection.write( - new ServerData(pingEvent.getPing().getDescriptionComponent(), - pingEvent.getPing().getFavicon().orElse(null), - packet.isSecureChatEnforced()) - ), playerConnection.eventLoop()); + server.getServerListPingHandler().getInitialPing(this.serverConn.getPlayer()).thenComposeAsync( + ping -> server.getEventManager() + .fire(new ProxyPingEvent(this.serverConn.getPlayer(), ping)), + playerConnection.eventLoop()).thenAcceptAsync(pingEvent -> this.playerConnection.write( + new ServerData(pingEvent.getPing().getDescriptionComponent(), + pingEvent.getPing().getFavicon().orElse(null), packet.isSecureChatEnforced())), + playerConnection.eventLoop()); return true; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java new file mode 100644 index 000000000..d84ae49ce --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/ConfigSessionHandler.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.backend; + +import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent; +import com.velocitypowered.api.event.player.ServerResourcePackSendEvent; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.connection.util.ConnectionMessages; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; +import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; +import com.velocitypowered.proxy.protocol.packet.Disconnect; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; +import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; +import net.kyori.adventure.text.Component; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A special session handler that catches "last minute" disconnects. This version is to accommodate + * 1.20.2+ switching. Yes, some of this is exceptionally stupid. + */ +public class ConfigSessionHandler implements MinecraftSessionHandler { + + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); + private static final Logger logger = LogManager.getLogger(ConfigSessionHandler.class); + private final VelocityServer server; + private final VelocityServerConnection serverConn; + private final CompletableFuture resultFuture; + + private ResourcePackInfo resourcePackToApply; + + private State state; + + /** + * Creates the new transition handler. + * + * @param server the Velocity server instance + * @param serverConn the server connection + * @param resultFuture the result future + */ + ConfigSessionHandler(VelocityServer server, VelocityServerConnection serverConn, + CompletableFuture resultFuture) { + this.server = server; + this.serverConn = serverConn; + this.resultFuture = resultFuture; + this.state = State.START; + } + + @Override + public void activated() { + resourcePackToApply = serverConn.getPlayer().getAppliedResourcePack(); + serverConn.getPlayer().clearAppliedResourcePack(); + } + + @Override + public boolean beforeHandle() { + if (!serverConn.isActive()) { + // Obsolete connection + serverConn.disconnect(); + return true; + } + return false; + } + + @Override + public boolean handle(StartUpdate packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(TagsUpdate packet) { + serverConn.getPlayer().getConnection().write(packet); + return true; + } + + @Override + public boolean handle(KeepAlive packet) { + serverConn.ensureConnected().write(packet); + return true; + } + + @Override + public boolean handle(ResourcePackRequest packet) { + final MinecraftConnection playerConnection = serverConn.getPlayer().getConnection(); + + ServerResourcePackSendEvent event = + new ServerResourcePackSendEvent(packet.toServerPromptedPack(), this.serverConn); + + server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackSendEvent -> { + if (playerConnection.isClosed()) { + return; + } + if (serverResourcePackSendEvent.getResult().isAllowed()) { + ResourcePackInfo toSend = serverResourcePackSendEvent.getProvidedResourcePack(); + if (toSend != serverResourcePackSendEvent.getReceivedResourcePack()) { + ((VelocityResourcePackInfo) toSend).setOriginalOrigin( + ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + } + + resourcePackToApply = null; + serverConn.getPlayer().queueResourcePack(toSend); + } else if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse(packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + } + }, playerConnection.eventLoop()).exceptionally((ex) -> { + if (serverConn.getConnection() != null) { + serverConn.getConnection().write(new ResourcePackResponse(packet.getHash(), + PlayerResourcePackStatusEvent.Status.DECLINED)); + } + logger.error("Exception while handling resource pack send for {}", playerConnection, ex); + return null; + }); + + return true; + } + + @Override + public boolean handle(FinishedUpdate packet) { + MinecraftConnection smc = serverConn.ensureConnected(); + ClientConfigSessionHandler configHandler = + (ClientConfigSessionHandler) serverConn.getPlayer().getConnection() + .getActiveSessionHandler(); + + smc.setAutoReading(false); + // Even when not auto reading messages are still decoded. Decode them with the correct state + smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY); + configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> { + if (serverConn == serverConn.getPlayer().getConnectedServer()) { + smc.setActiveSessionHandler(StateRegistry.PLAY); + } else { + smc.setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + } + if (serverConn.getPlayer().getAppliedResourcePack() == null && resourcePackToApply != null) { + serverConn.getPlayer().queueResourcePack(resourcePackToApply); + } + smc.setAutoReading(true); + }, smc.eventLoop()); + return true; + } + + @Override + public boolean handle(Disconnect packet) { + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect(packet, serverConn.getServer())); + return true; + } + + @Override + public boolean handle(PluginMessage packet) { + if (PluginMessageUtil.isMcBrand(packet)) { + serverConn.getPlayer().getConnection().write( + PluginMessageUtil.rewriteMinecraftBrand(packet, server.getVersion(), + serverConn.getPlayer().getProtocolVersion())); + } else { + // TODO: Change this so its usable for mod loaders + serverConn.disconnect(); + resultFuture.complete(ConnectionRequestResults.forDisconnect( + Component.translatable("multiplayer.disconnect.missing_tags"), serverConn.getServer())); + } + return true; + } + + @Override + public boolean handle(RegistrySync packet) { + serverConn.getPlayer().getConnection().write(packet.retain()); + return true; + } + + @Override + public void disconnected() { + resultFuture.completeExceptionally( + new IOException("Unexpectedly disconnected from remote server")); + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + serverConn.getPlayer().getConnection().write(packet); + } + + private void switchFailure(Throwable cause) { + logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), + serverConn.getPlayer().getUsername(), cause); + serverConn.getPlayer().disconnect(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + resultFuture.completeExceptionally(cause); + } + + /** + * Represents the state of the configuration stage. + */ + public static enum State { + START, NEGOTIATING, PLUGIN_MESSAGE_INTERRUPT, RESOURCE_PACK_INTERRUPT, COMPLETE + } +} 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 90a9a1300..e9801c315 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 @@ -27,6 +27,7 @@ import com.velocitypowered.proxy.config.VelocityConfiguration; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.VelocityConstants; +import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; @@ -34,6 +35,7 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; @@ -59,8 +61,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler { private static final Logger logger = LogManager.getLogger(LoginSessionHandler.class); - private static final Component MODERN_IP_FORWARDING_FAILURE = Component - .translatable("velocity.error.modern-forwarding-failed"); + private static final Component MODERN_IP_FORWARDING_FAILURE = + Component.translatable("velocity.error.modern-forwarding-failed"); private final VelocityServer server; private final VelocityServerConnection serverConn; @@ -150,10 +152,28 @@ public class LoginSessionHandler implements MinecraftSessionHandler { // Move into the PLAY phase. MinecraftConnection smc = serverConn.ensureConnected(); - smc.setState(StateRegistry.PLAY); + if (smc.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + smc.setActiveSessionHandler(StateRegistry.PLAY, + new TransitionSessionHandler(server, serverConn, resultFuture)); + } else { + smc.setAutoReading(false); + CompletableFuture switchFuture; + if (serverConn.getPlayer().getConnection() + .getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + switchFuture = ((ClientPlaySessionHandler) serverConn.getPlayer().getConnection() + .getActiveSessionHandler()).doSwitch(); + } else { + switchFuture = CompletableFuture.completedFuture(null); + } + switchFuture.thenAcceptAsync((unused) -> { + smc.write(new LoginAcknowledged()); + // Sync backend + smc.setActiveSessionHandler(StateRegistry.CONFIG, + new ConfigSessionHandler(server, serverConn, resultFuture)); + smc.setAutoReading(true); + }, smc.eventLoop()); + } - // Switch to the transition handler. - smc.setSessionHandler(new TransitionSessionHandler(server, serverConn, resultFuture)); return true; } @@ -165,12 +185,12 @@ public class LoginSessionHandler implements MinecraftSessionHandler { @Override public void disconnected() { if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.LEGACY) { - resultFuture.completeExceptionally( - new QuietRuntimeException("The connection to the remote server was unexpectedly closed.\n" - + "This is usually because the remote server does not have BungeeCord IP forwarding " + resultFuture.completeExceptionally(new QuietRuntimeException( + "The connection to the remote server was unexpectedly closed.\n" + + "This is usually because the remote server " + + "does not have BungeeCord IP forwarding " + "correctly enabled.\nSee https://velocitypowered.com/wiki/users/forwarding/ " - + "for instructions on how to configure player info forwarding correctly.") - ); + + "for instructions on how to configure player info forwarding correctly.")); } else { resultFuture.completeExceptionally( new QuietRuntimeException("The connection to the remote server was unexpectedly closed.") diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java index 5fe6886dc..30d1f4242 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/TransitionSessionHandler.java @@ -32,6 +32,7 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionMessages; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; @@ -120,17 +121,21 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Change the client to use the ClientPlaySessionHandler if required. ClientPlaySessionHandler playHandler; - if (player.getConnection().getSessionHandler() instanceof ClientPlaySessionHandler) { - playHandler = (ClientPlaySessionHandler) player.getConnection().getSessionHandler(); + if (player.getConnection() + .getActiveSessionHandler() instanceof ClientPlaySessionHandler) { + playHandler = + (ClientPlaySessionHandler) player.getConnection().getActiveSessionHandler(); } else { playHandler = new ClientPlaySessionHandler(server, player); - player.getConnection().setSessionHandler(playHandler); + player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, playHandler); } + assert playHandler != null; playHandler.handleBackendJoinGame(packet, serverConn); // Set the new play session handler for the server. We will have nothing more to do // with this connection once this task finishes up. - smc.setSessionHandler(new BackendPlaySessionHandler(server, serverConn)); + smc.setActiveSessionHandler(StateRegistry.PLAY, + new BackendPlaySessionHandler(server, serverConn)); // Clean up disabling auto-read while the connected event was being processed. smc.setAutoReading(true); @@ -138,12 +143,15 @@ public class TransitionSessionHandler implements MinecraftSessionHandler { // Now set the connected server. serverConn.getPlayer().setConnectedServer(serverConn); + if (player.getClientSettingsPacket() != null) { + serverConn.ensureConnected().write(player.getClientSettingsPacket()); + } + // We're done! :) server.getEventManager().fireAndForget(new ServerPostConnectEvent(player, previousServer)); resultFuture.complete(ConnectionRequestResults.successful(serverConn.getServer())); - }, smc.eventLoop()) - .exceptionally(exc -> { + }, smc.eventLoop()).exceptionally(exc -> { logger.error("Unable to switch to new server {} for {}", serverConn.getServerInfo().getName(), player.getUsername(), exc); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java index a2300b978..9a4b7d72d 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/VelocityServerConnection.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.config.PlayerInfoForwarding; import com.velocitypowered.proxy.connection.ConnectionTypes; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.protocol.StateRegistry; @@ -91,8 +92,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, /** * Connects to the server. * - * @return a {@link com.velocitypowered.api.proxy.ConnectionRequestBuilder.Result} representing - * whether or not the connect succeeded + * @return a {@link com.velocitypowered.api.proxy.ConnectionRequestBuilder.Result} + * representing whether the connection succeeded */ public CompletableFuture connect() { CompletableFuture result = new CompletableFuture<>(); @@ -108,15 +109,21 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, future.channel().pipeline().addLast(HANDLER, connection); // Kick off the connection process - connection.setSessionHandler( - new LoginSessionHandler(server, VelocityServerConnection.this, result)); + if (!connection.setActiveSessionHandler(StateRegistry.HANDSHAKE)) { + MinecraftSessionHandler handler = + new LoginSessionHandler(server, VelocityServerConnection.this, result); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, handler); + connection.addSessionHandler(StateRegistry.LOGIN, handler); + } - // Set the connection phase, which may, for future forge (or whatever), be determined + // Set the connection phase, which may, for future forge (or whatever), be + // determined // at this point already connectionPhase = connection.getType().getInitialBackendPhase(); startHandshake(); } else { - // Complete the result immediately. ConnectedPlayer will reset the in-flight connection. + // Complete the result immediately. ConnectedPlayer will reset the in-flight + // connection. result.completeExceptionally(future.cause()); } }); @@ -137,10 +144,8 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake, // separated by \0 (the null byte). In order, you send the original host, the player's IP, their // UUID (undashed), and if you are in online-mode, their login properties (from Mojang). - StringBuilder data = new StringBuilder() - .append(proxyPlayer.getVirtualHost() - .orElseGet(() -> registeredServer.getServerInfo().getAddress()) - .getHostString()) + StringBuilder data = new StringBuilder().append(proxyPlayer.getVirtualHost().orElseGet(() -> + registeredServer.getServerInfo().getAddress()).getHostString()) .append('\0') .append(getPlayerRemoteAddressAsString()) .append('\0') @@ -157,12 +162,10 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, private String createBungeeGuardForwardingAddress(byte[] forwardingSecret) { // Append forwarding secret as a BungeeGuard token. - Property property = new Property("bungeeguard-token", - new String(forwardingSecret, StandardCharsets.UTF_8), ""); - return createLegacyForwardingAddress(properties -> ImmutableList.builder() - .addAll(properties) - .add(property) - .build()); + Property property = + new Property("bungeeguard-token", new String(forwardingSecret, StandardCharsets.UTF_8), ""); + return createLegacyForwardingAddress( + properties -> ImmutableList.builder().addAll(properties).add(property).build()); } private void startHandshake() { @@ -171,9 +174,9 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, // Initiate the handshake. ProtocolVersion protocolVersion = proxyPlayer.getConnection().getProtocolVersion(); - String playerVhost = proxyPlayer.getVirtualHost() - .orElseGet(() -> registeredServer.getServerInfo().getAddress()) - .getHostString(); + String playerVhost = + proxyPlayer.getVirtualHost().orElseGet(() -> registeredServer.getServerInfo().getAddress()) + .getHostString(); Handshake handshake = new Handshake(); handshake.setNextStatus(StateRegistry.LOGIN_ID); @@ -193,7 +196,7 @@ public class VelocityServerConnection implements MinecraftConnectionAssociation, mc.delayedWrite(handshake); mc.setProtocolVersion(protocolVersion); - mc.setState(StateRegistry.LOGIN); + mc.setActiveSessionHandler(StateRegistry.LOGIN); if (proxyPlayer.getIdentifiedKey() == null && proxyPlayer.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_19_3) >= 0) { mc.delayedWrite(new ServerLogin(proxyPlayer.getUsername(), proxyPlayer.getUniqueId())); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java index f68d3bb59..603cf686a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/AuthSessionHandler.java @@ -26,6 +26,7 @@ import com.velocitypowered.api.event.connection.PostLoginEvent; import com.velocitypowered.api.event.permission.PermissionsSetupEvent; import com.velocitypowered.api.event.player.GameProfileRequestEvent; import com.velocitypowered.api.event.player.PlayerChooseInitialServerEvent; +import com.velocitypowered.api.network.ProtocolVersion; import com.velocitypowered.api.permission.PermissionFunction; import com.velocitypowered.api.proxy.crypto.IdentifiedKey; import com.velocitypowered.api.proxy.server.RegisteredServer; @@ -38,6 +39,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.ServerLoginSuccess; import com.velocitypowered.proxy.protocol.packet.SetCompression; import io.netty.buffer.ByteBuf; @@ -64,6 +66,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { private GameProfile profile; private @MonotonicNonNull ConnectedPlayer connectedPlayer; private final boolean onlineMode; + private State loginState = State.START; // 1.20.2+ AuthSessionHandler(VelocityServer server, LoginInboundConnection inbound, GameProfile profile, boolean onlineMode) { @@ -95,8 +98,9 @@ public class AuthSessionHandler implements MinecraftSessionHandler { inbound.getIdentifiedKey()); this.connectedPlayer = player; if (!server.canRegisterConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy", - NamedTextColor.RED), true); + player.disconnect0( + Component.translatable("velocity.error.already-connected-proxy", NamedTextColor.RED), + true); return CompletableFuture.completedFuture(null); } @@ -109,16 +113,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler { // wait for permissions to load, then set the players permission function final PermissionFunction function = event.createFunction(player); if (function == null) { - logger.error( - "A plugin permission provider {} provided an invalid permission function" - + " for player {}. This is a bug in the plugin, not in Velocity. Falling" - + " back to the default permission function.", - event.getProvider().getClass().getName(), - player.getUsername()); + logger.error("A plugin permission provider {} provided an invalid permission " + + "function for player {}. This is a bug in the plugin, not in " + + "Velocity. Falling back to the default permission function.", + event.getProvider().getClass().getName(), player.getUsername()); } else { player.setPermissionFunction(function); } - completeLoginProtocolPhaseAndInitialize(player); + startLoginCompletion(player); } }, mcConnection.eventLoop()); }, mcConnection.eventLoop()).exceptionally((ex) -> { @@ -127,7 +129,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler { }); } - private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { + private void startLoginCompletion(ConnectedPlayer player) { int threshold = server.getConfiguration().getCompressionThreshold(); if (threshold >= 0 && mcConnection.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { mcConnection.write(new SetCompression(threshold)); @@ -165,64 +167,87 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } } - ServerLoginSuccess success = new ServerLoginSuccess(); - success.setUsername(player.getUsername()); - success.setProperties(player.getGameProfileProperties()); - success.setUuid(playerUniqueId); - mcConnection.write(success); + completeLoginProtocolPhaseAndInitialize(player); + } + @Override + public boolean handle(LoginAcknowledged packet) { + if (loginState != State.SUCCESS_SENT) { + inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data")); + } else { + loginState = State.ACKNOWLEDGED; + mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, + new ClientConfigSessionHandler(server, connectedPlayer)); + + server.getEventManager().fire(new PostLoginEvent(connectedPlayer)) + .thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", connectedPlayer, ex); + return null; + }); + } + return true; + } + + private void completeLoginProtocolPhaseAndInitialize(ConnectedPlayer player) { mcConnection.setAssociation(player); - mcConnection.setState(StateRegistry.PLAY); - server.getEventManager().fire(new LoginEvent(player)) - .thenAcceptAsync(event -> { - if (mcConnection.isClosed()) { - // The player was disconnected - server.getEventManager().fireAndForget(new DisconnectEvent(player, - DisconnectEvent.LoginStatus.CANCELLED_BY_USER_BEFORE_COMPLETE)); - return; - } + server.getEventManager().fire(new LoginEvent(player)).thenAcceptAsync(event -> { + if (mcConnection.isClosed()) { + // The player was disconnected + server.getEventManager().fireAndForget(new DisconnectEvent(player, + DisconnectEvent.LoginStatus.CANCELLED_BY_USER_BEFORE_COMPLETE)); + return; + } - Optional reason = event.getResult().getReasonComponent(); - if (reason.isPresent()) { - player.disconnect0(reason.get(), true); - } else { - if (!server.registerConnection(player)) { - player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), - true); - return; - } + Optional reason = event.getResult().getReasonComponent(); + if (reason.isPresent()) { + player.disconnect0(reason.get(), true); + } else { + if (!server.registerConnection(player)) { + player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), + true); + return; + } - mcConnection.setSessionHandler(new InitialConnectSessionHandler(player, server)); - server.getEventManager().fire(new PostLoginEvent(player)) - .thenCompose((ignored) -> connectToInitialServer(player)) - .exceptionally((ex) -> { - logger.error("Exception while connecting {} to initial server", player, ex); - return null; - }); - } - }, mcConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception while completing login initialisation phase for {}", player, ex); - return null; - }); + ServerLoginSuccess success = new ServerLoginSuccess(); + success.setUsername(player.getUsername()); + success.setProperties(player.getGameProfileProperties()); + success.setUuid(player.getUniqueId()); + mcConnection.write(success); + + loginState = State.SUCCESS_SENT; + if (inbound.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + loginState = State.ACKNOWLEDGED; + mcConnection.setActiveSessionHandler(StateRegistry.PLAY, + new InitialConnectSessionHandler(player, server)); + server.getEventManager().fire(new PostLoginEvent(player)) + .thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> { + logger.error("Exception while connecting {} to initial server", player, ex); + return null; + }); + } + } + }, mcConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception while completing login initialisation phase for {}", player, ex); + return null; + }); } private CompletableFuture connectToInitialServer(ConnectedPlayer player) { Optional initialFromConfig = player.getNextServerToTry(); - PlayerChooseInitialServerEvent event = new PlayerChooseInitialServerEvent(player, - initialFromConfig.orElse(null)); + PlayerChooseInitialServerEvent event = + new PlayerChooseInitialServerEvent(player, initialFromConfig.orElse(null)); - return server.getEventManager().fire(event) - .thenRunAsync(() -> { - Optional toTry = event.getInitialServer(); - if (!toTry.isPresent()) { - player.disconnect0(Component.translatable("velocity.error.no-available-servers", - NamedTextColor.RED), true); - return; - } - player.createConnectionRequest(toTry.get()).fireAndForget(); - }, mcConnection.eventLoop()); + return server.getEventManager().fire(event).thenRunAsync(() -> { + Optional toTry = event.getInitialServer(); + if (!toTry.isPresent()) { + player.disconnect0( + Component.translatable("velocity.error.no-available-servers", NamedTextColor.RED), + true); + return; + } + player.createConnectionRequest(toTry.get()).fireAndForget(); + }, mcConnection.eventLoop()); } @Override @@ -237,4 +262,8 @@ public class AuthSessionHandler implements MinecraftSessionHandler { } this.inbound.cleanup(); } + + static enum State { + START, SUCCESS_SENT, ACKNOWLEDGED + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java new file mode 100644 index 000000000..83d9fe91b --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientConfigSessionHandler.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.client; + +import com.velocitypowered.proxy.VelocityServer; +import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.ClientSettings; +import com.velocitypowered.proxy.protocol.packet.KeepAlive; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.ResourcePackResponse; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import io.netty.buffer.ByteBuf; +import java.util.concurrent.CompletableFuture; +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; + +/** + * Handles the client config stage. + */ +public class ClientConfigSessionHandler implements MinecraftSessionHandler { + + private static final Logger logger = LogManager.getLogger(ClientConfigSessionHandler.class); + private final VelocityServer server; + private final ConnectedPlayer player; + + private CompletableFuture configSwitchFuture; + + /** + * Constructs a client config session handler. + * + * @param server the Velocity server instance + * @param player the player + */ + public ClientConfigSessionHandler(VelocityServer server, ConnectedPlayer player) { + this.server = server; + this.player = player; + } + + @Override + public void activated() { + configSwitchFuture = new CompletableFuture<>(); + } + + @Override + public void deactivated() { + } + + @Override + public boolean handle(KeepAlive packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId()); + if (sentTime != null) { + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null) { + player.setPing(System.currentTimeMillis() - sentTime); + smc.write(packet); + } + } + } + return true; + } + + @Override + public boolean handle(ClientSettings packet) { + player.setClientSettingsPacket(packet); + return true; + } + + @Override + public boolean handle(ResourcePackResponse packet) { + if (player.getConnectionInFlight() != null) { + player.getConnectionInFlight().ensureConnected().write(packet); + } + return player.onResourcePackResponse(packet.getStatus()); + } + + @Override + public boolean handle(FinishedUpdate packet) { + player.getConnection() + .setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player)); + + configSwitchFuture.complete(null); + return true; + } + + @Override + public void handleGeneric(MinecraftPacket packet) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && serverConnection.getPhase().consideredComplete()) { + if (packet instanceof PluginMessage) { + ((PluginMessage) packet).retain(); + } + smc.write(packet); + } + } + + @Override + public void handleUnknown(ByteBuf buf) { + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return; + } + + MinecraftConnection smc = serverConnection.getConnection(); + if (smc != null && !smc.isClosed() && serverConnection.getPhase().consideredComplete()) { + smc.write(buf.retain()); + } + } + + @Override + public void disconnected() { + player.teardown(); + } + + @Override + public void exception(Throwable throwable) { + player.disconnect( + Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED)); + } + + /** + * Handles the backend finishing the config stage. + * + * @param serverConn the server connection + * @return a future that completes when the config stage is finished + */ + public CompletableFuture handleBackendFinishUpdate(VelocityServerConnection serverConn) { + player.getConnection().write(new FinishedUpdate()); + serverConn.ensureConnected().write(new FinishedUpdate()); + return configSwitchFuture; + } +} 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 8993d9b24..d2da3aa8e 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 @@ -17,9 +17,6 @@ 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; import com.google.common.collect.ImmutableList; @@ -67,6 +64,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.session.SessionChatHandler import com.velocitypowered.proxy.protocol.packet.chat.session.SessionCommandHandler; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.protocol.util.PluginMessageUtil; import com.velocitypowered.proxy.util.CharacterUtil; @@ -80,6 +78,7 @@ import java.util.Collection; import java.util.List; import java.util.Queue; import java.util.UUID; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentLinkedQueue; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; @@ -105,6 +104,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { private final CommandHandler commandHandler; private final ChatTimeKeeper timeKeeper = new ChatTimeKeeper(); + private CompletableFuture configSwitchFuture; + /** * Constructs a client play session handler. * @@ -151,8 +152,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public void activated() { - Collection channels = server.getChannelRegistrar() - .getChannelsForProtocol(player.getProtocolVersion()); + configSwitchFuture = new CompletableFuture<>(); + Collection channels = + server.getChannelRegistrar().getChannelsForProtocol(player.getProtocolVersion()); if (!channels.isEmpty()) { PluginMessage register = constructChannelsPacket(player.getProtocolVersion(), channels); player.getConnection().write(register); @@ -185,7 +187,13 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { @Override public boolean handle(ClientSettings packet) { player.setPlayerSettings(packet); - return false; // will forward onto the server + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection == null) { + // No server connection yet, probably transitioning. + return true; + } + player.getConnectedServer().ensureConnected().write(packet); + return true; // will forward onto the server } @Override @@ -288,10 +296,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { MinecraftConnection backendConn = serverConn != null ? serverConn.getConnection() : null; if (serverConn != null && backendConn != null) { if (backendConn.getState() != StateRegistry.PLAY) { - logger.warn( - "A plugin message was received while the backend server was not " - + "ready. Channel: {}. Packet discarded.", - packet.getChannel()); + logger.warn("A plugin message was received while the backend server was not " + + "ready. Channel: {}. Packet discarded.", packet.getChannel()); } else if (PluginMessageUtil.isRegister(packet)) { List channels = PluginMessageUtil.getChannels(packet); List channelIdentifiers = new ArrayList<>(); @@ -377,6 +383,26 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { return player.onResourcePackResponse(packet.getStatus()); } + @Override + public boolean handle(FinishedUpdate packet) { + // Complete client switch + player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG); + VelocityServerConnection serverConnection = player.getConnectedServer(); + if (serverConnection != null) { + MinecraftConnection smc = serverConnection.ensureConnected(); + CompletableFuture.runAsync(() -> { + smc.write(packet); + smc.setActiveSessionHandler(StateRegistry.CONFIG); + smc.setAutoReading(true); + }, smc.eventLoop()).exceptionally((ex) -> { + logger.error("Error forwarding config state acknowledgement to server:", ex); + return null; + }); + } + configSwitchFuture.complete(null); + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { VelocityServerConnection serverConnection = player.getConnectedServer(); @@ -440,6 +466,33 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } + /** + * Handles switching stages for swapping between servers. + * + * @return a future that completes when the switch is complete + */ + public CompletableFuture doSwitch() { + VelocityServerConnection existingConnection = player.getConnectedServer(); + + if (existingConnection != null) { + // Shut down the existing server connection. + player.setConnectedServer(null); + existingConnection.disconnect(); + + // Send keep alive to try to avoid timeouts + player.sendKeepAlive(); + + // Reset Tablist header and footer to prevent desync + player.clearHeaderAndFooter(); + } + + spawned = false; + + player.switchToConfigState(); + + return configSwitchFuture; + } + /** * Handles the {@code JoinGame} packet. This function is responsible for handling the client-side * switching servers in Velocity. @@ -482,14 +535,6 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } serverBossBars.clear(); - // Tell the server about the proxy's plugin message channels. - ProtocolVersion serverVersion = serverMc.getProtocolVersion(); - final Collection channels = server.getChannelRegistrar() - .getChannelsForProtocol(serverMc.getProtocolVersion()); - if (!channels.isEmpty()) { - serverMc.delayedWrite(constructChannelsPacket(serverVersion, channels)); - } - // If we had plugin messages queued during login/FML handshake, send them now. PluginMessage pm; while ((pm = loginPluginMessages.poll()) != null) { @@ -497,7 +542,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } // Clear any title from the previous server. - if (player.getProtocolVersion().compareTo(MINECRAFT_1_8) >= 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_8) >= 0) { player.getConnection().delayedWrite( GenericTitlePacket.constructTitlePacket(GenericTitlePacket.ActionType.RESET, player.getProtocolVersion())); @@ -520,7 +565,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { // improving compatibility with mods. final Respawn respawn = Respawn.fromJoinGame(joinGame); - if (player.getProtocolVersion().compareTo(MINECRAFT_1_16) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.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. @@ -562,7 +607,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String commandLabel = command.substring(0, commandEndPosition); if (!server.getCommandManager().hasCommand(commandLabel)) { - if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide // additional tab completion support. outstandingTabComplete = packet; @@ -604,7 +649,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } private boolean handleRegularTabComplete(TabCompleteRequest packet) { - if (player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0) { + if (player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0) { // Outstanding tab completes are recorded for use with 1.12 clients and below to provide // additional tab completion support. outstandingTabComplete = packet; @@ -635,7 +680,8 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { String command = request.getCommand().substring(1); server.getCommandManager().offerBrigadierSuggestions(player, command) .thenAcceptAsync(offers -> { - boolean legacy = player.getProtocolVersion().compareTo(MINECRAFT_1_13) < 0; + boolean legacy = + player.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_13) < 0; try { for (Suggestion suggestion : offers.getList()) { String offer = suggestion.getText(); @@ -659,9 +705,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } }, player.getConnection().eventLoop()).exceptionally((ex) -> { logger.error( - "Exception while finishing command tab completion, with request {} and response {}", - request, - response, ex); + "Exception while finishing command tab completion," + + " with request {} and response {}", + request, response, ex); return null; }); } @@ -680,9 +726,9 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { player.getConnection().write(response); }, player.getConnection().eventLoop()).exceptionally((ex) -> { logger.error( - "Exception while finishing regular tab completion, with request {} and response{}", - request, - response, ex); + "Exception while finishing regular tab completion," + + " with request {} and response{}", + request, response, ex); return null; }); } @@ -702,5 +748,4 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler { } } } - } 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 90464c9c8..c922e933a 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 @@ -59,6 +59,7 @@ import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl; import com.velocitypowered.proxy.connection.util.VelocityInboundConnection; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.packet.ClientSettings; import com.velocitypowered.proxy.protocol.packet.Disconnect; import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; @@ -69,6 +70,7 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue; import com.velocitypowered.proxy.protocol.packet.chat.ChatType; import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket; import com.velocitypowered.proxy.server.VelocityRegisteredServer; import com.velocitypowered.proxy.tablist.InternalTabList; @@ -123,12 +125,9 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private static final int MAX_PLUGIN_CHANNELS = 1024; private static final PlainTextComponentSerializer PASS_THRU_TRANSLATE = - PlainTextComponentSerializer.builder() - .flattener(ComponentFlattener.basic().toBuilder() - .mapper(KeybindComponent.class, c -> "") - .mapper(TranslatableComponent.class, TranslatableComponent::key) - .build()) - .build(); + PlainTextComponentSerializer.builder().flattener( + ComponentFlattener.basic().toBuilder().mapper(KeybindComponent.class, c -> "") + .mapper(TranslatableComponent.class, TranslatableComponent::key).build()).build(); static final PermissionProvider DEFAULT_PERMISSIONS = s -> PermissionFunction.ALWAYS_UNDEFINED; private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class); @@ -159,17 +158,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, private final Queue outstandingResourcePacks = new ArrayDeque<>(); private @Nullable ResourcePackInfo pendingResourcePack; private @Nullable ResourcePackInfo appliedResourcePack; - private final @NotNull Pointers pointers = Player.super.pointers().toBuilder() - .withDynamic(Identity.UUID, this::getUniqueId) - .withDynamic(Identity.NAME, this::getUsername) - .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) - .withDynamic(Identity.LOCALE, this::getEffectiveLocale) - .withStatic(PermissionChecker.POINTER, getPermissionChecker()) - .withStatic(FacetPointers.TYPE, Type.PLAYER) - .build(); + private final @NotNull Pointers pointers = + Player.super.pointers().toBuilder().withDynamic(Identity.UUID, this::getUniqueId) + .withDynamic(Identity.NAME, this::getUsername) + .withDynamic(Identity.DISPLAY_NAME, () -> Component.text(this.getUsername())) + .withDynamic(Identity.LOCALE, this::getEffectiveLocale) + .withStatic(PermissionChecker.POINTER, getPermissionChecker()) + .withStatic(FacetPointers.TYPE, Type.PLAYER).build(); private @Nullable String clientBrand; private @Nullable Locale effectiveLocale; private @Nullable IdentifiedKey playerKey; + private @Nullable ClientSettings clientSettingsPacket; private final ChatQueue chatQueue; private final ChatBuilderFactory chatBuilderFactory; @@ -278,11 +277,19 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return settings == null ? ClientSettingsWrapper.DEFAULT : this.settings; } + public ClientSettings getClientSettingsPacket() { + return clientSettingsPacket; + } + @Override public boolean hasSentPlayerSettings() { return settings != null; } + public void setClientSettingsPacket(ClientSettings clientSettingsPacket) { + this.clientSettingsPacket = clientSettingsPacket; + } + void setPlayerSettings(ClientSettings settings) { ClientSettingsWrapper cs = new ClientSettingsWrapper(settings); this.settings = cs; @@ -674,8 +681,8 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, ServerKickResult result; if (kickedFromCurrent) { Optional next = getNextServerToTry(rs); - result = next.map(RedirectPlayer::create) - .orElseGet(() -> DisconnectPlayer.create(friendlyReason)); + result = + next.map(RedirectPlayer::create).orElseGet(() -> DisconnectPlayer.create(friendlyReason)); } else { // If we were kicked by going to another server, the connection should not be in flight if (connectionInFlight != null && connectionInFlight.getServer().equals(rs)) { @@ -689,86 +696,83 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } private void handleKickEvent(KickedFromServerEvent originalEvent, Component friendlyReason, - boolean kickedFromCurrent) { - server.getEventManager().fire(originalEvent) - .thenAcceptAsync(event -> { - // There can't be any connection in flight now. - connectionInFlight = null; + boolean kickedFromCurrent) { + server.getEventManager().fire(originalEvent).thenAcceptAsync(event -> { + // There can't be any connection in flight now. + connectionInFlight = null; - // Make sure we clear the current connected server as the connection is invalid. - VelocityServerConnection previousConnection = connectedServer; - if (kickedFromCurrent) { - connectedServer = null; - } + // Make sure we clear the current connected server as the connection is invalid. + VelocityServerConnection previousConnection = connectedServer; + if (kickedFromCurrent) { + connectedServer = null; + } - if (!isActive()) { - // If the connection is no longer active, it makes no sense to try and recover it. - return; - } + if (!isActive()) { + // If the connection is no longer active, it makes no sense to try and recover it. + return; + } - if (event.getResult() instanceof DisconnectPlayer) { - DisconnectPlayer res = (DisconnectPlayer) event.getResult(); - disconnect(res.getReasonComponent()); - } else if (event.getResult() instanceof RedirectPlayer) { - RedirectPlayer res = (RedirectPlayer) event.getResult(); - createConnectionRequest(res.getServer(), previousConnection) - .connect() - .whenCompleteAsync((status, throwable) -> { - if (throwable != null) { - handleConnectionException(status != null ? status.getAttemptedConnection() - : res.getServer(), throwable, true); - return; + if (event.getResult() instanceof DisconnectPlayer) { + DisconnectPlayer res = (DisconnectPlayer) event.getResult(); + disconnect(res.getReasonComponent()); + } else if (event.getResult() instanceof RedirectPlayer) { + RedirectPlayer res = (RedirectPlayer) event.getResult(); + createConnectionRequest(res.getServer(), previousConnection).connect() + .whenCompleteAsync((status, throwable) -> { + if (throwable != null) { + handleConnectionException( + status != null ? status.getAttemptedConnection() : res.getServer(), throwable, + true); + return; + } + + switch (status.getStatus()) { + // Impossible/nonsensical cases + case ALREADY_CONNECTED: + logger.error("{}: already connected to {}", this, + status.getAttemptedConnection().getServerInfo().getName()); + break; + case CONNECTION_IN_PROGRESS: + // Fatal case + case CONNECTION_CANCELLED: + Component fallbackMsg = res.getMessageComponent(); + if (fallbackMsg == null) { + fallbackMsg = friendlyReason; } - - switch (status.getStatus()) { - // Impossible/nonsensical cases - case ALREADY_CONNECTED: - logger.error("{}: already connected to {}", - this, - status.getAttemptedConnection().getServerInfo().getName() - ); - break; - case CONNECTION_IN_PROGRESS: - // Fatal case - case CONNECTION_CANCELLED: - Component fallbackMsg = res.getMessageComponent(); - if (fallbackMsg == null) { - fallbackMsg = friendlyReason; - } - disconnect(status.getReasonComponent().orElse(fallbackMsg)); - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); - handleConnectionException(res.getServer(), Disconnect.create(reason, - getProtocolVersion()), ((Impl) status).isSafe()); - break; - case SUCCESS: - Component requestedMessage = res.getMessageComponent(); - if (requestedMessage == null) { - requestedMessage = friendlyReason; - } - if (requestedMessage != Component.empty()) { - sendMessage(requestedMessage); - } - break; - default: - // The only remaining value is successful (no need to do anything!) - break; + disconnect(status.getReasonComponent().orElse(fallbackMsg)); + break; + case SERVER_DISCONNECTED: + Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + handleConnectionException(res.getServer(), + Disconnect.create(reason, getProtocolVersion()), ((Impl) status).isSafe()); + break; + case SUCCESS: + Component requestedMessage = res.getMessageComponent(); + if (requestedMessage == null) { + requestedMessage = friendlyReason; } - }, connection.eventLoop()); - } else if (event.getResult() instanceof Notify) { - Notify res = (Notify) event.getResult(); - if (event.kickedDuringServerConnect() && previousConnection != null) { - sendMessage(Identity.nil(), res.getMessageComponent()); - } else { - disconnect(res.getMessageComponent()); - } - } else { - // In case someone gets creative, assume we want to disconnect the player. - disconnect(friendlyReason); - } - }, connection.eventLoop()); + if (requestedMessage != Component.empty()) { + sendMessage(requestedMessage); + } + break; + default: + // The only remaining value is successful (no need to do anything!) + break; + } + }, connection.eventLoop()); + } else if (event.getResult() instanceof Notify) { + Notify res = (Notify) event.getResult(); + if (event.kickedDuringServerConnect() && previousConnection != null) { + sendMessage(Identity.nil(), res.getMessageComponent()); + } else { + disconnect(res.getMessageComponent()); + } + } else { + // In case someone gets creative, assume we want to disconnect the player. + disconnect(friendlyReason); + } + }, connection.eventLoop()); } /** @@ -1021,6 +1025,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, return pendingResourcePack; } + /** + * Clears the applied resource pack field. + */ + public void clearAppliedResourcePack() { + appliedResourcePack = null; + } + /** * Processes a client response to a sent resource-pack. */ @@ -1068,18 +1079,42 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, && queued.getOriginalOrigin() != ResourcePackInfo.Origin.DOWNSTREAM_SERVER; } + /** + * Gives an indication about the previous resource pack responses. + */ + public @Nullable Boolean getPreviousResourceResponse() { + return previousResourceResponse; + } + /** * Sends a {@link KeepAlive} packet to the player with a random ID. The response will be ignored * by Velocity as it will not match the ID last sent by the server. */ public void sendKeepAlive() { - if (connection.getState() == StateRegistry.PLAY) { + if (connection.getState() == StateRegistry.PLAY + || connection.getState() == StateRegistry.CONFIG) { KeepAlive keepAlive = new KeepAlive(); keepAlive.setRandomId(ThreadLocalRandom.current().nextLong()); connection.write(keepAlive); } } + /** + * Switches the connection to the client into config state. + */ + public void switchToConfigState() { + CompletableFuture.runAsync(() -> { + connection.write(new StartUpdate()); + connection.getChannel().pipeline() + .get(MinecraftEncoder.class).setState(StateRegistry.CONFIG); + // Make sure we don't send any play packets to the player after update start + connection.addPlayPacketQueueHandler(); + }, connection.eventLoop()).exceptionally((ex) -> { + logger.error("Error switching player connection to config state:", ex); + return null; + }); + } + /** * Gets the current "phase" of the connection, mostly used for tracking modded negotiation for * legacy forge servers and provides methods for performing phase specific actions. @@ -1147,37 +1182,34 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, } private CompletableFuture internalConnect() { - return this.getInitialStatus() - .thenCompose(initialCheck -> { - if (initialCheck.isPresent()) { - return completedFuture(plainResult(initialCheck.get(), toConnect)); - } + return this.getInitialStatus().thenCompose(initialCheck -> { + if (initialCheck.isPresent()) { + return completedFuture(plainResult(initialCheck.get(), toConnect)); + } - ServerPreConnectEvent event = new ServerPreConnectEvent(ConnectedPlayer.this, - toConnect, previousServer); - return server.getEventManager().fire(event) - .thenComposeAsync(newEvent -> { - Optional newDest = newEvent.getResult().getServer(); - if (!newDest.isPresent()) { - return completedFuture( - plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect) - ); - } + ServerPreConnectEvent event = + new ServerPreConnectEvent(ConnectedPlayer.this, toConnect, previousServer); + return server.getEventManager().fire(event).thenComposeAsync(newEvent -> { + Optional newDest = newEvent.getResult().getServer(); + if (!newDest.isPresent()) { + return completedFuture( + plainResult(ConnectionRequestBuilder.Status.CONNECTION_CANCELLED, toConnect)); + } - RegisteredServer realDestination = newDest.get(); - Optional check = checkServer(realDestination); - if (check.isPresent()) { - return completedFuture(plainResult(check.get(), realDestination)); - } + RegisteredServer realDestination = newDest.get(); + Optional check = checkServer(realDestination); + if (check.isPresent()) { + return completedFuture(plainResult(check.get(), realDestination)); + } - VelocityRegisteredServer vrs = (VelocityRegisteredServer) realDestination; - VelocityServerConnection con = new VelocityServerConnection(vrs, - previousServer, ConnectedPlayer.this, server); - connectionInFlight = con; - return con.connect().whenCompleteAsync( - (result, exception) -> this.resetIfInFlightIs(con), connection.eventLoop()); - }, connection.eventLoop()); - }); + VelocityRegisteredServer vrs = (VelocityRegisteredServer) realDestination; + VelocityServerConnection con = + new VelocityServerConnection(vrs, previousServer, ConnectedPlayer.this, server); + connectionInFlight = con; + return con.connect().whenCompleteAsync((result, exception) -> this.resetIfInFlightIs(con), + connection.eventLoop()); + }, connection.eventLoop()); + }); } private void resetIfInFlightIs(VelocityServerConnection establishedConnection) { @@ -1188,50 +1220,46 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player, @Override public CompletableFuture connect() { - return this.internalConnect() - .whenCompleteAsync((status, throwable) -> { - if (status != null && !status.isSuccessful()) { - if (!status.isSafe()) { - handleConnectionException(status.getAttemptedConnection(), throwable, false); - } - } - }, connection.eventLoop()) - .thenApply(x -> x); + return this.internalConnect().whenCompleteAsync((status, throwable) -> { + if (status != null && !status.isSuccessful()) { + if (!status.isSafe()) { + handleConnectionException(status.getAttemptedConnection(), throwable, false); + } + } + }, connection.eventLoop()).thenApply(x -> x); } @Override public CompletableFuture connectWithIndication() { - return internalConnect() - .whenCompleteAsync((status, throwable) -> { - if (throwable != null) { - // TODO: The exception handling from this is not very good. Find a better way. - handleConnectionException(status != null ? status.getAttemptedConnection() - : toConnect, throwable, true); - return; - } + return internalConnect().whenCompleteAsync((status, throwable) -> { + if (throwable != null) { + // TODO: The exception handling from this is not very good. Find a better way. + handleConnectionException(status != null ? status.getAttemptedConnection() : toConnect, + throwable, true); + return; + } - switch (status.getStatus()) { - case ALREADY_CONNECTED: - sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); - break; - case CONNECTION_IN_PROGRESS: - sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); - break; - case CONNECTION_CANCELLED: - // Ignored; the plugin probably already handled this. - break; - case SERVER_DISCONNECTED: - Component reason = status.getReasonComponent() - .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); - handleConnectionException(toConnect, Disconnect.create(reason, - getProtocolVersion()), status.isSafe()); - break; - default: - // The only remaining value is successful (no need to do anything!) - break; - } - }, connection.eventLoop()) - .thenApply(Result::isSuccessful); + switch (status.getStatus()) { + case ALREADY_CONNECTED: + sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED); + break; + case CONNECTION_IN_PROGRESS: + sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS); + break; + case CONNECTION_CANCELLED: + // Ignored; the plugin probably already handled this. + break; + case SERVER_DISCONNECTED: + Component reason = status.getReasonComponent() + .orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR); + handleConnectionException(toConnect, Disconnect.create(reason, getProtocolVersion()), + status.isSafe()); + break; + default: + // The only remaining value is successful (no need to do anything!) + break; + } + }, connection.eventLoop()).thenApply(Result::isSuccessful); } @Override 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 e814efaa3..079ce035f 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 @@ -47,8 +47,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; /** * The initial handler used when a connection is established to the proxy. This will either - * transition to {@link StatusSessionHandler} or {@link InitialLoginSessionHandler} as soon - * as the handshake packet is received. + * transition to {@link StatusSessionHandler} or {@link InitialLoginSessionHandler} as soon as the + * handshake packet is received. */ public class HandshakeSessionHandler implements MinecraftSessionHandler { @@ -65,9 +65,9 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { @Override public boolean handle(LegacyPing packet) { connection.setProtocolVersion(ProtocolVersion.LEGACY); - StatusSessionHandler handler = new StatusSessionHandler(server, - new LegacyInboundConnection(connection, packet)); - connection.setSessionHandler(handler); + StatusSessionHandler handler = + new StatusSessionHandler(server, new LegacyInboundConnection(connection, packet)); + connection.setActiveSessionHandler(StateRegistry.STATUS, handler); handler.handle(packet); return true; } @@ -90,13 +90,13 @@ public class HandshakeSessionHandler implements MinecraftSessionHandler { LOGGER.error("{} provided invalid protocol {}", ic, handshake.getNextStatus()); connection.close(true); } else { - connection.setState(nextState); connection.setProtocolVersion(handshake.getProtocolVersion()); connection.setAssociation(ic); switch (nextState) { case STATUS: - connection.setSessionHandler(new StatusSessionHandler(server, ic)); + connection.setActiveSessionHandler(StateRegistry.STATUS, + new StatusSessionHandler(server, ic)); break; case LOGIN: this.handleLogin(handshake, ic); @@ -140,14 +140,15 @@ 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(Component.translatable( - "velocity.error.modern-forwarding-needs-new-client")); + ic.disconnectQuietly( + Component.translatable("velocity.error.modern-forwarding-needs-new-client")); return; } LoginInboundConnection lic = new LoginInboundConnection(ic); server.getEventManager().fireAndForget(new ConnectionHandshakeEvent(lic)); - connection.setSessionHandler(new InitialLoginSessionHandler(server, connection, lic)); + connection.setActiveSessionHandler(StateRegistry.LOGIN, + new InitialLoginSessionHandler(server, connection, lic)); } private ConnectionType getHandshakeConnectionType(Handshake handshake) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java index 01bef5f92..1409dc4ba 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/InitialLoginSessionHandler.java @@ -34,6 +34,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.crypto.IdentifiedKeyImpl; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.packet.EncryptionRequest; import com.velocitypowered.proxy.protocol.packet.EncryptionResponse; @@ -120,47 +121,45 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { this.login = packet; PreLoginEvent event = new PreLoginEvent(inbound, login.getUsername()); - server.getEventManager().fire(event) - .thenRunAsync(() -> { - if (mcConnection.isClosed()) { - // The player was disconnected - return; + server.getEventManager().fire(event).thenRunAsync(() -> { + if (mcConnection.isClosed()) { + // The player was disconnected + return; + } + + PreLoginComponentResult result = event.getResult(); + Optional disconnectReason = result.getReasonComponent(); + if (disconnectReason.isPresent()) { + // The component is guaranteed to be provided if the connection was denied. + inbound.disconnect(disconnectReason.get()); + return; + } + + inbound.loginEventFired(() -> { + if (mcConnection.isClosed()) { + // The player was disconnected + return; + } + + mcConnection.eventLoop().execute(() -> { + if (!result.isForceOfflineMode() + && (server.getConfiguration().isOnlineMode() || result.isOnlineModeAllowed())) { + // Request encryption. + EncryptionRequest request = generateEncryptionRequest(); + this.verify = Arrays.copyOf(request.getVerifyToken(), 4); + mcConnection.write(request); + this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; + } else { + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, + GameProfile.forOfflinePlayer(login.getUsername()), false)); } - - PreLoginComponentResult result = event.getResult(); - Optional disconnectReason = result.getReasonComponent(); - if (disconnectReason.isPresent()) { - // The component is guaranteed to be provided if the connection was denied. - inbound.disconnect(disconnectReason.get()); - return; - } - - inbound.loginEventFired(() -> { - if (mcConnection.isClosed()) { - // The player was disconnected - return; - } - - mcConnection.eventLoop().execute(() -> { - if (!result.isForceOfflineMode() && (server.getConfiguration().isOnlineMode() - || result.isOnlineModeAllowed())) { - // Request encryption. - EncryptionRequest request = generateEncryptionRequest(); - this.verify = Arrays.copyOf(request.getVerifyToken(), 4); - mcConnection.write(request); - this.currentState = LoginState.ENCRYPTION_REQUEST_SENT; - } else { - mcConnection.setSessionHandler(new AuthSessionHandler( - server, inbound, GameProfile.forOfflinePlayer(login.getUsername()), false - )); - } - }); - }); - }, mcConnection.eventLoop()) - .exceptionally((ex) -> { - logger.error("Exception in pre-login stage", ex); - return null; }); + }); + }, mcConnection.eventLoop()).exceptionally((ex) -> { + logger.error("Exception in pre-login stage", ex); + return null; + }); return true; } @@ -246,13 +245,12 @@ public class InitialLoginSessionHandler implements MinecraftSessionHandler { } } // All went well, initialize the session. - mcConnection.setSessionHandler(new AuthSessionHandler( - server, inbound, profile, true - )); + mcConnection.setActiveSessionHandler(StateRegistry.LOGIN, + new AuthSessionHandler(server, inbound, profile, true)); } else if (profileResponse.getStatusCode() == 204) { // Apparently an offline-mode user logged onto this online-mode proxy. - inbound.disconnect(Component.translatable("velocity.error.online-mode-only", - NamedTextColor.RED)); + inbound.disconnect( + Component.translatable("velocity.error.online-mode-only", NamedTextColor.RED)); } else { // Something else went wrong logger.error( diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java index 5d7542170..19b09c122 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/forge/legacy/LegacyForgeHandshakeClientPhase.java @@ -76,9 +76,9 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { }, /** - * The Mod list is sent to the server, captured by Velocity. Transition to - * {@link #WAITING_SERVER_DATA} when an ACK is sent, which indicates to the server to start - * sending state data. + * The Mod list is sent to the server, captured by Velocity. Transition to {@link + * #WAITING_SERVER_DATA} when an ACK is sent, which indicates to the server to start sending state + * data. */ MOD_LIST(LegacyForgeConstants.ACK_DISCRIMINATOR) { @Override @@ -138,11 +138,10 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { /** * The handshake is complete. The handshake can be reset. * - *

Note that a successful connection to a server does not mean that - * we will be in this state. After a handshake reset, if the next server is vanilla we will still - * be in the {@link #NOT_STARTED} phase, which means we must NOT send a reset packet. This is - * handled by overriding the {@link #resetConnectionPhase(ConnectedPlayer)} in this element (it is - * usually a no-op).

+ *

Note that a successful connection to a server does not mean that we will be in this state. + * After a handshake reset, if the next server is vanilla we will still be in the {@link + * #NOT_STARTED} phase, which means we must NOT send a reset packet. This is handled by overriding + * the {@link #resetConnectionPhase(ConnectedPlayer)} in this element (it is usually a no-op). */ COMPLETE(null) { @Override @@ -165,7 +164,7 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { // just in case the timing is awful player.sendKeepAlive(); - MinecraftSessionHandler handler = backendConn.getSessionHandler(); + MinecraftSessionHandler handler = backendConn.getActiveSessionHandler(); if (handler instanceof ClientPlaySessionHandler) { ((ClientPlaySessionHandler) handler).flushQueuedMessages(); } @@ -182,8 +181,8 @@ public enum LegacyForgeHandshakeClientPhase implements ClientConnectionPhase { * * @param packetToAdvanceOn The ID of the packet discriminator that indicates that the client has * moved onto a new phase, and as such, Velocity should do so too - * (inspecting {@link #nextPhase()}. A null indicates there is no further - * phase to transition to. + * (inspecting {@link #nextPhase()}. A null indicates there is + * no further phase to transition to. */ LegacyForgeHandshakeClientPhase(Integer packetToAdvanceOn) { this.packetToAdvanceOn = packetToAdvanceOn; diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java new file mode 100644 index 000000000..1bca74515 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/ClientConfigData.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.registry; + +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import net.kyori.adventure.key.Key; +import org.jetbrains.annotations.Nullable; + +/** + * Holds the registry data that is sent + * to the client during the config stage. + */ +public class ClientConfigData { + + private final @Nullable VelocityResourcePackInfo resourcePackInfo; + private final DataTag tag; + private final RegistrySync registry; + private final Key[] features; + private final String brand; + + private ClientConfigData(@Nullable VelocityResourcePackInfo resourcePackInfo, DataTag tag, + RegistrySync registry, Key[] features, String brand) { + this.resourcePackInfo = resourcePackInfo; + this.tag = tag; + this.registry = registry; + this.features = features; + this.brand = brand; + } + + public RegistrySync getRegistry() { + return registry; + } + + public DataTag getTag() { + return tag; + } + + public Key[] getFeatures() { + return features; + } + + public @Nullable VelocityResourcePackInfo getResourcePackInfo() { + return resourcePackInfo; + } + + public String getBrand() { + return brand; + } + + /** + * Creates a new builder. + * + * @return ClientConfigData.Builder + */ + public static ClientConfigData.Builder builder() { + return new Builder(); + } + + /** + * Builder for ClientConfigData. + */ + public static class Builder { + private VelocityResourcePackInfo resourcePackInfo; + private DataTag tag; + private RegistrySync registry; + private Key[] features; + private String brand; + + private Builder() { + } + + /** + * Clears the builder. + */ + public void clear() { + this.resourcePackInfo = null; + this.tag = null; + this.registry = null; + this.features = null; + this.brand = null; + } + + public Builder resourcePack(@Nullable VelocityResourcePackInfo resourcePackInfo) { + this.resourcePackInfo = resourcePackInfo; + return this; + } + + public Builder dataTag(DataTag tag) { + this.tag = tag; + return this; + } + + public Builder registry(RegistrySync registry) { + this.registry = registry; + return this; + } + + public Builder features(Key[] features) { + this.features = features; + return this; + } + + public Builder brand(String brand) { + this.brand = brand; + return this; + } + + public ClientConfigData build() { + return new ClientConfigData(resourcePackInfo, tag, registry, features, brand); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java new file mode 100644 index 000000000..9a7d0de73 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/registry/DataTag.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2019-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.connection.registry; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.Keyed; +import org.jetbrains.annotations.NotNull; + +/** + * Represents a data tag. + */ +public class DataTag { + private final ImmutableList entrySets; + + public DataTag(ImmutableList entrySets) { + this.entrySets = entrySets; + } + + /** + * Returns the entry sets. + * + * @return List of entry sets + */ + public List getEntrySets() { + return entrySets; + } + + /** + * Represents a data tag set. + */ + public static class Set implements Keyed { + + private final Key key; + private final ImmutableList entries; + + public Set(Key key, ImmutableList entries) { + this.key = key; + this.entries = entries; + } + + /** + * Returns the entries. + * + * @return List of entries + */ + public List getEntries() { + return entries; + } + + @Override + public @NotNull Key key() { + return key; + } + } + + /** + * Represents a data tag entry. + */ + public static class Entry implements Keyed { + + private final Key key; + private final int[] elements; + + public Entry(Key key, int[] elements) { + this.key = key; + this.elements = elements; + } + + public int[] getElements() { + return elements; + } + + @Override + public @NotNull Key key() { + return key; + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java index dff8d2236..27ec4ba8b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/Connections.java @@ -35,6 +35,7 @@ public class Connections { public static final String MINECRAFT_DECODER = "minecraft-decoder"; public static final String MINECRAFT_ENCODER = "minecraft-encoder"; public static final String READ_TIMEOUT = "read-timeout"; + public static final String PLAY_PACKET_QUEUE = "play-packet-queue"; private Connections() { throw new AssertionError(); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java index 62f38ebd7..ef8e6b1cb 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/network/ServerChannelInitializer.java @@ -29,6 +29,7 @@ import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.LegacyPingDecoder; import com.velocitypowered.proxy.protocol.netty.LegacyPingEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; @@ -67,7 +68,8 @@ public class ServerChannelInitializer extends ChannelInitializer { .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.CLIENTBOUND)); final MinecraftConnection connection = new MinecraftConnection(ch, this.server); - connection.setSessionHandler(new HandshakeSessionHandler(connection, this.server)); + connection.setActiveSessionHandler(StateRegistry.HANDSHAKE, + new HandshakeSessionHandler(connection, this.server)); ch.pipeline().addLast(Connections.HANDLER, connection); if (this.server.getConfiguration().isProxyProtocol()) { 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 97b40def7..e65ee056b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/ProtocolUtils.java @@ -41,6 +41,7 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import net.kyori.adventure.key.Key; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; @@ -202,8 +203,7 @@ public enum ProtocolUtils { buf.readableBytes()); String str = buf.toString(buf.readerIndex(), length, StandardCharsets.UTF_8); buf.skipBytes(length); - checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", - str.length(), cap); + checkFrame(str.length() <= cap, "Got a too-long string (got %s, max %s)", str.length(), cap); return str; } @@ -219,6 +219,59 @@ public enum ProtocolUtils { buf.writeCharSequence(str, StandardCharsets.UTF_8); } + /** + * Reads a standard Mojang Text namespaced:key from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key + */ + public static Key readKey(ByteBuf buf) { + return Key.key(readString(buf), Key.DEFAULT_SEPARATOR); + } + + /** + * Writes a standard Mojang Text namespaced:key to the buffer. + * + * @param buf the buffer to write to + * @param key the key to write + */ + public static void writeKey(ByteBuf buf, Key key) { + writeString(buf, key.asString()); + } + + /** + * Reads a standard Mojang Text namespaced:key array from the buffer. + * + * @param buf the buffer to read from + * @return the decoded key array + */ + public static Key[] readKeyArray(ByteBuf buf) { + int length = readVarInt(buf); + checkFrame(length >= 0, "Got a negative-length array (%s)", length); + checkFrame(buf.isReadable(length), + "Trying to read an array that is too long (wanted %s, only have %s)", length, + buf.readableBytes()); + Key[] ret = new Key[length]; + + for (int i = 0; i < ret.length; i++) { + ret[i] = ProtocolUtils.readKey(buf); + } + return ret; + } + + /** + * Writes a standard Mojang Text namespaced:key array to the buffer. + * + * @param buf the buffer to write to + * @param keys the keys to write + */ + public static void writeKeyArray(ByteBuf buf, Key[] keys) { + writeVarInt(buf, keys.length); + for (Key key : keys) { + writeKey(buf, key); + } + } + public static byte[] readByteArray(ByteBuf buf) { return readByteArray(buf, DEFAULT_MAX_STRING_SIZE); } @@ -368,6 +421,38 @@ public enum ProtocolUtils { } } + /** + * Reads an Integer array from the {@code buf}. + * + * @param buf the buffer to read from + * @return the Integer array from the buffer + */ + public static int[] readVarIntArray(ByteBuf buf) { + int length = readVarInt(buf); + checkFrame(length >= 0, "Got a negative-length array (%s)", length); + checkFrame(buf.isReadable(length), + "Trying to read an array that is too long (wanted %s, only have %s)", length, + buf.readableBytes()); + int[] ret = new int[length]; + for (int i = 0; i < length; i++) { + ret[i] = readVarInt(buf); + } + return ret; + } + + /** + * Writes an Integer Array to the {@code buf}. + * + * @param buf the buffer to write to + * @param intArray the array to write + */ + public static void writeVarIntArray(ByteBuf buf, int[] intArray) { + writeVarInt(buf, intArray.length); + for (int i = 0; i < intArray.length; i++) { + writeVarInt(buf, intArray[i]); + } + } + /** * Writes a list of {@link com.velocitypowered.api.util.GameProfile.Property} to the buffer. * diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index 9cb4aacdc..8845dbeae 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -33,6 +33,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_1; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_3; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_19_4; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_20_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_7_2; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_8; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; @@ -55,8 +56,10 @@ import com.velocitypowered.proxy.protocol.packet.HeaderAndFooter; import com.velocitypowered.proxy.protocol.packet.JoinGame; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; +import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import com.velocitypowered.proxy.protocol.packet.LoginPluginMessage; import com.velocitypowered.proxy.protocol.packet.LoginPluginResponse; +import com.velocitypowered.proxy.protocol.packet.PingIdentify; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.RemovePlayerInfo; import com.velocitypowered.proxy.protocol.packet.ResourcePackRequest; @@ -79,6 +82,11 @@ import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerCommand; import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerChat; import com.velocitypowered.proxy.protocol.packet.chat.session.SessionPlayerCommand; +import com.velocitypowered.proxy.protocol.packet.config.ActiveFeatures; +import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import com.velocitypowered.proxy.protocol.packet.config.RegistrySync; +import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; +import com.velocitypowered.proxy.protocol.packet.config.TagsUpdate; import com.velocitypowered.proxy.protocol.packet.title.LegacyTitlePacket; import com.velocitypowered.proxy.protocol.packet.title.TitleActionbarPacket; import com.velocitypowered.proxy.protocol.packet.title.TitleClearPacket; @@ -98,9 +106,7 @@ import java.util.Objects; import java.util.function.Supplier; import org.checkerframework.checker.nullness.qual.Nullable; -/** - * Registry of all Minecraft protocol states and the packets for each state. - */ +/** Registry of all Minecraft protocol states and the packets for each state. */ public enum StateRegistry { HANDSHAKE { @@ -111,15 +117,46 @@ public enum StateRegistry { }, STATUS { { - serverbound.register(StatusRequest.class, () -> StatusRequest.INSTANCE, - map(0x00, MINECRAFT_1_7_2, false)); - serverbound.register(StatusPing.class, StatusPing::new, - map(0x01, MINECRAFT_1_7_2, false)); + serverbound.register( + StatusRequest.class, () -> StatusRequest.INSTANCE, map(0x00, MINECRAFT_1_7_2, false)); + serverbound.register(StatusPing.class, StatusPing::new, map(0x01, MINECRAFT_1_7_2, false)); - clientbound.register(StatusResponse.class, StatusResponse::new, - map(0x00, MINECRAFT_1_7_2, false)); - clientbound.register(StatusPing.class, StatusPing::new, - map(0x01, MINECRAFT_1_7_2, false)); + clientbound.register( + StatusResponse.class, StatusResponse::new, map(0x00, MINECRAFT_1_7_2, false)); + clientbound.register(StatusPing.class, StatusPing::new, map(0x01, MINECRAFT_1_7_2, false)); + } + }, + CONFIG { + { + serverbound.register( + ClientSettings.class, ClientSettings::new, map(0x00, MINECRAFT_1_20_2, false)); + serverbound.register( + PluginMessage.class, PluginMessage::new, map(0x01, MINECRAFT_1_20_2, false)); + serverbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x02, MINECRAFT_1_20_2, false)); + serverbound.register(KeepAlive.class, KeepAlive::new, map(0x03, MINECRAFT_1_20_2, false)); + serverbound.register( + PingIdentify.class, PingIdentify::new, map(0x04, MINECRAFT_1_20_2, false)); + serverbound.register( + ResourcePackResponse.class, + ResourcePackResponse::new, + map(0x05, MINECRAFT_1_20_2, false)); + + clientbound.register( + PluginMessage.class, PluginMessage::new, map(0x00, MINECRAFT_1_20_2, false)); + clientbound.register(Disconnect.class, Disconnect::new, map(0x01, MINECRAFT_1_20_2, false)); + clientbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x02, MINECRAFT_1_20_2, false)); + clientbound.register(KeepAlive.class, KeepAlive::new, map(0x03, MINECRAFT_1_20_2, false)); + clientbound.register( + PingIdentify.class, PingIdentify::new, map(0x04, MINECRAFT_1_20_2, false)); + clientbound.register( + RegistrySync.class, RegistrySync::new, map(0x05, MINECRAFT_1_20_2, false)); + clientbound.register( + ResourcePackRequest.class, ResourcePackRequest::new, map(0x06, MINECRAFT_1_20_2, false)); + clientbound.register( + ActiveFeatures.class, ActiveFeatures::new, map(0x07, MINECRAFT_1_20_2, false)); + clientbound.register(TagsUpdate.class, TagsUpdate::new, map(0x08, MINECRAFT_1_20_2, false)); } }, PLAY { @@ -137,8 +174,11 @@ public enum StateRegistry { map(0x08, MINECRAFT_1_19, false), map(0x09, MINECRAFT_1_19_1, false), map(0x08, MINECRAFT_1_19_3, false), - map(0x09, MINECRAFT_1_19_4, false)); - serverbound.register(LegacyChat.class, LegacyChat::new, + map(0x09, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); + serverbound.register( + LegacyChat.class, + LegacyChat::new, map(0x01, MINECRAFT_1_7_2, false), map(0x02, MINECRAFT_1_9, false), map(0x03, MINECRAFT_1_12, false), @@ -152,9 +192,13 @@ public enum StateRegistry { map(0x05, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); serverbound.register(SessionPlayerCommand.class, SessionPlayerCommand::new, map(0x04, MINECRAFT_1_19_3, false)); - serverbound.register(SessionPlayerChat.class, SessionPlayerChat::new, - map(0x05, MINECRAFT_1_19_3, false)); - serverbound.register(ClientSettings.class, ClientSettings::new, + serverbound.register( + SessionPlayerChat.class, + SessionPlayerChat::new, + map(0x05, MINECRAFT_1_19_3, MINECRAFT_1_20_2, false)); + serverbound.register( + ClientSettings.class, + ClientSettings::new, map(0x15, MINECRAFT_1_7_2, false), map(0x04, MINECRAFT_1_9, false), map(0x05, MINECRAFT_1_12, false), @@ -163,8 +207,11 @@ public enum StateRegistry { map(0x07, MINECRAFT_1_19, false), map(0x08, MINECRAFT_1_19_1, false), map(0x07, MINECRAFT_1_19_3, false), - map(0x08, MINECRAFT_1_19_4, false)); - serverbound.register(PluginMessage.class, PluginMessage::new, + map(0x08, MINECRAFT_1_19_4, false), + map(0x09, MINECRAFT_1_20_2, false)); + serverbound.register( + PluginMessage.class, + PluginMessage::new, map(0x17, MINECRAFT_1_7_2, false), map(0x09, MINECRAFT_1_9, false), map(0x0A, MINECRAFT_1_12, false), @@ -175,8 +222,11 @@ public enum StateRegistry { map(0x0C, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_1, false), map(0x0C, MINECRAFT_1_19_3, false), - map(0x0D, MINECRAFT_1_19_4, false)); - serverbound.register(KeepAlive.class, KeepAlive::new, + map(0x0D, MINECRAFT_1_19_4, false), + map(0x0F, MINECRAFT_1_20_2, false)); + serverbound.register( + KeepAlive.class, + KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x0B, MINECRAFT_1_9, false), map(0x0C, MINECRAFT_1_12, false), @@ -188,8 +238,11 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_19, false), map(0x12, MINECRAFT_1_19_1, false), map(0x11, MINECRAFT_1_19_3, false), - map(0x12, MINECRAFT_1_19_4, false)); - serverbound.register(ResourcePackResponse.class, ResourcePackResponse::new, + map(0x12, MINECRAFT_1_19_4, false), + map(0x14, MINECRAFT_1_20_2, false)); + serverbound.register( + ResourcePackResponse.class, + ResourcePackResponse::new, map(0x19, MINECRAFT_1_8, false), map(0x16, MINECRAFT_1_9, false), map(0x18, MINECRAFT_1_12, false), @@ -198,16 +251,24 @@ public enum StateRegistry { map(0x20, MINECRAFT_1_16, false), map(0x21, MINECRAFT_1_16_2, false), map(0x23, MINECRAFT_1_19, false), - map(0x24, MINECRAFT_1_19_1, false)); + map(0x24, MINECRAFT_1_19_1, false), + map(0x27, MINECRAFT_1_20_2, false)); + serverbound.register( + FinishedUpdate.class, FinishedUpdate::new, map(0x0B, MINECRAFT_1_20_2, false)); - clientbound.register(BossBar.class, BossBar::new, + clientbound.register( + BossBar.class, + BossBar::new, map(0x0C, MINECRAFT_1_9, false), map(0x0D, MINECRAFT_1_15, false), map(0x0C, MINECRAFT_1_16, false), map(0x0D, MINECRAFT_1_17, false), map(0x0A, MINECRAFT_1_19, false), - map(0x0B, MINECRAFT_1_19_4, false)); - clientbound.register(LegacyChat.class, LegacyChat::new, + map(0x0B, MINECRAFT_1_19_4, false), + map(0x0A, MINECRAFT_1_20_2, false)); + clientbound.register( + LegacyChat.class, + LegacyChat::new, map(0x02, MINECRAFT_1_7_2, true), map(0x0F, MINECRAFT_1_9, true), map(0x0E, MINECRAFT_1_13, true), @@ -224,8 +285,11 @@ public enum StateRegistry { map(0x11, MINECRAFT_1_17, false), map(0x0E, MINECRAFT_1_19, false), map(0x0D, MINECRAFT_1_19_3, false), - map(0x0F, MINECRAFT_1_19_4, false)); - clientbound.register(AvailableCommands.class, AvailableCommands::new, + map(0x0F, MINECRAFT_1_19_4, false), + map(0x10, MINECRAFT_1_20_2, false)); + clientbound.register( + AvailableCommands.class, + AvailableCommands::new, map(0x11, MINECRAFT_1_13, false), map(0x12, MINECRAFT_1_15, false), map(0x11, MINECRAFT_1_16, false), @@ -233,8 +297,11 @@ public enum StateRegistry { map(0x12, MINECRAFT_1_17, false), map(0x0F, MINECRAFT_1_19, false), map(0x0E, MINECRAFT_1_19_3, false), - map(0x10, MINECRAFT_1_19_4, false)); - clientbound.register(PluginMessage.class, PluginMessage::new, + map(0x10, MINECRAFT_1_19_4, false), + map(0x11, MINECRAFT_1_20_2, false)); + clientbound.register( + PluginMessage.class, + PluginMessage::new, map(0x3F, MINECRAFT_1_7_2, false), map(0x18, MINECRAFT_1_9, false), map(0x19, MINECRAFT_1_13, false), @@ -246,8 +313,11 @@ public enum StateRegistry { map(0x15, MINECRAFT_1_19, false), map(0x16, MINECRAFT_1_19_1, false), map(0x15, MINECRAFT_1_19_3, false), - map(0x17, MINECRAFT_1_19_4, false)); - clientbound.register(Disconnect.class, Disconnect::new, + map(0x17, MINECRAFT_1_19_4, false), + map(0x18, MINECRAFT_1_20_2, false)); + clientbound.register( + Disconnect.class, + Disconnect::new, map(0x40, MINECRAFT_1_7_2, false), map(0x1A, MINECRAFT_1_9, false), map(0x1B, MINECRAFT_1_13, false), @@ -259,8 +329,11 @@ public enum StateRegistry { map(0x17, MINECRAFT_1_19, false), map(0x19, MINECRAFT_1_19_1, false), map(0x17, MINECRAFT_1_19_3, false), - map(0x1A, MINECRAFT_1_19_4, false)); - clientbound.register(KeepAlive.class, KeepAlive::new, + map(0x1A, MINECRAFT_1_19_4, false), + map(0x1B, MINECRAFT_1_20_2, false)); + clientbound.register( + KeepAlive.class, + KeepAlive::new, map(0x00, MINECRAFT_1_7_2, false), map(0x1F, MINECRAFT_1_9, false), map(0x21, MINECRAFT_1_13, false), @@ -272,8 +345,11 @@ public enum StateRegistry { map(0x1E, MINECRAFT_1_19, false), map(0x20, MINECRAFT_1_19_1, false), map(0x1F, MINECRAFT_1_19_3, false), - map(0x23, MINECRAFT_1_19_4, false)); - clientbound.register(JoinGame.class, JoinGame::new, + map(0x23, MINECRAFT_1_19_4, false), + map(0x24, MINECRAFT_1_20_2, false)); + clientbound.register( + JoinGame.class, + JoinGame::new, map(0x01, MINECRAFT_1_7_2, false), map(0x23, MINECRAFT_1_9, false), map(0x25, MINECRAFT_1_13, false), @@ -285,8 +361,11 @@ public enum StateRegistry { map(0x23, MINECRAFT_1_19, false), map(0x25, MINECRAFT_1_19_1, false), map(0x24, MINECRAFT_1_19_3, false), - map(0x28, MINECRAFT_1_19_4, false)); - clientbound.register(Respawn.class, Respawn::new, + map(0x28, MINECRAFT_1_19_4, false), + map(0x29, MINECRAFT_1_20_2, false)); + clientbound.register( + Respawn.class, + Respawn::new, map(0x07, MINECRAFT_1_7_2, true), map(0x33, MINECRAFT_1_9, true), map(0x34, MINECRAFT_1_12, true), @@ -300,8 +379,11 @@ public enum StateRegistry { map(0x3B, MINECRAFT_1_19, true), map(0x3E, MINECRAFT_1_19_1, true), map(0x3D, MINECRAFT_1_19_3, true), - map(0x41, MINECRAFT_1_19_4, true)); - clientbound.register(ResourcePackRequest.class, ResourcePackRequest::new, + map(0x41, MINECRAFT_1_19_4, true), + map(0x43, MINECRAFT_1_20_2, true)); + clientbound.register( + ResourcePackRequest.class, + ResourcePackRequest::new, map(0x48, MINECRAFT_1_8, false), map(0x32, MINECRAFT_1_9, false), map(0x33, MINECRAFT_1_12, false), @@ -315,8 +397,11 @@ public enum StateRegistry { map(0x3A, MINECRAFT_1_19, false), map(0x3D, MINECRAFT_1_19_1, false), map(0x3C, MINECRAFT_1_19_3, false), - map(0x40, MINECRAFT_1_19_4, false)); - clientbound.register(HeaderAndFooter.class, HeaderAndFooter::new, + map(0x40, MINECRAFT_1_19_4, false), + map(0x42, MINECRAFT_1_20_2, false)); + clientbound.register( + HeaderAndFooter.class, + HeaderAndFooter::new, map(0x47, MINECRAFT_1_8, true), map(0x48, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_9_4, true), @@ -331,8 +416,11 @@ public enum StateRegistry { map(0x60, MINECRAFT_1_19, true), map(0x63, MINECRAFT_1_19_1, true), map(0x61, MINECRAFT_1_19_3, true), - map(0x65, MINECRAFT_1_19_4, true)); - clientbound.register(LegacyTitlePacket.class, LegacyTitlePacket::new, + map(0x65, MINECRAFT_1_19_4, true), + map(0x68, MINECRAFT_1_20_2, true)); + clientbound.register( + LegacyTitlePacket.class, + LegacyTitlePacket::new, map(0x45, MINECRAFT_1_8, true), map(0x45, MINECRAFT_1_9, true), map(0x47, MINECRAFT_1_12, true), @@ -346,31 +434,46 @@ public enum StateRegistry { map(0x58, MINECRAFT_1_18, true), map(0x5B, MINECRAFT_1_19_1, true), map(0x59, MINECRAFT_1_19_3, true), - map(0x5D, MINECRAFT_1_19_4, true)); - clientbound.register(TitleTextPacket.class, TitleTextPacket::new, + map(0x5D, MINECRAFT_1_19_4, true), + map(0x5F, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleTextPacket.class, + TitleTextPacket::new, map(0x59, MINECRAFT_1_17, true), map(0x5A, MINECRAFT_1_18, true), map(0x5D, MINECRAFT_1_19_1, true), map(0x5B, MINECRAFT_1_19_3, true), - map(0x5F, MINECRAFT_1_19_4, true)); - clientbound.register(TitleActionbarPacket.class, TitleActionbarPacket::new, + map(0x5F, MINECRAFT_1_19_4, true), + map(0x61, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleActionbarPacket.class, + TitleActionbarPacket::new, map(0x41, MINECRAFT_1_17, true), map(0x40, MINECRAFT_1_19, true), map(0x43, MINECRAFT_1_19_1, true), map(0x42, MINECRAFT_1_19_3, true), - map(0x46, MINECRAFT_1_19_4, true)); - clientbound.register(TitleTimesPacket.class, TitleTimesPacket::new, + map(0x46, MINECRAFT_1_19_4, true), + map(0x48, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleTimesPacket.class, + TitleTimesPacket::new, map(0x5A, MINECRAFT_1_17, true), map(0x5B, MINECRAFT_1_18, true), map(0x5E, MINECRAFT_1_19_1, true), map(0x5C, MINECRAFT_1_19_3, true), - map(0x60, MINECRAFT_1_19_4, true)); - clientbound.register(TitleClearPacket.class, TitleClearPacket::new, + map(0x60, MINECRAFT_1_19_4, true), + map(0x62, MINECRAFT_1_20_2, true)); + clientbound.register( + TitleClearPacket.class, + TitleClearPacket::new, map(0x10, MINECRAFT_1_17, true), map(0x0D, MINECRAFT_1_19, true), map(0x0C, MINECRAFT_1_19_3, true), - map(0x0E, MINECRAFT_1_19_4, true)); - clientbound.register(LegacyPlayerListItem.class, LegacyPlayerListItem::new, + map(0x0E, MINECRAFT_1_19_4, true), + map(0x0F, MINECRAFT_1_20_2, true)); + clientbound.register( + LegacyPlayerListItem.class, + LegacyPlayerListItem::new, map(0x38, MINECRAFT_1_7_2, false), map(0x2D, MINECRAFT_1_9, false), map(0x2E, MINECRAFT_1_12_1, false), @@ -384,44 +487,59 @@ public enum StateRegistry { map(0x37, MINECRAFT_1_19_1, MINECRAFT_1_19_1, false)); clientbound.register(RemovePlayerInfo.class, RemovePlayerInfo::new, map(0x35, MINECRAFT_1_19_3, false), - map(0x39, MINECRAFT_1_19_4, false)); - clientbound.register(UpsertPlayerInfo.class, UpsertPlayerInfo::new, + map(0x39, MINECRAFT_1_19_4, false), + map(0x3B, MINECRAFT_1_20_2, false)); + clientbound.register( + UpsertPlayerInfo.class, + UpsertPlayerInfo::new, map(0x36, MINECRAFT_1_19_3, false), - map(0x3A, MINECRAFT_1_19_4, false)); - clientbound.register(SystemChat.class, SystemChat::new, + map(0x3A, MINECRAFT_1_19_4, false), + map(0x3C, MINECRAFT_1_20_2, false)); + clientbound.register( + SystemChat.class, + SystemChat::new, map(0x5F, MINECRAFT_1_19, true), map(0x62, MINECRAFT_1_19_1, true), map(0x60, MINECRAFT_1_19_3, true), - map(0x64, MINECRAFT_1_19_4, true)); - clientbound.register(PlayerChatCompletion.class, PlayerChatCompletion::new, + map(0x64, MINECRAFT_1_19_4, true), + map(0x67, MINECRAFT_1_20_2, true)); + clientbound.register( + PlayerChatCompletion.class, + PlayerChatCompletion::new, map(0x15, MINECRAFT_1_19_1, true), map(0x14, MINECRAFT_1_19_3, true), - map(0x16, MINECRAFT_1_19_4, true)); - clientbound.register(ServerData.class, ServerData::new, + map(0x16, MINECRAFT_1_19_4, true), + map(0x17, MINECRAFT_1_20_2, true)); + clientbound.register( + ServerData.class, + ServerData::new, map(0x3F, MINECRAFT_1_19, false), map(0x42, MINECRAFT_1_19_1, false), map(0x41, MINECRAFT_1_19_3, false), - map(0x45, MINECRAFT_1_19_4, false)); + map(0x45, MINECRAFT_1_19_4, false), + map(0x47, MINECRAFT_1_20_2, false)); + clientbound.register(StartUpdate.class, StartUpdate::new, map(0x65, MINECRAFT_1_20_2, false)); } }, LOGIN { { - serverbound.register(ServerLogin.class, ServerLogin::new, - map(0x00, MINECRAFT_1_7_2, false)); - serverbound.register(EncryptionResponse.class, EncryptionResponse::new, - map(0x01, MINECRAFT_1_7_2, false)); - serverbound.register(LoginPluginResponse.class, LoginPluginResponse::new, - map(0x02, MINECRAFT_1_13, false)); - clientbound.register(Disconnect.class, Disconnect::new, - map(0x00, MINECRAFT_1_7_2, false)); - clientbound.register(EncryptionRequest.class, EncryptionRequest::new, - map(0x01, MINECRAFT_1_7_2, false)); - clientbound.register(ServerLoginSuccess.class, ServerLoginSuccess::new, - map(0x02, MINECRAFT_1_7_2, false)); - clientbound.register(SetCompression.class, SetCompression::new, - map(0x03, MINECRAFT_1_8, false)); - clientbound.register(LoginPluginMessage.class, LoginPluginMessage::new, - map(0x04, MINECRAFT_1_13, false)); + serverbound.register(ServerLogin.class, ServerLogin::new, map(0x00, MINECRAFT_1_7_2, false)); + serverbound.register( + EncryptionResponse.class, EncryptionResponse::new, map(0x01, MINECRAFT_1_7_2, false)); + serverbound.register( + LoginPluginResponse.class, LoginPluginResponse::new, map(0x02, MINECRAFT_1_13, false)); + serverbound.register( + LoginAcknowledged.class, LoginAcknowledged::new, map(0x03, MINECRAFT_1_20_2, false)); + + clientbound.register(Disconnect.class, Disconnect::new, map(0x00, MINECRAFT_1_7_2, false)); + clientbound.register( + EncryptionRequest.class, EncryptionRequest::new, map(0x01, MINECRAFT_1_7_2, false)); + clientbound.register( + ServerLoginSuccess.class, ServerLoginSuccess::new, map(0x02, MINECRAFT_1_7_2, false)); + clientbound.register( + SetCompression.class, SetCompression::new, map(0x03, MINECRAFT_1_8, false)); + clientbound.register( + LoginPluginMessage.class, LoginPluginMessage::new, map(0x04, MINECRAFT_1_13, false)); } }; @@ -435,9 +553,7 @@ public enum StateRegistry { return (direction == SERVERBOUND ? serverbound : clientbound).getProtocolRegistry(version); } - /** - * Packet registry. - */ + /** Packet registry. */ public static class PacketRegistry { private final Direction direction; @@ -505,19 +621,24 @@ public enum StateRegistry { } ProtocolRegistry registry = this.versions.get(protocol); if (registry == null) { - throw new IllegalArgumentException("Unknown protocol version " - + current.protocolVersion); + throw new IllegalArgumentException( + "Unknown protocol version " + current.protocolVersion); } if (registry.packetIdToSupplier.containsKey(current.id)) { - throw new IllegalArgumentException("Can not register class " + clazz.getSimpleName() - + " with id " + current.id + " for " + registry.version - + " because another packet is already registered"); + throw new IllegalArgumentException( + "Can not register class " + + clazz.getSimpleName() + + " with id " + + current.id + + " for " + + registry.version + + " because another packet is already registered"); } if (registry.packetClassToId.containsKey(clazz)) { - throw new IllegalArgumentException(clazz.getSimpleName() - + " is already registered for version " + registry.version); + throw new IllegalArgumentException( + clazz.getSimpleName() + " is already registered for version " + registry.version); } if (!current.encodeOnly) { @@ -528,9 +649,7 @@ public enum StateRegistry { } } - /** - * Protocol registry. - */ + /** Protocol registry. */ public class ProtocolRegistry { public final ProtocolVersion version; @@ -575,12 +694,20 @@ public enum StateRegistry { } return id; } + + /** + * Checks if the registry contains a packet with the specified {@code id}. + * + * @param packet the packet to check + * @return {@code true} if the packet is registered, {@code false} otherwise + */ + public boolean containsPacket(final MinecraftPacket packet) { + return this.packetClassToId.containsKey(packet.getClass()); + } } } - /** - * Packet mapping. - */ + /** Packet mapping. */ public static final class PacketMapping { private final int id; @@ -599,9 +726,12 @@ public enum StateRegistry { @Override public String toString() { return "PacketMapping{" - + "id=" + id - + ", protocolVersion=" + protocolVersion - + ", encodeOnly=" + encodeOnly + + "id=" + + id + + ", protocolVersion=" + + protocolVersion + + ", encodeOnly=" + + encodeOnly + '}'; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java index 313b7858d..7133f1d26 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/MinecraftEncoder.java @@ -62,4 +62,8 @@ public class MinecraftEncoder extends MessageToByteEncoder { this.state = state; this.setProtocolVersion(registry.version); } + + public ProtocolUtils.Direction getDirection() { + return direction; + } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java new file mode 100644 index 000000000..612de451e --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/netty/PlayPacketQueueHandler.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.netty; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.PlatformDependent; +import java.util.Queue; +import org.jetbrains.annotations.NotNull; + +/** + * Queues up any pending PLAY packets while the client is in the CONFIG state. + * + *

Much of the Velocity API (i.e. chat messages) utilize PLAY packets, however the client is + * incapable of receiving these packets during the CONFIG state. Certain events such as the + * ServerPreConnectEvent may be called during this time, and we need to ensure that any API that + * uses these packets will work as expected. + * + *

This handler will queue up any packets that are sent to the client during this time, and send + * them once the client has (re)entered the PLAY state. + */ +public class PlayPacketQueueHandler extends ChannelDuplexHandler { + + private final StateRegistry.PacketRegistry.ProtocolRegistry registry; + private final Queue queue = PlatformDependent.newMpscQueue(); + + /** + * Provides registries for client & server bound packets. + * + * @param version the protocol version + */ + public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) { + this.registry = + StateRegistry.CONFIG.getProtocolRegistry(direction, version); + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + if (!(msg instanceof MinecraftPacket)) { + ctx.write(msg, promise); + return; + } + + // If the packet exists in the CONFIG state, we want to always + // ensure that it gets sent out to the client + if (this.registry.containsPacket(((MinecraftPacket) msg))) { + ctx.write(msg, promise); + return; + } + + // Otherwise, queue the packet + this.queue.offer((MinecraftPacket) msg); + } + + @Override + public void channelInactive(@NotNull ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, false); + + super.channelInactive(ctx); + } + + @Override + public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { + this.releaseQueue(ctx, ctx.channel().isActive()); + } + + private void releaseQueue(ChannelHandlerContext ctx, boolean active) { + if (this.queue.isEmpty()) { + return; + } + + // Send out all the queued packets + MinecraftPacket packet; + while ((packet = this.queue.poll()) != null) { + if (active) { + ctx.write(packet, ctx.voidPromise()); + } else { + ReferenceCountUtil.release(packet); + } + } + + if (active) { + ctx.flush(); + } + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java index 5c267d195..a42d1ee90 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientSettings.java @@ -23,10 +23,10 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import java.util.Objects; + import org.checkerframework.checker.nullness.qual.Nullable; public class ClientSettings implements MinecraftPacket { - private @Nullable String locale; private byte viewDistance; private int chatVisibility; @@ -120,16 +120,10 @@ public class ClientSettings implements MinecraftPacket { @Override public String toString() { - return "ClientSettings{" - + "locale='" + locale + '\'' - + ", viewDistance=" + viewDistance - + ", chatVisibility=" + chatVisibility - + ", chatColors=" + chatColors - + ", skinParts=" + skinParts - + ", mainHand=" + mainHand - + ", chatFilteringEnabled=" + chatFilteringEnabled - + ", clientListingAllowed=" + clientListingAllowed - + '}'; + return "ClientSettings{" + "locale='" + locale + '\'' + ", viewDistance=" + viewDistance + + ", chatVisibility=" + chatVisibility + ", chatColors=" + chatColors + ", skinParts=" + + skinParts + ", mainHand=" + mainHand + ", chatFilteringEnabled=" + chatFilteringEnabled + + ", clientListingAllowed=" + clientListingAllowed + '}'; } @Override diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java index cfbecce31..938d1190a 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/JoinGame.java @@ -42,6 +42,7 @@ public class JoinGame implements MinecraftPacket { private int viewDistance; // 1.14+ private boolean reducedDebugInfo; private boolean showRespawnScreen; + private boolean doLimitedCrafting; // 1.20.2+ private ImmutableSet levelNames; // 1.16+ private CompoundBinaryTag registry; // 1.16+ private DimensionInfo dimensionInfo; // 1.16+ @@ -143,6 +144,14 @@ public class JoinGame implements MinecraftPacket { this.isHardcore = isHardcore; } + public boolean getDoLimitedCrafting() { + return doLimitedCrafting; + } + + public void setDoLimitedCrafting(boolean doLimitedCrafting) { + this.doLimitedCrafting = doLimitedCrafting; + } + public CompoundBinaryTag getCurrentDimensionData() { return currentDimensionData; } @@ -177,32 +186,24 @@ public class JoinGame implements MinecraftPacket { @Override public String toString() { - return "JoinGame{" - + "entityId=" + entityId - + ", gamemode=" + gamemode - + ", dimension=" + dimension - + ", partialHashedSeed=" + partialHashedSeed - + ", difficulty=" + difficulty - + ", isHardcore=" + isHardcore - + ", maxPlayers=" + maxPlayers - + ", levelType='" + levelType + '\'' - + ", viewDistance=" + viewDistance - + ", reducedDebugInfo=" + reducedDebugInfo - + ", showRespawnScreen=" + showRespawnScreen - + ", levelNames=" + levelNames - + ", registry='" + registry + '\'' - + ", dimensionInfo='" + dimensionInfo + '\'' - + ", currentDimensionData='" + currentDimensionData + '\'' - + ", previousGamemode=" + previousGamemode - + ", simulationDistance=" + simulationDistance - + ", lastDeathPosition='" + lastDeathPosition + '\'' - + ", portalCooldown=" + portalCooldown - + '}'; + return "JoinGame{" + "entityId=" + entityId + ", gamemode=" + gamemode + ", dimension=" + + dimension + ", partialHashedSeed=" + partialHashedSeed + ", difficulty=" + difficulty + + ", isHardcore=" + isHardcore + ", maxPlayers=" + maxPlayers + ", levelType='" + levelType + + '\'' + ", viewDistance=" + viewDistance + ", reducedDebugInfo=" + reducedDebugInfo + + ", showRespawnScreen=" + showRespawnScreen + ", doLimitedCrafting=" + doLimitedCrafting + + ", levelNames=" + levelNames + ", registry='" + registry + '\'' + ", dimensionInfo='" + + dimensionInfo + '\'' + ", currentDimensionData='" + currentDimensionData + '\'' + + ", previousGamemode=" + previousGamemode + ", simulationDistance=" + simulationDistance + + ", lastDeathPosition='" + lastDeathPosition + '\'' + ", portalCooldown=" + portalCooldown + + '}'; } @Override public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // haha funny, they made 1.20.2 more complicated + this.decode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, // so separate it out. this.decode116Up(buf, version); @@ -295,9 +296,46 @@ public class JoinGame implements MinecraftPacket { } } + private void decode1202Up(ByteBuf buf, ProtocolVersion version) { + this.entityId = buf.readInt(); + this.isHardcore = buf.readBoolean(); + + this.levelNames = ImmutableSet.copyOf(ProtocolUtils.readStringArray(buf)); + + this.maxPlayers = ProtocolUtils.readVarInt(buf); + + this.viewDistance = ProtocolUtils.readVarInt(buf); + this.simulationDistance = ProtocolUtils.readVarInt(buf); + + this.reducedDebugInfo = buf.readBoolean(); + this.showRespawnScreen = buf.readBoolean(); + this.doLimitedCrafting = buf.readBoolean(); + + String dimensionIdentifier = ProtocolUtils.readString(buf); + String levelName = ProtocolUtils.readString(buf); + this.partialHashedSeed = buf.readLong(); + + this.gamemode = buf.readByte(); + this.previousGamemode = buf.readByte(); + + boolean isDebug = buf.readBoolean(); + boolean isFlat = buf.readBoolean(); + this.dimensionInfo = new DimensionInfo(dimensionIdentifier, levelName, isFlat, isDebug); + + // optional death location + if (buf.readBoolean()) { + this.lastDeathPosition = Pair.of(ProtocolUtils.readString(buf), buf.readLong()); + } + + this.portalCooldown = ProtocolUtils.readVarInt(buf); + } + @Override public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + // haha funny, they made 1.20.2 more complicated + this.encode1202Up(buf, version); + } else if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) >= 0) { // Minecraft 1.16 and above have significantly more complicated logic for reading this packet, // so separate it out. this.encode116Up(buf, version); @@ -396,6 +434,43 @@ public class JoinGame implements MinecraftPacket { } } + private void encode1202Up(ByteBuf buf, ProtocolVersion version) { + buf.writeInt(entityId); + buf.writeBoolean(isHardcore); + + ProtocolUtils.writeStringArray(buf, levelNames.toArray(String[]::new)); + + ProtocolUtils.writeVarInt(buf, maxPlayers); + + ProtocolUtils.writeVarInt(buf, viewDistance); + ProtocolUtils.writeVarInt(buf, simulationDistance); + + buf.writeBoolean(reducedDebugInfo); + buf.writeBoolean(showRespawnScreen); + buf.writeBoolean(doLimitedCrafting); + + ProtocolUtils.writeString(buf, dimensionInfo.getRegistryIdentifier()); + ProtocolUtils.writeString(buf, dimensionInfo.getLevelName()); + buf.writeLong(partialHashedSeed); + + buf.writeByte(gamemode); + buf.writeByte(previousGamemode); + + buf.writeBoolean(dimensionInfo.isDebugType()); + buf.writeBoolean(dimensionInfo.isFlat()); + + // optional death location + if (lastDeathPosition != null) { + buf.writeBoolean(true); + ProtocolUtils.writeString(buf, lastDeathPosition.key()); + buf.writeLong(lastDeathPosition.value()); + } else { + buf.writeBoolean(false); + } + + ProtocolUtils.writeVarInt(buf, portalCooldown); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java new file mode 100644 index 000000000..1f7941004 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/LoginAcknowledged.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class LoginAcknowledged implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java new file mode 100644 index 000000000..2d3d9b5da --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/PingIdentify.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018-2021 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class PingIdentify implements MinecraftPacket { + + private int id; + + @Override + public String toString() { + return "Ping{" + "id=" + id + '}'; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + id = buf.readInt(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion version) { + buf.writeInt(id); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java index 641407f9f..c80b97c16 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackRequest.java @@ -17,17 +17,23 @@ package com.velocitypowered.proxy.protocol.packet; +import com.google.common.base.Preconditions; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.player.ResourcePackInfo; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.Nullable; +import java.util.regex.Pattern; + public class ResourcePackRequest implements MinecraftPacket { private @MonotonicNonNull String url; @@ -35,6 +41,8 @@ public class ResourcePackRequest implements MinecraftPacket { private boolean isRequired; // 1.17+ private @Nullable Component prompt; // 1.17+ + private static final Pattern PLAUSIBLE_SHA1_HASH = Pattern.compile("^[a-z0-9]{40}$"); // 1.20.2+ + public @Nullable String getUrl() { return url; } @@ -99,6 +107,19 @@ public class ResourcePackRequest implements MinecraftPacket { } } + public VelocityResourcePackInfo toServerPromptedPack() { + ResourcePackInfo.Builder builder = + new VelocityResourcePackInfo.BuilderImpl(Preconditions.checkNotNull(url)).setPrompt(prompt) + .setShouldForce(isRequired).setOrigin(ResourcePackInfo.Origin.DOWNSTREAM_SERVER); + + if (hash != null && !hash.isEmpty()) { + if (PLAUSIBLE_SHA1_HASH.matcher(hash).matches()) { + builder.setHash(ByteBufUtil.decodeHexDump(hash)); + } + } + return (VelocityResourcePackInfo) builder.build(); + } + @Override public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); @@ -106,11 +127,7 @@ public class ResourcePackRequest implements MinecraftPacket { @Override public String toString() { - return "ResourcePackRequest{" - + "url='" + url + '\'' - + ", hash='" + hash + '\'' - + ", isRequired=" + isRequired - + ", prompt='" + prompt + '\'' - + '}'; + return "ResourcePackRequest{" + "url='" + url + '\'' + ", hash='" + hash + '\'' + + ", isRequired=" + isRequired + ", prompt='" + prompt + '\'' + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java index 3b7c93608..4b67cf695 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ResourcePackResponse.java @@ -73,9 +73,6 @@ public class ResourcePackResponse implements MinecraftPacket { @Override public String toString() { - return "ResourcePackResponse{" - + "hash=" + hash + ", " - + "status=" + status - + '}'; + return "ResourcePackResponse{" + "hash=" + hash + ", " + "status=" + status + '}'; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java index 0cd589ee9..57c8c61c2 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ServerLogin.java @@ -74,10 +74,7 @@ public class ServerLogin implements MinecraftPacket { @Override public String toString() { - return "ServerLogin{" - + "username='" + username + '\'' - + "playerKey='" + playerKey + '\'' - + '}'; + return "ServerLogin{" + "username='" + username + '\'' + "playerKey='" + playerKey + '\'' + '}'; } @Override @@ -98,6 +95,11 @@ public class ServerLogin implements MinecraftPacket { } } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + this.holderUuid = ProtocolUtils.readUuid(buf); + return; + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (buf.readBoolean()) { holderUuid = ProtocolUtils.readUuid(buf); @@ -125,6 +127,11 @@ public class ServerLogin implements MinecraftPacket { } } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + ProtocolUtils.writeUuid(buf, this.holderUuid); + return; + } + if (version.compareTo(ProtocolVersion.MINECRAFT_1_19_1) >= 0) { if (playerKey != null && playerKey.getSignatureHolder() != null) { buf.writeBoolean(true); diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java new file mode 100644 index 000000000..de2a5047c --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/ActiveFeatures.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +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.key.Key; + +public class ActiveFeatures implements MinecraftPacket { + + private Key[] activeFeatures; + + public ActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } + + public ActiveFeatures() { + this.activeFeatures = new Key[0]; + } + + public void setActiveFeatures(Key[] activeFeatures) { + this.activeFeatures = activeFeatures; + } + + public Key[] getActiveFeatures() { + return activeFeatures; + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + activeFeatures = ProtocolUtils.readKeyArray(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ProtocolUtils.writeKeyArray(buf, activeFeatures); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java new file mode 100644 index 000000000..866819b48 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/FinishedUpdate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class FinishedUpdate implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java new file mode 100644 index 000000000..74c275401 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/RegistrySync.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.util.DeferredByteBufHolder; +import io.netty.buffer.ByteBuf; + +public class RegistrySync extends DeferredByteBufHolder implements MinecraftPacket { + + public RegistrySync() { + super(null); + } + + // NBT change in 1.20.2 makes it difficult to parse this packet. + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + this.replace(buf.readRetainedSlice(buf.readableBytes())); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + buf.writeBytes(content()); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java new file mode 100644 index 000000000..d1c25fa27 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/StartUpdate.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public class StartUpdate implements MinecraftPacket { + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + } + + @Override + public int expectedMaxLength(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion version) { + return 0; + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java new file mode 100644 index 000000000..79b432158 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/config/TagsUpdate.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2018-2023 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet.config; + +import com.google.common.collect.ImmutableMap; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +import java.util.Map; + +public class TagsUpdate implements MinecraftPacket { + + private Map> tags; + + public TagsUpdate(Map> tags) { + this.tags = tags; + } + + public TagsUpdate() { + this.tags = Map.of(); + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + int size = ProtocolUtils.readVarInt(buf); + for (int i = 0; i < size; i++) { + String key = ProtocolUtils.readString(buf); + + int innerSize = ProtocolUtils.readVarInt(buf); + ImmutableMap.Builder innerBuilder = ImmutableMap.builder(); + for (int j = 0; j < innerSize; j++) { + String innerKey = ProtocolUtils.readString(buf); + int[] innerValue = ProtocolUtils.readVarIntArray(buf); + innerBuilder.put(innerKey, innerValue); + } + + builder.put(key, innerBuilder.build()); + } + tags = builder.build(); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, + ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, tags.size()); + for (Map.Entry> entry : tags.entrySet()) { + ProtocolUtils.writeString(buf, entry.getKey()); + // Oh, joy + ProtocolUtils.writeVarInt(buf, entry.getValue().size()); + for (Map.Entry innerEntry : entry.getValue().entrySet()) { + // Yea, object oriented programming be damned + ProtocolUtils.writeString(buf, innerEntry.getKey()); + ProtocolUtils.writeVarIntArray(buf, innerEntry.getValue()); + } + } + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } +} 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 7bafc99b7..4ed43e86b 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/PingSessionHandler.java @@ -32,8 +32,8 @@ import java.io.IOException; import java.util.concurrent.CompletableFuture; /** - * Session handler used to implement - * {@link VelocityRegisteredServer#ping(EventLoop, ProtocolVersion)}. + * Session handler used to implement {@link VelocityRegisteredServer#ping(EventLoop, + * ProtocolVersion)}. */ public class PingSessionHandler implements MinecraftSessionHandler { @@ -60,6 +60,7 @@ public class PingSessionHandler implements MinecraftSessionHandler { handshake.setProtocolVersion(version); connection.delayedWrite(handshake); + connection.setActiveSessionHandler(StateRegistry.STATUS); connection.setState(StateRegistry.STATUS); connection.delayedWrite(StatusRequest.INSTANCE); 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 57780110b..928b4ee72 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/server/VelocityRegisteredServer.java @@ -37,6 +37,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.backend.VelocityServerConnection; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.ProtocolUtils; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder; import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder; import com.velocitypowered.proxy.protocol.netty.MinecraftVarintFrameDecoder; @@ -94,8 +95,8 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud } /** - * Pings the specified server using the specified event {@code loop}, claiming to be - * {@code version}. + * Pings the specified server using the specified event {@code loop}, claiming to be {@code + * version}. * * @param loop the event loop to use * @param pingOptions the options to apply to this ping @@ -106,35 +107,30 @@ public class VelocityRegisteredServer implements RegisteredServer, ForwardingAud throw new IllegalStateException("No Velocity proxy instance available"); } CompletableFuture pingFuture = new CompletableFuture<>(); - server.createBootstrap(loop) - .handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline() - .addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) - .addLast(READ_TIMEOUT, - new ReadTimeoutHandler(pingOptions.getTimeout() == 0 - ? server.getConfiguration().getReadTimeout() : pingOptions.getTimeout(), - TimeUnit.MILLISECONDS)) - .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) - .addLast(MINECRAFT_DECODER, - new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) - .addLast(MINECRAFT_ENCODER, - new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); + server.createBootstrap(loop).handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ch.pipeline().addLast(FRAME_DECODER, new MinecraftVarintFrameDecoder()) + .addLast(READ_TIMEOUT, new ReadTimeoutHandler( + pingOptions.getTimeout() == 0 + ? server.getConfiguration().getReadTimeout() + : pingOptions.getTimeout(), TimeUnit.MILLISECONDS)) + .addLast(FRAME_ENCODER, MinecraftVarintLengthEncoder.INSTANCE) + .addLast(MINECRAFT_DECODER, new MinecraftDecoder(ProtocolUtils.Direction.CLIENTBOUND)) + .addLast(MINECRAFT_ENCODER, new MinecraftEncoder(ProtocolUtils.Direction.SERVERBOUND)); - ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); - } - }) - .connect(serverInfo.getAddress()) - .addListener((ChannelFutureListener) future -> { - if (future.isSuccess()) { - MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); - conn.setSessionHandler(new PingSessionHandler( - pingFuture, VelocityRegisteredServer.this, conn, pingOptions.getProtocolVersion())); - } else { - pingFuture.completeExceptionally(future.cause()); - } - }); + ch.pipeline().addLast(HANDLER, new MinecraftConnection(ch, server)); + } + }).connect(serverInfo.getAddress()).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + MinecraftConnection conn = future.channel().pipeline().get(MinecraftConnection.class); + conn.setActiveSessionHandler(StateRegistry.HANDSHAKE, + new PingSessionHandler(pingFuture, VelocityRegisteredServer.this, conn, + pingOptions.getProtocolVersion())); + } else { + pingFuture.completeExceptionally(future.cause()); + } + }); return pingFuture; }