From 06ae0450c0ec8b241080405fe633d299b61074ee Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 09:47:50 +0300 Subject: [PATCH 01/17] Properly check for pending disconnection --- .../net/elytrium/limboapi/server/LimboSessionHandlerImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 1d58da89..de493fc1 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -330,7 +330,8 @@ public void disconnected() { } public void switchDisconnection(Runnable runnable) { - if (this.disconnecting ^= true) { + if (!this.disconnecting) { + this.disconnecting = true; if (this.player.getConnection().getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { runnable.run(); } else { From adc1eca071b30fd1d48f9502ab56c3d104f6e63c Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:05:33 +0300 Subject: [PATCH 02/17] Force synchronize rejoin state switching --- .../injection/login/LoginTasksQueue.java | 18 +++---- .../TransitionConfirmHandler.java | 7 +++ .../elytrium/limboapi/server/LimboImpl.java | 51 +++++++++---------- .../server/LimboSessionHandlerImpl.java | 3 +- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index d9f5f42c..72892603 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -262,19 +262,15 @@ private void connectToServer(Logger logger, ConnectedPlayer player, MinecraftCon } catch (Throwable e) { throw new ReflectionException(e); } - } else { - if (connection.getState() == StateRegistry.PLAY) { - connection.write(new StartUpdate()); - - TransitionConfirmHandler confirm = new TransitionConfirmHandler(connection); - confirm.setPlayer(player); + } else if (connection.getState() == StateRegistry.PLAY) { + connection.write(new StartUpdate()); - connection.setActiveSessionHandler(StateRegistry.PLAY, confirm); - confirm.waitForConfirmation(() -> this.connectToServer(logger, player, connection)); - - return; - } + // Synchronize to make sure that nothing from PLAY state isn't received in CONFIG state + new TransitionConfirmHandler(connection) + .trackTransition(player, () -> this.connectToServer(logger, player, connection)); + return; // Re-running this method due to synchronization with the client + } else { connection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(this.server, this.player)); } diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java index 96931145..273f657a 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java @@ -18,6 +18,7 @@ package net.elytrium.limboapi.injection.login.confirmation; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; @@ -37,4 +38,10 @@ public boolean handle(FinishedUpdate packet) { return false; } + + public void trackTransition(ConnectedPlayer player, Runnable runnable) { + this.setPlayer(player); + this.connection.setActiveSessionHandler(StateRegistry.PLAY, this); + this.waitForConfirmation(runnable); + } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 6864e679..aa746e58 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -92,6 +92,7 @@ import net.elytrium.limboapi.api.protocol.PreparedPacket; import net.elytrium.limboapi.api.protocol.packets.PacketMapping; import net.elytrium.limboapi.injection.login.confirmation.ConfirmHandler; +import net.elytrium.limboapi.injection.login.confirmation.TransitionConfirmHandler; import net.elytrium.limboapi.injection.packet.MinecraftLimitedCompressDecoder; import net.elytrium.limboapi.material.Biome; import net.elytrium.limboapi.protocol.LimboProtocol; @@ -322,33 +323,32 @@ public void spawnPlayer(Player apiPlayer, LimboSessionHandler handler) { private void spawnPlayerLocal(Class handlerClass, LimboSessionHandlerImpl sessionHandler, ConnectedPlayer player, MinecraftConnection connection) { - boolean callSpawn = connection.getState() != StateRegistry.CONFIG && !this.shouldRejoin; - if (callSpawn || connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { - this.preSpawn(handlerClass, connection, player); - } - - connection.setActiveSessionHandler(connection.getState(), sessionHandler); - - if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + boolean stateSwitching = connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0; + if (stateSwitching) { if (connection.getState() != StateRegistry.CONFIG) { if (this.shouldRejoin) { - connection.delayedWrite(this.configTransitionPackets); - connection.setState(StateRegistry.CONFIG); - // There is desync in the client then switching state too quickly - // as it tries to use CONFIG handler while being on the PLAY state. - // As a workaround, queue to ensure that packets are not concatinated - connection.eventLoop().schedule(() -> connection.write(this.configPackets), 250, TimeUnit.MILLISECONDS); + connection.write(this.configTransitionPackets); + + // As the client can't switch state immediately due to race condition between + // main and network threads, it should be synchronized + new TransitionConfirmHandler(connection).trackTransition(player, () -> { + this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); + }); + + return; // Wait for transition } } else { connection.delayedWrite(this.configPackets); } } + connection.setActiveSessionHandler(connection.getState(), sessionHandler); + this.currentOnline.increment(); sessionHandler.onConfig(new LimboPlayerImpl(this.plugin, this, player)); - if (callSpawn || connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { - this.postSpawn(sessionHandler, connection, player); + if (!stateSwitching || (connection.getState() != StateRegistry.CONFIG && !this.shouldRejoin)) { + this.onSpawn(handlerClass, connection, player, sessionHandler); } connection.flush(); @@ -429,16 +429,9 @@ private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handle }); } - protected void postSpawn(LimboSessionHandlerImpl sessionHandler, MinecraftConnection connection, ConnectedPlayer player) { - if (this.shouldRespawn) { - this.respawnPlayer(player); - } - - sessionHandler.onSpawn(); - } - - protected void preSpawn(Class handlerClass, - MinecraftConnection connection, ConnectedPlayer player) { + protected void onSpawn(Class handlerClass, + MinecraftConnection connection, ConnectedPlayer player, + LimboSessionHandlerImpl sessionHandler) { if (this.plugin.isLimboJoined(player)) { if (this.shouldRejoin) { if (connection.getType() == ConnectionTypes.LEGACY_FORGE) { @@ -484,6 +477,12 @@ protected void preSpawn(Class handlerClass, connection.delayedWrite(this.getBrandMessage(handlerClass)); this.plugin.setLimboJoined(player); + + if (this.shouldRespawn) { + this.respawnPlayer(player); + } + + sessionHandler.onSpawn(); } @Override diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index de493fc1..79dae5bd 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -136,8 +136,7 @@ public void onSpawn() { public boolean handle(FinishedUpdate packet) { this.player.getConnection().setState(this.limbo.localStateRegistry); - this.limbo.preSpawn(this.callback.getClass(), this.player.getConnection(), this.player); - this.limbo.postSpawn(this, this.player.getConnection(), this.player); + this.limbo.onSpawn(this.callback.getClass(), this.player.getConnection(), this.player, this); this.player.getConnection().flush(); return true; } From 04d1a3406faa47c54d802d13bd48ee292529fb69 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:06:21 +0300 Subject: [PATCH 03/17] Deduplicate prepared config packets --- .../src/main/java/net/elytrium/limboapi/server/LimboImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index aa746e58..a3d1cc41 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -220,7 +220,8 @@ protected void refresh() { this.addPostJoin(this.postJoinPackets); this.configTransitionPackets = this.plugin.createPreparedPacket() - .prepare(new StartUpdate(), ProtocolVersion.MINECRAFT_1_20_2); + .prepare(new StartUpdate(), ProtocolVersion.MINECRAFT_1_20_2) + .build(); this.configPackets = this.plugin.createConfigPreparedPacket(); this.configPackets.prepare(this::createRegistrySync, ProtocolVersion.MINECRAFT_1_20_2); @@ -228,6 +229,7 @@ protected void refresh() { this.configPackets.prepare(this::createTagsUpdate, ProtocolVersion.MINECRAFT_1_20_2); } this.configPackets.prepare(new FinishedUpdate(), ProtocolVersion.MINECRAFT_1_20_2); + this.configPackets.build(); this.firstChunks = this.createFirstChunks(); this.delayedChunks = this.createDelayedChunksPackets(); From cbb5892837c3eba3bd2d825ea3ac6cdf6c539e0a Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:16:50 +0300 Subject: [PATCH 04/17] Don't preserve ConfirmHandler and ClientPlaySessionHandler Preserving them will cause issues with server switching --- .../elytrium/limboapi/server/LimboSessionHandlerImpl.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 79dae5bd..8abedbe5 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -22,6 +22,7 @@ import com.velocitypowered.proxy.connection.MinecraftConnection; import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.AuthSessionHandler; +import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.protocol.MinecraftPacket; @@ -49,6 +50,7 @@ import net.elytrium.limboapi.Settings; import net.elytrium.limboapi.api.LimboSessionHandler; import net.elytrium.limboapi.api.player.LimboPlayer; +import net.elytrium.limboapi.injection.login.confirmation.ConfirmHandler; import net.elytrium.limboapi.protocol.LimboProtocol; import net.elytrium.limboapi.protocol.packets.c2s.MoveOnGroundOnlyPacket; import net.elytrium.limboapi.protocol.packets.c2s.MovePacket; @@ -315,7 +317,10 @@ public void disconnected() { return; } - if (!(this.originalHandler instanceof AuthSessionHandler) && !(this.originalHandler instanceof LimboSessionHandlerImpl)) { + if (!(this.originalHandler instanceof AuthSessionHandler) + && !(this.originalHandler instanceof LimboSessionHandlerImpl) + && !(this.originalHandler instanceof ClientPlaySessionHandler) // cause issues with server switching + && !(this.originalHandler instanceof ConfirmHandler)) { connection.eventLoop().execute(() -> connection.setActiveSessionHandler(connection.getState(), this.originalHandler)); } From c431c013566c8fb31e566db01e0a9ad9337122a7 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 10:42:35 +0300 Subject: [PATCH 05/17] Close "confirming" connection on spam --- .../injection/login/LoginTasksQueue.java | 2 +- .../login/confirmation/ConfirmHandler.java | 88 ++++++++++++++++++- .../elytrium/limboapi/server/LimboImpl.java | 2 +- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index 72892603..c050fad0 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -238,7 +238,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { this.player.disconnect0(reason.get(), true); } else { if (this.server.registerConnection(this.player)) { - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { + if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm && !confirm.isDone()) { confirm.waitForConfirmation(() -> this.connectToServer(logger, this.player, connection)); } else { this.connectToServer(logger, this.player, connection); diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java index bf251e51..ef518472 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java @@ -22,25 +22,34 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; +import com.velocitypowered.proxy.protocol.packet.PluginMessage; +import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; +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 io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.PlatformDependent; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.util.Queue; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; import net.elytrium.commons.utils.reflection.ReflectionException; import net.elytrium.limboapi.LimboAPI; +import net.elytrium.limboapi.Settings; public abstract class ConfirmHandler implements MinecraftSessionHandler { private static final MethodHandle TEARDOWN_METHOD; protected final CompletableFuture confirmation = new CompletableFuture<>(); - protected final Queue queuedPackets = PlatformDependent.newMpscQueue(); + protected final List queuedPackets = new ArrayList<>(); protected final MinecraftConnection connection; protected ConnectedPlayer player; + protected int genericBytes; public ConfirmHandler(MinecraftConnection connection) { this.connection = connection; @@ -50,17 +59,90 @@ public void setPlayer(ConnectedPlayer player) { this.player = player; } + public boolean isDone() { + return this.confirmation.isDone(); + } + public void waitForConfirmation(Runnable runnable) { this.confirmation.thenRun(runnable).thenRun(this::processQueued); } + @Override + public boolean handle(LegacyChat packet) { + return this.handleChat(packet.getMessage()); + } + + @Override + public boolean handle(KeyedPlayerChat packet) { + return this.handleChat(packet.getMessage()); + } + + @Override + public boolean handle(KeyedPlayerCommand packet) { + return this.handleChat("/" + packet.getCommand()); + } + + @Override + public boolean handle(SessionPlayerChat packet) { + return this.handleChat(packet.getMessage()); + } + + @Override + public boolean handle(SessionPlayerCommand packet) { + return this.handleChat("/" + packet.getCommand()); + } + + private boolean handleChat(String message) { + int messageLength = message.length(); + this.genericBytes += messageLength; + if (messageLength > Settings.IMP.MAIN.MAX_CHAT_MESSAGE_LENGTH) { + this.close("chat", messageLength); + } else if (this.genericBytes > Settings.IMP.MAIN.MAX_MULTI_GENERIC_PACKET_LENGTH) { + this.close("chat, multi", this.genericBytes); + } + + return true; + } + + @Override + public void handleUnknown(ByteBuf packet) { + int readableBytes = packet.readableBytes(); + this.genericBytes += readableBytes; + if (readableBytes > Settings.IMP.MAIN.MAX_UNKNOWN_PACKET_LENGTH) { + this.close("unknown", readableBytes); + } else if (this.genericBytes > Settings.IMP.MAIN.MAX_MULTI_GENERIC_PACKET_LENGTH) { + this.close("unknown, multi", this.genericBytes); + } + } + @Override public void handleGeneric(MinecraftPacket packet) { + if (packet instanceof PluginMessage pluginMessage) { + int singleLength = pluginMessage.content().readableBytes() + pluginMessage.getChannel().length() * 4; + this.genericBytes += singleLength; + if (singleLength > Settings.IMP.MAIN.MAX_SINGLE_GENERIC_PACKET_LENGTH) { + this.close("generic (PluginMessage packet (custom payload)), single", singleLength); + return; + } else if (this.genericBytes > Settings.IMP.MAIN.MAX_MULTI_GENERIC_PACKET_LENGTH) { + this.close("generic (PluginMessage packet (custom payload)), multi", this.genericBytes); + return; + } + } + if (this.connection.getState() == StateRegistry.CONFIG) { this.queuedPackets.add(ReferenceCountUtil.retain(packet)); } } + public void close(String type, int length) { + this.connection.close(); + + if (Settings.IMP.MAIN.LOGGING_ENABLED) { + LimboAPI.getLogger().warn( + "{} sent too big packet while confirming transition. (type: {}, length: {})", this.player, type, length); + } + } + @Override public void disconnected() { try { diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index a3d1cc41..5927ebb5 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -423,7 +423,7 @@ private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handle () -> this.limboName ); - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { + if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm && !confirm.isDone()) { confirm.waitForConfirmation(() -> this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection)); } else { this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); From 3c17b1cc37709e50f94a84ab93fa499f88b4ba35 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:31:19 +0300 Subject: [PATCH 06/17] Speedup 1.20.3+ world loading by following Vanilla behavior --- .../src/main/java/net/elytrium/limboapi/server/LimboImpl.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 5927ebb5..c1d30f4f 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -241,9 +241,6 @@ protected void refresh() { ).prepare( this.createUpdateViewPosition((int) this.world.getSpawnX(), (int) this.world.getSpawnZ()), ProtocolVersion.MINECRAFT_1_14 - ).prepare( - this.createLevelChunksLoadStartGameState(), - ProtocolVersion.MINECRAFT_1_20_3 ); if (this.shouldUpdateTags) { @@ -277,6 +274,7 @@ private TagsUpdate createTagsUpdate(ProtocolVersion version) { private void addPostJoin(PreparedPacket packet) { packet.prepare(this.createAvailableCommandsPacket(), ProtocolVersion.MINECRAFT_1_13) .prepare(this.createDefaultSpawnPositionPacket()) + .prepare(this.createLevelChunksLoadStartGameState(), ProtocolVersion.MINECRAFT_1_20_3) .prepare(this.createWorldTicksPacket()) .prepare(this::createBrandMessage) .build(); From ecdd520f032a2ef69b98ba13bff9669173f477f1 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:55:03 +0300 Subject: [PATCH 07/17] Synchronize LOGIN transition --- .../injection/login/LoginListener.java | 27 +++++++++++------ .../injection/login/LoginTasksQueue.java | 2 +- .../login/confirmation/ConfirmHandler.java | 25 ++++++++++++++-- .../elytrium/limboapi/server/LimboImpl.java | 29 +++++++++++-------- 4 files changed, 59 insertions(+), 24 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java index e5fb655f..aa502524 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java @@ -208,15 +208,11 @@ public void hookLoginSession(GameProfileRequestEvent event) throws Throwable { connection.setState(StateRegistry.PLAY); } - this.server.getEventManager().fire(new LoginLimboRegisterEvent(player)).thenAcceptAsync(limboRegisterEvent -> { - LoginTasksQueue queue = new LoginTasksQueue(this.plugin, handler, this.server, player, inbound, limboRegisterEvent.getOnJoinCallbacks()); - this.plugin.addLoginQueue(player, queue); - this.plugin.setKickCallback(player, limboRegisterEvent.getOnKickCallback()); - queue.next(); - }, connection.eventLoop()).exceptionally(t -> { - LimboAPI.getLogger().error("Exception while registering LimboAPI login handlers for {}.", player, t); - return null; - }); + if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { + confirm.thenRun(() -> this.callRegisterEvent(player, connection, inbound, handler)); + } else { + this.callRegisterEvent(player, connection, inbound, handler); + } } } else { player.disconnect0(Component.translatable("velocity.error.already-connected-proxy", NamedTextColor.RED), true); @@ -229,6 +225,19 @@ public void hookLoginSession(GameProfileRequestEvent event) throws Throwable { } } + private void callRegisterEvent(ConnectedPlayer player, MinecraftConnection connection, + InitialInboundConnection inbound, Object handler) { + this.server.getEventManager().fire(new LoginLimboRegisterEvent(player)).thenAcceptAsync(limboRegisterEvent -> { + LoginTasksQueue queue = new LoginTasksQueue(this.plugin, handler, this.server, player, inbound, limboRegisterEvent.getOnJoinCallbacks()); + this.plugin.addLoginQueue(player, queue); + this.plugin.setKickCallback(player, limboRegisterEvent.getOnKickCallback()); + queue.next(); + }, connection.eventLoop()).exceptionally(t -> { + LimboAPI.getLogger().error("Exception while registering LimboAPI login handlers for {}.", player, t); + return null; + }); + } + @Subscribe public void hookPlaySession(ServerConnectedEvent event) { ConnectedPlayer player = (ConnectedPlayer) event.getPlayer(); diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index c050fad0..72892603 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -238,7 +238,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { this.player.disconnect0(reason.get(), true); } else { if (this.server.registerConnection(this.player)) { - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm && !confirm.isDone()) { + if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { confirm.waitForConfirmation(() -> this.connectToServer(logger, this.player, connection)); } else { this.connectToServer(logger, this.player, connection); diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java index ef518472..3fe28258 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java @@ -50,6 +50,7 @@ public abstract class ConfirmHandler implements MinecraftSessionHandler { protected final MinecraftConnection connection; protected ConnectedPlayer player; protected int genericBytes; + protected boolean disableRelease; public ConfirmHandler(MinecraftConnection connection) { this.connection = connection; @@ -63,8 +64,26 @@ public boolean isDone() { return this.confirmation.isDone(); } + public CompletableFuture thenRun(Runnable runnable) { + return this.confirmation.thenRun(runnable); + } + public void waitForConfirmation(Runnable runnable) { - this.confirmation.thenRun(runnable).thenRun(this::processQueued); + this.thenRun(() -> { + this.disableRelease = true; + try { + runnable.run(); + } catch (Throwable throwable) { + LimboAPI.getLogger().error("Failed to confirm transition for " + this.player, throwable); + } + + try { + this.processQueued(); + } catch (Throwable throwable) { + LimboAPI.getLogger().error("Failed to process confirmation queue for " + this.player, throwable); + } + this.disableRelease = false; + }); } @Override @@ -154,7 +173,9 @@ public void disconnected() { } } } finally { - this.releaseQueue(); + if (!this.disableRelease) { + this.releaseQueue(); + } } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index c1d30f4f..8eb9dfa9 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -321,10 +321,9 @@ public void spawnPlayer(Player apiPlayer, LimboSessionHandler handler) { } } - private void spawnPlayerLocal(Class handlerClass, + private void spawnPlayerConfirmed(Class handlerClass, LimboSessionHandlerImpl sessionHandler, ConnectedPlayer player, MinecraftConnection connection) { - boolean stateSwitching = connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0; - if (stateSwitching) { + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { if (connection.getState() != StateRegistry.CONFIG) { if (this.shouldRejoin) { connection.write(this.configTransitionPackets); @@ -332,7 +331,7 @@ private void spawnPlayerLocal(Class handlerClass, // As the client can't switch state immediately due to race condition between // main and network threads, it should be synchronized new TransitionConfirmHandler(connection).trackTransition(player, () -> { - this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); + this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); }); return; // Wait for transition @@ -342,18 +341,28 @@ private void spawnPlayerLocal(Class handlerClass, } } - connection.setActiveSessionHandler(connection.getState(), sessionHandler); - this.currentOnline.increment(); + connection.setActiveSessionHandler(connection.getState(), sessionHandler); sessionHandler.onConfig(new LimboPlayerImpl(this.plugin, this, player)); - if (!stateSwitching || (connection.getState() != StateRegistry.CONFIG && !this.shouldRejoin)) { + + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0 + || (connection.getState() != StateRegistry.CONFIG && !this.shouldRejoin)) { this.onSpawn(handlerClass, connection, player, sessionHandler); } connection.flush(); } + private void spawnPlayerLocal(Class handlerClass, + LimboSessionHandlerImpl sessionHandler, ConnectedPlayer player, MinecraftConnection connection) { + if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { + confirm.waitForConfirmation(() -> this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection)); + } else { + this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); + } + } + private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handler, RegisteredServer previousServer) { MinecraftConnection connection = player.getConnection(); connection.eventLoop().execute(() -> { @@ -421,11 +430,7 @@ private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handle () -> this.limboName ); - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm && !confirm.isDone()) { - confirm.waitForConfirmation(() -> this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection)); - } else { - this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); - } + this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); }); } From cd64903f2fcc75e12543b67c78961c98418b6a80 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:21:37 +0300 Subject: [PATCH 08/17] Ensure that FastPrepareAPI encoder matches current state --- gradle.properties | 2 +- .../java/net/elytrium/limboapi/LimboAPI.java | 37 ++++++++++++++++++- .../injection/login/LoginListener.java | 2 +- .../injection/login/LoginTasksQueue.java | 6 +-- .../login/confirmation/ConfirmHandler.java | 4 +- .../confirmation/LoginConfirmHandler.java | 7 ++-- .../TransitionConfirmHandler.java | 9 +++-- .../elytrium/limboapi/server/LimboImpl.java | 2 +- .../server/LimboSessionHandlerImpl.java | 5 ++- 9 files changed, 57 insertions(+), 17 deletions(-) diff --git a/gradle.properties b/gradle.properties index d74abd00..e67ad429 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx4096m -fastPrepareVersion=1.0.8 +fastPrepareVersion=1.0.9 velocityVersion=3.3.0-SNAPSHOT nettyVersion=4.1.86.Final fastutilVersion=8.5.11 diff --git a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java index 75832a66..4e6abe30 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java +++ b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java @@ -32,6 +32,7 @@ import com.velocitypowered.natives.util.Natives; import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.event.VelocityEventManager; import com.velocitypowered.proxy.network.Connections; @@ -61,6 +62,7 @@ import net.elytrium.commons.utils.reflection.ReflectionException; import net.elytrium.commons.utils.updates.UpdatesChecker; import net.elytrium.fastprepare.PreparedPacketFactory; +import net.elytrium.fastprepare.handler.PreparedPacketEncoder; import net.elytrium.limboapi.api.Limbo; import net.elytrium.limboapi.api.LimboFactory; import net.elytrium.limboapi.api.chunk.BuiltInBiome; @@ -414,7 +416,40 @@ public ByteBuf encodeSingleLoginUncompressed(MinecraftPacket packet, ProtocolVer } public void inject3rdParty(Player player, MinecraftConnection connection, ChannelPipeline pipeline) { - this.preparedPacketFactory.inject(player, connection, pipeline); + StateRegistry state = connection.getState(); + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0 + || (state != StateRegistry.CONFIG && state != StateRegistry.LOGIN)) { + this.preparedPacketFactory.inject(player, connection, pipeline); + } else { + this.configPreparedPacketFactory.inject(player, connection, pipeline); + } + } + + public void setState(MinecraftConnection connection, StateRegistry stateRegistry) { + connection.setState(stateRegistry); + this.refresh3rdParty(connection); + } + + public void setActiveSessionHandler(MinecraftConnection connection, StateRegistry stateRegistry, + MinecraftSessionHandler sessionHandler) { + connection.setActiveSessionHandler(stateRegistry, sessionHandler); + this.refresh3rdParty(connection); + } + + public void refresh3rdParty(MinecraftConnection connection) { + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { + return; // No need to change the state for a current version + } + + PreparedPacketEncoder encoder = connection.getChannel().pipeline().get(PreparedPacketEncoder.class); + if (encoder != null) { + StateRegistry state = connection.getState(); + if (state != StateRegistry.CONFIG && state != StateRegistry.LOGIN) { + encoder.setFactory(this.preparedPacketFactory); + } else if (state == StateRegistry.CONFIG) { + encoder.setFactory(this.configPreparedPacketFactory); + } + } } public void deject3rdParty(ChannelPipeline pipeline) { diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java index aa502524..fa674780 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java @@ -136,7 +136,7 @@ public void hookLoginSession(GameProfileRequestEvent event) throws Throwable { MC_CONNECTION_FIELD.set(handler, CLOSED_MINECRAFT_CONNECTION); if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { - connection.setActiveSessionHandler(StateRegistry.LOGIN, new LoginConfirmHandler(connection)); + connection.setActiveSessionHandler(StateRegistry.LOGIN, new LoginConfirmHandler(this.plugin, connection)); } // From Velocity. diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index 72892603..e3a517ba 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -201,7 +201,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { connection.setAssociation(this.player); if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0 || (connection.getState() != StateRegistry.LOGIN && connection.getState() != StateRegistry.CONFIG)) { - connection.setState(StateRegistry.PLAY); + this.plugin.setState(connection, StateRegistry.PLAY); } ChannelPipeline pipeline = connection.getChannel().pipeline(); @@ -266,12 +266,12 @@ private void connectToServer(Logger logger, ConnectedPlayer player, MinecraftCon connection.write(new StartUpdate()); // Synchronize to make sure that nothing from PLAY state isn't received in CONFIG state - new TransitionConfirmHandler(connection) + new TransitionConfirmHandler(this.plugin, connection) .trackTransition(player, () -> this.connectToServer(logger, player, connection)); return; // Re-running this method due to synchronization with the client } else { - connection.setActiveSessionHandler(StateRegistry.CONFIG, + this.plugin.setActiveSessionHandler(connection, StateRegistry.CONFIG, new ClientConfigSessionHandler(this.server, this.player)); } diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java index 3fe28258..ad5eee40 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java @@ -45,6 +45,7 @@ public abstract class ConfirmHandler implements MinecraftSessionHandler { private static final MethodHandle TEARDOWN_METHOD; + protected final LimboAPI plugin; protected final CompletableFuture confirmation = new CompletableFuture<>(); protected final List queuedPackets = new ArrayList<>(); protected final MinecraftConnection connection; @@ -52,7 +53,8 @@ public abstract class ConfirmHandler implements MinecraftSessionHandler { protected int genericBytes; protected boolean disableRelease; - public ConfirmHandler(MinecraftConnection connection) { + public ConfirmHandler(LimboAPI plugin, MinecraftConnection connection) { + this.plugin = plugin; this.connection = connection; } diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java index a28c174e..6fe715e8 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java @@ -21,16 +21,17 @@ import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import io.netty.buffer.ByteBuf; +import net.elytrium.limboapi.LimboAPI; public class LoginConfirmHandler extends ConfirmHandler { - public LoginConfirmHandler(MinecraftConnection connection) { - super(connection); + public LoginConfirmHandler(LimboAPI plugin, MinecraftConnection connection) { + super(plugin, connection); } @Override public boolean handle(LoginAcknowledged packet) { - this.connection.setState(StateRegistry.CONFIG); + this.plugin.setState(this.connection, StateRegistry.CONFIG); this.confirmation.complete(this); return true; } diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java index 273f657a..f5f41a66 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java @@ -21,17 +21,18 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; +import net.elytrium.limboapi.LimboAPI; public class TransitionConfirmHandler extends ConfirmHandler { - public TransitionConfirmHandler(MinecraftConnection connection) { - super(connection); + public TransitionConfirmHandler(LimboAPI plugin, MinecraftConnection connection) { + super(plugin, connection); } @Override public boolean handle(FinishedUpdate packet) { if (this.connection.getState() == StateRegistry.PLAY) { - this.connection.setState(StateRegistry.CONFIG); + this.plugin.setState(this.connection, StateRegistry.CONFIG); this.confirmation.complete(this); return true; } @@ -41,7 +42,7 @@ public boolean handle(FinishedUpdate packet) { public void trackTransition(ConnectedPlayer player, Runnable runnable) { this.setPlayer(player); - this.connection.setActiveSessionHandler(StateRegistry.PLAY, this); + this.plugin.setActiveSessionHandler(this.connection, StateRegistry.PLAY, this); this.waitForConfirmation(runnable); } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 8eb9dfa9..ccdbb37f 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -330,7 +330,7 @@ private void spawnPlayerConfirmed(Class handlerCl // As the client can't switch state immediately due to race condition between // main and network threads, it should be synchronized - new TransitionConfirmHandler(connection).trackTransition(player, () -> { + new TransitionConfirmHandler(this.plugin, connection).trackTransition(player, () -> { this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); }); diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 8abedbe5..9950d57e 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -136,7 +136,7 @@ public void onSpawn() { @Override public boolean handle(FinishedUpdate packet) { - this.player.getConnection().setState(this.limbo.localStateRegistry); + this.plugin.setState(this.player.getConnection(), this.limbo.localStateRegistry); this.limbo.onSpawn(this.callback.getClass(), this.player.getConnection(), this.player, this); this.player.getConnection().flush(); @@ -321,7 +321,8 @@ public void disconnected() { && !(this.originalHandler instanceof LimboSessionHandlerImpl) && !(this.originalHandler instanceof ClientPlaySessionHandler) // cause issues with server switching && !(this.originalHandler instanceof ConfirmHandler)) { - connection.eventLoop().execute(() -> connection.setActiveSessionHandler(connection.getState(), this.originalHandler)); + connection.eventLoop().execute(() -> + this.plugin.setActiveSessionHandler(connection, connection.getState(), this.originalHandler)); } ChannelPipeline pipeline = connection.getChannel().pipeline(); From 504ea39153a31bc63b2cda48fcf195c6e168595f Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:31:04 +0300 Subject: [PATCH 09/17] Remove NbtUtils as Velocity now supports 1.20.2 NBT --- .../protocol/packets/s2c/ChunkDataPacket.java | 7 ++- .../protocol/packets/s2c/SetSlotPacket.java | 3 +- .../elytrium/limboapi/server/LimboImpl.java | 3 +- .../net/elytrium/limboapi/utils/NbtUtils.java | 43 ------------------- 4 files changed, 5 insertions(+), 51 deletions(-) delete mode 100644 plugin/src/main/java/net/elytrium/limboapi/utils/NbtUtils.java diff --git a/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/ChunkDataPacket.java b/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/ChunkDataPacket.java index baab1af5..a94f7bf3 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/ChunkDataPacket.java +++ b/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/ChunkDataPacket.java @@ -40,7 +40,6 @@ import net.elytrium.limboapi.mcprotocollib.BitStorage116; import net.elytrium.limboapi.mcprotocollib.BitStorage19; import net.elytrium.limboapi.protocol.util.NetworkSection; -import net.elytrium.limboapi.utils.NbtUtils; import net.kyori.adventure.nbt.CompoundBinaryTag; public class ChunkDataPacket implements MinecraftPacket { @@ -131,9 +130,9 @@ public void encode(ByteBuf buf, Direction direction, ProtocolVersion version) { // 1.14+ heightMap. if (version.compareTo(ProtocolVersion.MINECRAFT_1_14) >= 0) { if (version.compareTo(ProtocolVersion.MINECRAFT_1_16) < 0) { - NbtUtils.writeCompoundTag(buf, this.heightmap114, version); + ProtocolUtils.writeBinaryTag(buf, version, this.heightmap114); } else { - NbtUtils.writeCompoundTag(buf, this.heightmap116, version); + ProtocolUtils.writeBinaryTag(buf, version, this.heightmap116); } } @@ -172,7 +171,7 @@ public void encode(ByteBuf buf, Direction direction, ProtocolVersion version) { blockEntityNbt.putInt("z", blockEntityEntry.getPosZ()); } - NbtUtils.writeCompoundTag(buf, blockEntityNbt, version); + ProtocolUtils.writeBinaryTag(buf, version, blockEntityNbt); } } if (version.compareTo(ProtocolVersion.MINECRAFT_1_17_1) > 0) { diff --git a/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetSlotPacket.java b/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetSlotPacket.java index a41c992a..4394fe4b 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetSlotPacket.java +++ b/plugin/src/main/java/net/elytrium/limboapi/protocol/packets/s2c/SetSlotPacket.java @@ -23,7 +23,6 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils; import io.netty.buffer.ByteBuf; import net.elytrium.limboapi.api.material.VirtualItem; -import net.elytrium.limboapi.utils.NbtUtils; import net.kyori.adventure.nbt.CompoundBinaryTag; import org.checkerframework.checker.nullness.qual.Nullable; @@ -93,7 +92,7 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi buf.writeByte(0); } } else { - NbtUtils.writeCompoundTag(buf, this.nbt, protocolVersion); + ProtocolUtils.writeBinaryTag(buf, protocolVersion, this.nbt); } } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index ccdbb37f..9327ccd8 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -103,7 +103,6 @@ import net.elytrium.limboapi.protocol.packets.s2c.TimeUpdatePacket; import net.elytrium.limboapi.protocol.packets.s2c.UpdateViewPositionPacket; import net.elytrium.limboapi.server.world.SimpleTagManager; -import net.elytrium.limboapi.utils.NbtUtils; import net.kyori.adventure.nbt.BinaryTagIO; import net.kyori.adventure.nbt.BinaryTagTypes; import net.kyori.adventure.nbt.CompoundBinaryTag; @@ -260,7 +259,7 @@ private RegistrySync createRegistrySync(ProtocolVersion version) { // Blame Velocity for this madness ByteBuf encodedRegistry = this.plugin.getPreparedPacketFactory().getPreparedPacketAllocator().ioBuffer(); - NbtUtils.writeCompoundTag(encodedRegistry, join.getRegistry(), version); + ProtocolUtils.writeBinaryTag(encodedRegistry, version, join.getRegistry()); RegistrySync sync = new RegistrySync(); sync.replace(encodedRegistry); diff --git a/plugin/src/main/java/net/elytrium/limboapi/utils/NbtUtils.java b/plugin/src/main/java/net/elytrium/limboapi/utils/NbtUtils.java deleted file mode 100644 index b68f0ccd..00000000 --- a/plugin/src/main/java/net/elytrium/limboapi/utils/NbtUtils.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2021 - 2023 Elytrium - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package net.elytrium.limboapi.utils; - -import com.velocitypowered.api.network.ProtocolVersion; -import com.velocitypowered.proxy.protocol.ProtocolUtils; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufOutputStream; -import io.netty.handler.codec.EncoderException; -import java.io.IOException; -import net.kyori.adventure.nbt.BinaryTagTypes; -import net.kyori.adventure.nbt.CompoundBinaryTag; - -// TODO: contribute to the Velocity? -public class NbtUtils { - public static void writeCompoundTag(ByteBuf buf, CompoundBinaryTag tag, ProtocolVersion version) { - if (version.compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { - try { - buf.writeByte(BinaryTagTypes.COMPOUND.id()); - BinaryTagTypes.COMPOUND.write(tag, new ByteBufOutputStream(buf)); - } catch (IOException exception) { - throw new EncoderException("Unable to encode NBT CompoundTag", exception); - } - } else { - ProtocolUtils.writeBinaryTag(buf, version, tag); - } - } -} From d1b5dc84119a346044db4c8d157580a00f15942b Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Thu, 28 Dec 2023 15:06:25 +0300 Subject: [PATCH 10/17] Check for a custom PLAY state while sending UpsertPlayerInfo --- .../net/elytrium/limboapi/injection/login/LoginTasksQueue.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index e3a517ba..ec08abdb 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -146,7 +146,8 @@ private void finish() { .setProperties(gameProfile.getGameProfile().getProperties()) ) )); - } else if (connection.getState() == StateRegistry.PLAY) { + } else if (connection.getState() != StateRegistry.CONFIG + && connection.getState() != StateRegistry.LOGIN) { UpsertPlayerInfo.Entry playerInfoEntry = new UpsertPlayerInfo.Entry(this.player.getUniqueId()); playerInfoEntry.setDisplayName(new ComponentHolder(this.player.getProtocolVersion(), Component.text(gameProfile.getUsername()))); playerInfoEntry.setProfile(gameProfile.getGameProfile()); From a50578d127fa6bf7ad9f7c338145932799c0f510 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Fri, 29 Dec 2023 18:13:02 +0300 Subject: [PATCH 11/17] Rollback CONFIG handler on server switch --- .../elytrium/limboapi/server/LimboPlayerImpl.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java index 52d72810..ce5b8ccf 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java @@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.Player; import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler; import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; @@ -277,10 +278,15 @@ private void deject() { } private void sendToRegisteredServer(RegisteredServer server) { - this.connection.eventLoop().execute(() -> { - this.connection.setState(StateRegistry.PLAY); - this.player.createConnectionRequest(server).fireAndForget(); - }); + this.connection.setState(StateRegistry.PLAY); + + // Rollback CONFIG session handler to Velocity one + if (this.connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + this.connection.addSessionHandler(StateRegistry.CONFIG, + new ClientConfigSessionHandler(this.plugin.getServer(), this.player)); + } + + this.player.createConnectionRequest(server).fireAndForget(); } @Override From 96c9a474e005ebd1dadadf13899316d325d392f6 Mon Sep 17 00:00:00 2001 From: UserNugget <47762903+UserNugget@users.noreply.github.com> Date: Tue, 2 Jan 2024 22:24:21 +0300 Subject: [PATCH 12/17] Move the entire PLAY->CONFIG transition logic to the LimboSessionHandlerImpl --- .../java/net/elytrium/limboapi/LimboAPI.java | 2 +- .../java/net/elytrium/limboapi/Settings.java | 1 + .../injection/login/LoginListener.java | 49 ++-- .../injection/login/LoginTasksQueue.java | 21 +- .../login/confirmation/ConfirmHandler.java | 225 ------------------ .../confirmation/LoginConfirmHandler.java | 97 +++++++- .../TransitionConfirmHandler.java | 48 ---- .../limboapi/server/CachedPackets.java | 27 ++- .../elytrium/limboapi/server/LimboImpl.java | 35 +-- .../limboapi/server/LimboPlayerImpl.java | 25 +- .../server/LimboSessionHandlerImpl.java | 87 +++++-- 11 files changed, 252 insertions(+), 365 deletions(-) delete mode 100644 plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java delete mode 100644 plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java diff --git a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java index 4e6abe30..40fe8b5c 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java +++ b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java @@ -446,7 +446,7 @@ public void refresh3rdParty(MinecraftConnection connection) { StateRegistry state = connection.getState(); if (state != StateRegistry.CONFIG && state != StateRegistry.LOGIN) { encoder.setFactory(this.preparedPacketFactory); - } else if (state == StateRegistry.CONFIG) { + } else { encoder.setFactory(this.configPreparedPacketFactory); } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/Settings.java b/plugin/src/main/java/net/elytrium/limboapi/Settings.java index 36005bd0..4eeb932a 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/Settings.java +++ b/plugin/src/main/java/net/elytrium/limboapi/Settings.java @@ -115,6 +115,7 @@ public static class MESSAGES { public String TOO_BIG_PACKET = "{PRFX}{NL}{NL}&cYour client sent too big packet!"; public String INVALID_PING = "{PRFX}{NL}{NL}&cYour client sent invalid ping packet!"; + public String INVALID_SWITCH = "{PRFX}{NL}{NL}&cYour client sent an unexpected state switching packet!"; public String TIME_OUT = "{PRFX}{NL}{NL}Timed out."; } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java index fa674780..99af8c89 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java @@ -76,7 +76,6 @@ import net.elytrium.limboapi.injection.dummy.ClosedChannel; import net.elytrium.limboapi.injection.dummy.ClosedMinecraftConnection; import net.elytrium.limboapi.injection.dummy.DummyEventPool; -import net.elytrium.limboapi.injection.login.confirmation.ConfirmHandler; import net.elytrium.limboapi.injection.login.confirmation.LoginConfirmHandler; import net.elytrium.limboapi.injection.packet.ServerLoginSuccessHook; import net.kyori.adventure.text.Component; @@ -153,7 +152,7 @@ public void hookLoginSession(GameProfileRequestEvent event) throws Throwable { inboundConnection.getIdentifiedKey() ); if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { - ((ConfirmHandler) connection.getActiveSessionHandler()).setPlayer(player); + ((LoginConfirmHandler) connection.getActiveSessionHandler()).setPlayer(player); } if (this.server.canRegisterConnection(player)) { if (!connection.isClosed()) { @@ -204,14 +203,12 @@ public void hookLoginSession(GameProfileRequestEvent event) throws Throwable { this.plugin.setInitialID(player, playerUniqueID); - if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { - connection.setState(StateRegistry.PLAY); - } - - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { - confirm.thenRun(() -> this.callRegisterEvent(player, connection, inbound, handler)); + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + ((LoginConfirmHandler) connection.getActiveSessionHandler()) + .thenRun(() -> this.fireRegisterEvent(player, connection, inbound, handler)); } else { - this.callRegisterEvent(player, connection, inbound, handler); + connection.setState(StateRegistry.PLAY); + this.fireRegisterEvent(player, connection, inbound, handler); } } } else { @@ -225,8 +222,8 @@ public void hookLoginSession(GameProfileRequestEvent event) throws Throwable { } } - private void callRegisterEvent(ConnectedPlayer player, MinecraftConnection connection, - InitialInboundConnection inbound, Object handler) { + private void fireRegisterEvent(ConnectedPlayer player, MinecraftConnection connection, + InitialInboundConnection inbound, Object handler) { this.server.getEventManager().fire(new LoginLimboRegisterEvent(player)).thenAcceptAsync(limboRegisterEvent -> { LoginTasksQueue queue = new LoginTasksQueue(this.plugin, handler, this.server, player, inbound, limboRegisterEvent.getOnJoinCallbacks()); this.plugin.addLoginQueue(player, queue); @@ -243,17 +240,27 @@ public void hookPlaySession(ServerConnectedEvent event) { ConnectedPlayer player = (ConnectedPlayer) event.getPlayer(); MinecraftConnection connection = player.getConnection(); - connection.eventLoop().execute(() -> { - if (!(connection.getActiveSessionHandler() instanceof ClientPlaySessionHandler)) { - try { - ClientPlaySessionHandler playHandler = new ClientPlaySessionHandler(this.server, player); - SPAWNED_FIELD.invokeExact(playHandler, this.plugin.isLimboJoined(player)); - connection.setActiveSessionHandler(connection.getState(), playHandler); - } catch (Throwable e) { - throw new ReflectionException(e); - } + // No need to do things below as the client should be despawned by default + if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { + return; + } + + // Should never happen with default Velocity + if (!connection.eventLoop().inEventLoop()) { + connection.eventLoop().execute(() -> this.hookPlaySession(event)); + return; + } + + // As Velocity automatically replaces custom handlers with ClientPlaySessionHandler, do it before + if (!(connection.getActiveSessionHandler() instanceof ClientPlaySessionHandler)) { + try { + ClientPlaySessionHandler playHandler = new ClientPlaySessionHandler(this.server, player); + SPAWNED_FIELD.invokeExact(playHandler, this.plugin.isLimboJoined(player)); + connection.setActiveSessionHandler(connection.getState(), playHandler); + } catch (Throwable e) { + throw new ReflectionException(e); } - }); + } } static { diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index ec08abdb..d8afbc8f 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -57,7 +57,6 @@ import com.velocitypowered.proxy.protocol.packet.LegacyPlayerListItem; import com.velocitypowered.proxy.protocol.packet.UpsertPlayerInfo; import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder; -import com.velocitypowered.proxy.protocol.packet.config.StartUpdate; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.netty.channel.ChannelPipeline; import io.netty.channel.EventLoop; @@ -74,8 +73,8 @@ import net.elytrium.commons.utils.reflection.ReflectionException; import net.elytrium.limboapi.LimboAPI; import net.elytrium.limboapi.injection.event.EventManagerHook; -import net.elytrium.limboapi.injection.login.confirmation.ConfirmHandler; -import net.elytrium.limboapi.injection.login.confirmation.TransitionConfirmHandler; +import net.elytrium.limboapi.injection.login.confirmation.LoginConfirmHandler; +import net.elytrium.limboapi.server.LimboSessionHandlerImpl; import net.kyori.adventure.text.Component; import org.slf4j.Logger; @@ -146,8 +145,7 @@ private void finish() { .setProperties(gameProfile.getGameProfile().getProperties()) ) )); - } else if (connection.getState() != StateRegistry.CONFIG - && connection.getState() != StateRegistry.LOGIN) { + } else if (connection.getState() != StateRegistry.CONFIG) { UpsertPlayerInfo.Entry playerInfoEntry = new UpsertPlayerInfo.Entry(this.player.getUniqueId()); playerInfoEntry.setDisplayName(new ComponentHolder(this.player.getProtocolVersion(), Component.text(gameProfile.getUsername()))); playerInfoEntry.setProfile(gameProfile.getGameProfile()); @@ -201,7 +199,7 @@ private void finish() { private void initialize(MinecraftConnection connection) throws Throwable { connection.setAssociation(this.player); if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0 - || (connection.getState() != StateRegistry.LOGIN && connection.getState() != StateRegistry.CONFIG)) { + || connection.getState() != StateRegistry.CONFIG) { this.plugin.setState(connection, StateRegistry.PLAY); } @@ -239,7 +237,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { this.player.disconnect0(reason.get(), true); } else { if (this.server.registerConnection(this.player)) { - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { + if (connection.getActiveSessionHandler() instanceof LoginConfirmHandler confirm) { confirm.waitForConfirmation(() -> this.connectToServer(logger, this.player, connection)); } else { this.connectToServer(logger, this.player, connection); @@ -255,6 +253,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { }); } + @SuppressFBWarnings("NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE") private void connectToServer(Logger logger, ConnectedPlayer player, MinecraftConnection connection) { if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { try { @@ -264,11 +263,9 @@ private void connectToServer(Logger logger, ConnectedPlayer player, MinecraftCon throw new ReflectionException(e); } } else if (connection.getState() == StateRegistry.PLAY) { - connection.write(new StartUpdate()); - - // Synchronize to make sure that nothing from PLAY state isn't received in CONFIG state - new TransitionConfirmHandler(this.plugin, connection) - .trackTransition(player, () -> this.connectToServer(logger, player, connection)); + // Synchronize to make sure that nothing from PLAY state received in CONFIG state + ((LimboSessionHandlerImpl) connection.getActiveSessionHandler()) + .disconnectToConfig(() -> this.connectToServer(logger, player, connection)); return; // Re-running this method due to synchronization with the client } else { diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java deleted file mode 100644 index ad5eee40..00000000 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/ConfirmHandler.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2021 - 2023 Elytrium - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package net.elytrium.limboapi.injection.login.confirmation; - -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.MinecraftSessionHandler; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.MinecraftPacket; -import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.packet.PluginMessage; -import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; -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 io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.util.ReferenceCountUtil; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import net.elytrium.commons.utils.reflection.ReflectionException; -import net.elytrium.limboapi.LimboAPI; -import net.elytrium.limboapi.Settings; - -public abstract class ConfirmHandler implements MinecraftSessionHandler { - - private static final MethodHandle TEARDOWN_METHOD; - - protected final LimboAPI plugin; - protected final CompletableFuture confirmation = new CompletableFuture<>(); - protected final List queuedPackets = new ArrayList<>(); - protected final MinecraftConnection connection; - protected ConnectedPlayer player; - protected int genericBytes; - protected boolean disableRelease; - - public ConfirmHandler(LimboAPI plugin, MinecraftConnection connection) { - this.plugin = plugin; - this.connection = connection; - } - - public void setPlayer(ConnectedPlayer player) { - this.player = player; - } - - public boolean isDone() { - return this.confirmation.isDone(); - } - - public CompletableFuture thenRun(Runnable runnable) { - return this.confirmation.thenRun(runnable); - } - - public void waitForConfirmation(Runnable runnable) { - this.thenRun(() -> { - this.disableRelease = true; - try { - runnable.run(); - } catch (Throwable throwable) { - LimboAPI.getLogger().error("Failed to confirm transition for " + this.player, throwable); - } - - try { - this.processQueued(); - } catch (Throwable throwable) { - LimboAPI.getLogger().error("Failed to process confirmation queue for " + this.player, throwable); - } - this.disableRelease = false; - }); - } - - @Override - public boolean handle(LegacyChat packet) { - return this.handleChat(packet.getMessage()); - } - - @Override - public boolean handle(KeyedPlayerChat packet) { - return this.handleChat(packet.getMessage()); - } - - @Override - public boolean handle(KeyedPlayerCommand packet) { - return this.handleChat("/" + packet.getCommand()); - } - - @Override - public boolean handle(SessionPlayerChat packet) { - return this.handleChat(packet.getMessage()); - } - - @Override - public boolean handle(SessionPlayerCommand packet) { - return this.handleChat("/" + packet.getCommand()); - } - - private boolean handleChat(String message) { - int messageLength = message.length(); - this.genericBytes += messageLength; - if (messageLength > Settings.IMP.MAIN.MAX_CHAT_MESSAGE_LENGTH) { - this.close("chat", messageLength); - } else if (this.genericBytes > Settings.IMP.MAIN.MAX_MULTI_GENERIC_PACKET_LENGTH) { - this.close("chat, multi", this.genericBytes); - } - - return true; - } - - @Override - public void handleUnknown(ByteBuf packet) { - int readableBytes = packet.readableBytes(); - this.genericBytes += readableBytes; - if (readableBytes > Settings.IMP.MAIN.MAX_UNKNOWN_PACKET_LENGTH) { - this.close("unknown", readableBytes); - } else if (this.genericBytes > Settings.IMP.MAIN.MAX_MULTI_GENERIC_PACKET_LENGTH) { - this.close("unknown, multi", this.genericBytes); - } - } - - @Override - public void handleGeneric(MinecraftPacket packet) { - if (packet instanceof PluginMessage pluginMessage) { - int singleLength = pluginMessage.content().readableBytes() + pluginMessage.getChannel().length() * 4; - this.genericBytes += singleLength; - if (singleLength > Settings.IMP.MAIN.MAX_SINGLE_GENERIC_PACKET_LENGTH) { - this.close("generic (PluginMessage packet (custom payload)), single", singleLength); - return; - } else if (this.genericBytes > Settings.IMP.MAIN.MAX_MULTI_GENERIC_PACKET_LENGTH) { - this.close("generic (PluginMessage packet (custom payload)), multi", this.genericBytes); - return; - } - } - - if (this.connection.getState() == StateRegistry.CONFIG) { - this.queuedPackets.add(ReferenceCountUtil.retain(packet)); - } - } - - public void close(String type, int length) { - this.connection.close(); - - if (Settings.IMP.MAIN.LOGGING_ENABLED) { - LimboAPI.getLogger().warn( - "{} sent too big packet while confirming transition. (type: {}, length: {})", this.player, type, length); - } - } - - @Override - public void disconnected() { - try { - if (this.player != null) { - try { - TEARDOWN_METHOD.invokeExact(this.player); - } catch (Throwable e) { - throw new ReflectionException(e); - } - } - } finally { - if (!this.disableRelease) { - this.releaseQueue(); - } - } - } - - public void processQueued() { - try { - ChannelHandlerContext ctx = this.connection.getChannel().pipeline().context(this.connection); - MinecraftSessionHandler sessionHandler = this.connection.getActiveSessionHandler(); - if (sessionHandler != null && !sessionHandler.beforeHandle()) { - for (MinecraftPacket packet : this.queuedPackets) { - if (!this.connection.isClosed()) { - try { - if (!packet.handle(sessionHandler)) { - sessionHandler.handleGeneric(packet); - } - } catch (Throwable throwable) { - try { - this.connection.exceptionCaught(ctx, throwable); - } catch (Throwable t) { - LimboAPI.getLogger().error("{}: exception handling exception in {}", ctx.channel().remoteAddress(), this, t); - } - } - } - } - } - } finally { - this.releaseQueue(); - } - } - - public void releaseQueue() { - for (MinecraftPacket packet : this.queuedPackets) { - ReferenceCountUtil.release(packet); - } - this.queuedPackets.clear(); - } - - static { - try { - TEARDOWN_METHOD = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup()) - .findVirtual(ConnectedPlayer.class, "teardown", MethodType.methodType(void.class)); - } catch (NoSuchMethodException | IllegalAccessException e) { - throw new ReflectionException(e); - } - } -} diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java index 6fe715e8..73622811 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java @@ -18,15 +18,73 @@ package net.elytrium.limboapi.injection.login.confirmation; import com.velocitypowered.proxy.connection.MinecraftConnection; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.connection.client.ConnectedPlayer; +import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.LoginAcknowledged; import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.ReferenceCountUtil; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import net.elytrium.commons.utils.reflection.ReflectionException; import net.elytrium.limboapi.LimboAPI; -public class LoginConfirmHandler extends ConfirmHandler { +public class LoginConfirmHandler implements MinecraftSessionHandler { + + private static final MethodHandle TEARDOWN_METHOD; + + private final LimboAPI plugin; + private final CompletableFuture confirmation = new CompletableFuture<>(); + private final List queuedPackets = new ArrayList<>(); + private final MinecraftConnection connection; + private ConnectedPlayer player; public LoginConfirmHandler(LimboAPI plugin, MinecraftConnection connection) { - super(plugin, connection); + this.plugin = plugin; + this.connection = connection; + } + + public void setPlayer(ConnectedPlayer player) { + this.player = player; + } + + public boolean isDone() { + return this.confirmation.isDone(); + } + + public CompletableFuture thenRun(Runnable runnable) { + return this.confirmation.thenRun(runnable); + } + + public void waitForConfirmation(Runnable runnable) { + this.thenRun(() -> { + try { + runnable.run(); + } catch (Throwable throwable) { + LimboAPI.getLogger().error("Failed to confirm transition for " + this.player, throwable); + } + + try { + ChannelHandlerContext ctx = this.connection.getChannel().pipeline().context(this.connection); + for (MinecraftPacket packet : this.queuedPackets) { + try { + this.connection.channelRead(ctx, packet); + } catch (Throwable throwable) { + LimboAPI.getLogger().error("{}: exception handling exception in {}", ctx.channel().remoteAddress(), this, throwable); + } + } + + this.queuedPackets.clear(); + } catch (Throwable throwable) { + LimboAPI.getLogger().error("Failed to process packet queue for " + this.player, throwable); + } + }); } @Override @@ -36,8 +94,43 @@ public boolean handle(LoginAcknowledged packet) { return true; } + @Override + public void handleGeneric(MinecraftPacket packet) { + // As Velocity/LimboAPI can easly skip packets due to random delays, packets should be queued + // (executing events or spamming EventLoop::execute cause delays) + if (this.connection.getState() == StateRegistry.CONFIG) { + this.queuedPackets.add(ReferenceCountUtil.retain(packet)); + } + } + @Override public void handleUnknown(ByteBuf buf) { this.connection.close(true); } + + @Override + public void disconnected() { + try { + if (this.player != null) { + try { + TEARDOWN_METHOD.invokeExact(this.player); + } catch (Throwable e) { + throw new ReflectionException(e); + } + } + } finally { + for (MinecraftPacket packet : this.queuedPackets) { + ReferenceCountUtil.release(packet); + } + } + } + + static { + try { + TEARDOWN_METHOD = MethodHandles.privateLookupIn(ConnectedPlayer.class, MethodHandles.lookup()) + .findVirtual(ConnectedPlayer.class, "teardown", MethodType.methodType(void.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new ReflectionException(e); + } + } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java deleted file mode 100644 index f5f41a66..00000000 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/TransitionConfirmHandler.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2021 - 2023 Elytrium - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero 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 Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package net.elytrium.limboapi.injection.login.confirmation; - -import com.velocitypowered.proxy.connection.MinecraftConnection; -import com.velocitypowered.proxy.connection.client.ConnectedPlayer; -import com.velocitypowered.proxy.protocol.StateRegistry; -import com.velocitypowered.proxy.protocol.packet.config.FinishedUpdate; -import net.elytrium.limboapi.LimboAPI; - -public class TransitionConfirmHandler extends ConfirmHandler { - - public TransitionConfirmHandler(LimboAPI plugin, MinecraftConnection connection) { - super(plugin, connection); - } - - @Override - public boolean handle(FinishedUpdate packet) { - if (this.connection.getState() == StateRegistry.PLAY) { - this.plugin.setState(this.connection, StateRegistry.CONFIG); - this.confirmation.complete(this); - return true; - } - - return false; - } - - public void trackTransition(ConnectedPlayer player, Runnable runnable) { - this.setPlayer(player); - this.plugin.setActiveSessionHandler(this.connection, StateRegistry.PLAY, this); - this.waitForConfirmation(runnable); - } -} diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/CachedPackets.java b/plugin/src/main/java/net/elytrium/limboapi/server/CachedPackets.java index c3b5c994..d08aa529 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/CachedPackets.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/CachedPackets.java @@ -18,6 +18,7 @@ package net.elytrium.limboapi.server; import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.Disconnect; import net.elytrium.limboapi.LimboAPI; import net.elytrium.limboapi.Settings; @@ -29,7 +30,9 @@ public class CachedPackets { private PreparedPacket tooBigPacket; private PreparedPacket invalidPing; - private PreparedPacket timeOut; + private PreparedPacket invalidSwitch; + private PreparedPacket playTimeOut; + private PreparedPacket configTimeOut; private boolean cached; public CachedPackets(LimboAPI plugin) { @@ -45,8 +48,12 @@ public void createPackets() { .prepare(version -> this.createDisconnectPacket(Settings.IMP.MAIN.MESSAGES.TOO_BIG_PACKET, version)).build(); this.invalidPing = this.plugin.createPreparedPacket() .prepare(version -> this.createDisconnectPacket(Settings.IMP.MAIN.MESSAGES.INVALID_PING, version)).build(); - this.timeOut = this.plugin.createPreparedPacket() + this.invalidSwitch = this.plugin.createConfigPreparedPacket() + .prepare(version -> this.createDisconnectPacket(Settings.IMP.MAIN.MESSAGES.INVALID_SWITCH, version), ProtocolVersion.MINECRAFT_1_20_2).build(); + this.playTimeOut = this.plugin.createPreparedPacket() .prepare(version -> this.createDisconnectPacket(Settings.IMP.MAIN.MESSAGES.TIME_OUT, version)).build(); + this.configTimeOut = this.plugin.createConfigPreparedPacket() + .prepare(version -> this.createDisconnectPacket(Settings.IMP.MAIN.MESSAGES.TIME_OUT, version), ProtocolVersion.MINECRAFT_1_20_2).build(); this.cached = true; } @@ -63,13 +70,23 @@ public PreparedPacket getInvalidPing() { return this.invalidPing; } - public PreparedPacket getTimeOut() { - return this.timeOut; + public PreparedPacket getInvalidSwitch() { + return this.invalidSwitch; + } + + public PreparedPacket getTimeOut(StateRegistry stateRegistry) { + if (stateRegistry == StateRegistry.CONFIG) { + return this.configTimeOut; + } + + return this.playTimeOut; } public void dispose() { this.tooBigPacket.release(); this.invalidPing.release(); - this.timeOut.release(); + this.invalidSwitch.release(); + this.playTimeOut.release(); + this.configTimeOut.release(); } } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 9327ccd8..465109aa 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -91,8 +91,7 @@ import net.elytrium.limboapi.api.protocol.PacketDirection; import net.elytrium.limboapi.api.protocol.PreparedPacket; import net.elytrium.limboapi.api.protocol.packets.PacketMapping; -import net.elytrium.limboapi.injection.login.confirmation.ConfirmHandler; -import net.elytrium.limboapi.injection.login.confirmation.TransitionConfirmHandler; +import net.elytrium.limboapi.injection.login.confirmation.LoginConfirmHandler; import net.elytrium.limboapi.injection.packet.MinecraftLimitedCompressDecoder; import net.elytrium.limboapi.material.Biome; import net.elytrium.limboapi.protocol.LimboProtocol; @@ -219,8 +218,8 @@ protected void refresh() { this.addPostJoin(this.postJoinPackets); this.configTransitionPackets = this.plugin.createPreparedPacket() - .prepare(new StartUpdate(), ProtocolVersion.MINECRAFT_1_20_2) - .build(); + .prepare(new StartUpdate(), ProtocolVersion.MINECRAFT_1_20_2) + .build(); this.configPackets = this.plugin.createConfigPreparedPacket(); this.configPackets.prepare(this::createRegistrySync, ProtocolVersion.MINECRAFT_1_20_2); @@ -320,20 +319,16 @@ public void spawnPlayer(Player apiPlayer, LimboSessionHandler handler) { } } - private void spawnPlayerConfirmed(Class handlerClass, + protected void spawnPlayerConfirmed(Class handlerClass, LimboSessionHandlerImpl sessionHandler, ConnectedPlayer player, MinecraftConnection connection) { if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { if (connection.getState() != StateRegistry.CONFIG) { if (this.shouldRejoin) { connection.write(this.configTransitionPackets); - // As the client can't switch state immediately due to race condition between - // main and network threads, it should be synchronized - new TransitionConfirmHandler(this.plugin, connection).trackTransition(player, () -> { - this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); - }); - - return; // Wait for transition + // Continue state switching on the handler side + connection.setActiveSessionHandler(connection.getState(), sessionHandler); + return; } } else { connection.delayedWrite(this.configPackets); @@ -353,15 +348,6 @@ private void spawnPlayerConfirmed(Class handlerCl connection.flush(); } - private void spawnPlayerLocal(Class handlerClass, - LimboSessionHandlerImpl sessionHandler, ConnectedPlayer player, MinecraftConnection connection) { - if (connection.getActiveSessionHandler() instanceof ConfirmHandler confirm) { - confirm.waitForConfirmation(() -> this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection)); - } else { - this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); - } - } - private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handler, RegisteredServer previousServer) { MinecraftConnection connection = player.getConnection(); connection.eventLoop().execute(() -> { @@ -424,12 +410,17 @@ private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handle this, player, handler, + connection.getState(), connection.getActiveSessionHandler(), previousServer, () -> this.limboName ); - this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); + if (connection.getActiveSessionHandler() instanceof LoginConfirmHandler confirm) { + confirm.waitForConfirmation(() -> this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection)); + } else { + this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); + } }); } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java index ce5b8ccf..cd008b24 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java @@ -234,15 +234,13 @@ public void enableFalling() { public void disconnect() { this.connection.eventLoop().execute(() -> { if (this.connection.getActiveSessionHandler() == this.sessionHandler) { - this.sessionHandler.switchDisconnection(() -> { + this.sessionHandler.disconnect(() -> { if (this.plugin.hasLoginQueue(this.player)) { this.sessionHandler.disconnected(); this.plugin.getLoginQueue(this.player).next(); } else { RegisteredServer server = this.sessionHandler.getPreviousServer(); if (server != null) { - this.deject(); - this.sessionHandler.disconnected(); this.sendToRegisteredServer(server); } else { this.sessionHandler.disconnected(); @@ -257,14 +255,12 @@ public void disconnect() { public void disconnect(RegisteredServer server) { this.connection.eventLoop().execute(() -> { if (this.connection.getActiveSessionHandler() == this.sessionHandler) { - this.sessionHandler.switchDisconnection(() -> { + this.sessionHandler.disconnect(() -> { if (this.plugin.hasLoginQueue(this.player)) { this.sessionHandler.disconnected(); this.plugin.setNextServer(this.player, server); this.plugin.getLoginQueue(this.player).next(); } else { - this.deject(); - this.sessionHandler.disconnected(); this.sendToRegisteredServer(server); } }); @@ -278,15 +274,20 @@ private void deject() { } private void sendToRegisteredServer(RegisteredServer server) { - this.connection.setState(StateRegistry.PLAY); + this.deject(); + this.plugin.setState(this.connection, StateRegistry.PLAY); - // Rollback CONFIG session handler to Velocity one if (this.connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { - this.connection.addSessionHandler(StateRegistry.CONFIG, - new ClientConfigSessionHandler(this.plugin.getServer(), this.player)); + this.sessionHandler.disconnectToConfig(() -> { + // Rollback original CONFIG handler + this.connection.setActiveSessionHandler(StateRegistry.CONFIG, + new ClientConfigSessionHandler(this.plugin.getServer(), this.player)); + this.player.createConnectionRequest(server).fireAndForget(); + }); + } else { + this.sessionHandler.disconnected(); + this.player.createConnectionRequest(server).fireAndForget(); } - - this.player.createConnectionRequest(server).fireAndForget(); } @Override diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 9950d57e..45d306a4 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -26,6 +26,7 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer; import com.velocitypowered.proxy.network.Connections; import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.StateRegistry; import com.velocitypowered.proxy.protocol.packet.KeepAlive; import com.velocitypowered.proxy.protocol.packet.PluginMessage; import com.velocitypowered.proxy.protocol.packet.chat.keyed.KeyedPlayerChat; @@ -34,6 +35,7 @@ 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.config.StartUpdate; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelPipeline; import io.netty.handler.timeout.ReadTimeoutHandler; @@ -50,7 +52,7 @@ import net.elytrium.limboapi.Settings; import net.elytrium.limboapi.api.LimboSessionHandler; import net.elytrium.limboapi.api.player.LimboPlayer; -import net.elytrium.limboapi.injection.login.confirmation.ConfirmHandler; +import net.elytrium.limboapi.injection.login.confirmation.LoginConfirmHandler; import net.elytrium.limboapi.protocol.LimboProtocol; import net.elytrium.limboapi.protocol.packets.c2s.MoveOnGroundOnlyPacket; import net.elytrium.limboapi.protocol.packets.c2s.MovePacket; @@ -66,10 +68,12 @@ public class LimboSessionHandlerImpl implements MinecraftSessionHandler { private final LimboImpl limbo; private final ConnectedPlayer player; private final LimboSessionHandler callback; + private final StateRegistry originalState; private final MinecraftSessionHandler originalHandler; private final RegisteredServer previousServer; private final Supplier limboName; - private final CompletableFuture transition = new CompletableFuture<>(); + private final CompletableFuture playTransition = new CompletableFuture<>(); + private final CompletableFuture configTransition = new CompletableFuture<>(); private LimboPlayer limboPlayer; private ScheduledFuture keepAliveTask; @@ -79,15 +83,17 @@ public class LimboSessionHandlerImpl implements MinecraftSessionHandler { private int ping = -1; private int genericBytes; private boolean loaded; + private boolean switching; private boolean disconnecting; - //private boolean disconnected; - public LimboSessionHandlerImpl(LimboAPI plugin, LimboImpl limbo, ConnectedPlayer player, LimboSessionHandler callback, - MinecraftSessionHandler originalHandler, RegisteredServer previousServer, Supplier limboName) { + public LimboSessionHandlerImpl(LimboAPI plugin, LimboImpl limbo, ConnectedPlayer player, + LimboSessionHandler callback, StateRegistry originalState, MinecraftSessionHandler originalHandler, + RegisteredServer previousServer, Supplier limboName) { this.plugin = plugin; this.limbo = limbo; this.player = player; this.callback = callback; + this.originalState = originalState; this.originalHandler = originalHandler; this.previousServer = previousServer; this.limboName = limboName; @@ -112,7 +118,7 @@ public void onConfig(LimboPlayer player) { } if (this.keepAlivePending) { - connection.closeWith(this.plugin.getPackets().getTimeOut()); + connection.closeWith(this.plugin.getPackets().getTimeOut(this.player.getConnection().getState())); if (Settings.IMP.MAIN.LOGGING_ENABLED) { LimboAPI.getLogger().warn("{} was kicked due to keepalive timeout.", this.player); } @@ -131,11 +137,46 @@ public void onSpawn() { this.callback.onSpawn(this.limbo, this.limboPlayer); // Player is spawned, so can trust that transition to the PLAY state is complete - this.transition.complete(this); + this.playTransition.complete(this); + } + + public void disconnectToConfig(Runnable runnable) { + if (this.configTransition.isDone()) { + runnable.run(); + return; + } + + this.release(); + + this.switching = true; + this.loaded = false; + + this.player.getConnection().write(new StartUpdate()); + this.configTransition.thenRun(this::disconnected).thenRun(runnable); } @Override public boolean handle(FinishedUpdate packet) { + // Switching to the CONFIG state + if (this.player.getConnection().getState() != StateRegistry.CONFIG) { + this.plugin.setActiveSessionHandler(this.player.getConnection(), StateRegistry.CONFIG, this); + + if (!this.loaded && !this.disconnecting) { + this.limbo.spawnPlayerConfirmed(this.callback.getClass(), this, this.player, this.player.getConnection()); + } else if (this.switching) { + this.switching = false; + this.configTransition.complete(this); + } else { + this.player.getConnection().closeWith(this.plugin.getPackets().getInvalidSwitch()); + + if (Settings.IMP.MAIN.LOGGING_ENABLED) { + LimboAPI.getLogger().warn("{} sent an unexpected state switch confirmation.", this.player); + } + } + + return true; + } + this.plugin.setState(this.player.getConnection(), this.limbo.localStateRegistry); this.limbo.onSpawn(this.callback.getClass(), this.player.getConnection(), this.player, this); @@ -290,15 +331,21 @@ private void kickTooBigPacket(String type, int length) { } } - @Override - public void disconnected() { - //this.disconnected = true; + public void release() { if (this.keepAliveTask != null) { this.keepAliveTask.cancel(true); } - this.limbo.onDisconnect(); - this.callback.onDisconnect(); + if (this.loaded) { + this.limbo.onDisconnect(); + this.callback.onDisconnect(); + } + } + + @Override + public void disconnected() { + //this.disconnected = true; + this.release(); if (Settings.IMP.MAIN.LOGGING_ENABLED) { LimboAPI.getLogger().info( @@ -320,9 +367,15 @@ public void disconnected() { if (!(this.originalHandler instanceof AuthSessionHandler) && !(this.originalHandler instanceof LimboSessionHandlerImpl) && !(this.originalHandler instanceof ClientPlaySessionHandler) // cause issues with server switching - && !(this.originalHandler instanceof ConfirmHandler)) { - connection.eventLoop().execute(() -> - this.plugin.setActiveSessionHandler(connection, connection.getState(), this.originalHandler)); + && !(this.originalHandler instanceof LoginConfirmHandler)) { + connection.eventLoop().execute(() -> { + // Ensure that originalHandler is returned to the proper state + if (connection.getState() != this.originalState) { + connection.addSessionHandler(this.originalState, this.originalHandler); + } else { + this.plugin.setActiveSessionHandler(connection, connection.getState(), this.originalHandler); + } + }); } ChannelPipeline pipeline = connection.getChannel().pipeline(); @@ -334,13 +387,13 @@ public void disconnected() { } } - public void switchDisconnection(Runnable runnable) { + public void disconnect(Runnable runnable) { if (!this.disconnecting) { this.disconnecting = true; if (this.player.getConnection().getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { runnable.run(); } else { - this.transition.thenRun(runnable); + this.playTransition.thenRun(runnable); } } } From 777fa694d33ff5a1e8e8ed0f77e1346fac0a8c28 Mon Sep 17 00:00:00 2001 From: UserNugget Date: Sat, 6 Jan 2024 14:31:54 +0300 Subject: [PATCH 13/17] Fix invalid CONFIG packets being sent at PLAY state --- plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java | 7 +++---- .../main/java/net/elytrium/limboapi/server/LimboImpl.java | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java index 40fe8b5c..2aaa4548 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java +++ b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java @@ -427,23 +427,22 @@ public void inject3rdParty(Player player, MinecraftConnection connection, Channe public void setState(MinecraftConnection connection, StateRegistry stateRegistry) { connection.setState(stateRegistry); - this.refresh3rdParty(connection); + this.setEncoderState(connection, stateRegistry); } public void setActiveSessionHandler(MinecraftConnection connection, StateRegistry stateRegistry, MinecraftSessionHandler sessionHandler) { connection.setActiveSessionHandler(stateRegistry, sessionHandler); - this.refresh3rdParty(connection); + this.setEncoderState(connection, stateRegistry); } - public void refresh3rdParty(MinecraftConnection connection) { + public void setEncoderState(MinecraftConnection connection, StateRegistry state) { if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { return; // No need to change the state for a current version } PreparedPacketEncoder encoder = connection.getChannel().pipeline().get(PreparedPacketEncoder.class); if (encoder != null) { - StateRegistry state = connection.getState(); if (state != StateRegistry.CONFIG && state != StateRegistry.LOGIN) { encoder.setFactory(this.preparedPacketFactory); } else { diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 465109aa..1dffee23 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -331,7 +331,12 @@ protected void spawnPlayerConfirmed(Class handler return; } } else { + // Send configuration packets and transition to PLAY state connection.delayedWrite(this.configPackets); + + // As the client still send CONFIG packets but "in PLAY state at protocol level", + // change state to PLAY to ensure that sent packets are not corrupted + this.plugin.setEncoderState(connection, this.localStateRegistry); } } From 9401389633d2af1c0cceed74d6b7103338becb10 Mon Sep 17 00:00:00 2001 From: UserNugget Date: Sat, 6 Jan 2024 15:53:44 +0300 Subject: [PATCH 14/17] Mitigate clientside race condition --- .../server/LimboSessionHandlerImpl.java | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 45d306a4..854c6483 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -58,6 +58,7 @@ import net.elytrium.limboapi.protocol.packets.c2s.MovePacket; import net.elytrium.limboapi.protocol.packets.c2s.MovePositionOnlyPacket; import net.elytrium.limboapi.protocol.packets.c2s.MoveRotationOnlyPacket; +import net.elytrium.limboapi.protocol.packets.c2s.PlayerChatSessionPacket; import net.elytrium.limboapi.protocol.packets.c2s.TeleportConfirmPacket; public class LimboSessionHandlerImpl implements MinecraftSessionHandler { @@ -74,6 +75,7 @@ public class LimboSessionHandlerImpl implements MinecraftSessionHandler { private final Supplier limboName; private final CompletableFuture playTransition = new CompletableFuture<>(); private final CompletableFuture configTransition = new CompletableFuture<>(); + private final CompletableFuture keyFetching = new CompletableFuture<>(); private LimboPlayer limboPlayer; private ScheduledFuture keepAliveTask; @@ -151,8 +153,16 @@ public void disconnectToConfig(Runnable runnable) { this.switching = true; this.loaded = false; - this.player.getConnection().write(new StartUpdate()); - this.configTransition.thenRun(this::disconnected).thenRun(runnable); + if (this.player.isOnlineMode()) { + // Wait for PlayerChatSessionPacket to ensure that client created a chat session and so can't break state switching + this.keyFetching.thenRunAsync(() -> { + this.player.getConnection().write(new StartUpdate()); + this.configTransition.thenRun(this::disconnected).thenRun(runnable); + }, this.player.getConnection().eventLoop()); + } else { + this.player.getConnection().write(new StartUpdate()); + this.configTransition.thenRun(this::disconnected).thenRun(runnable); + } } @Override @@ -308,6 +318,10 @@ public void handleUnknown(ByteBuf packet) { @Override public void handleGeneric(MinecraftPacket packet) { + if (packet instanceof PlayerChatSessionPacket chatSession) { + this.keyFetching.complete(this); + } + if (packet instanceof PluginMessage pluginMessage) { int singleLength = pluginMessage.content().readableBytes() + pluginMessage.getChannel().length() * 4; this.genericBytes += singleLength; From 1b1e679893aaa3c855e7930a13d472698eb2e3b0 Mon Sep 17 00:00:00 2001 From: UserNugget Date: Sat, 6 Jan 2024 17:00:52 +0300 Subject: [PATCH 15/17] Small improvements --- .../java/net/elytrium/limboapi/LimboAPI.java | 3 ++- .../injection/login/LoginListener.java | 27 ++++++++----------- .../injection/login/LoginTasksQueue.java | 2 +- .../confirmation/LoginConfirmHandler.java | 4 +-- .../elytrium/limboapi/server/LimboImpl.java | 19 ++++++------- .../limboapi/server/LimboPlayerImpl.java | 2 +- .../server/LimboSessionHandlerImpl.java | 16 ++++++----- 7 files changed, 36 insertions(+), 37 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java index 2aaa4548..c2d8a594 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java +++ b/plugin/src/main/java/net/elytrium/limboapi/LimboAPI.java @@ -437,8 +437,9 @@ public void setActiveSessionHandler(MinecraftConnection connection, StateRegistr } public void setEncoderState(MinecraftConnection connection, StateRegistry state) { + // As CONFIG state was added in 1.20.2, no need to track it for lower versions if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) < 0) { - return; // No need to change the state for a current version + return; } PreparedPacketEncoder encoder = connection.getChannel().pipeline().get(PreparedPacketEncoder.class); diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java index 99af8c89..a3cec9ef 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginListener.java @@ -240,27 +240,22 @@ public void hookPlaySession(ServerConnectedEvent event) { ConnectedPlayer player = (ConnectedPlayer) event.getPlayer(); MinecraftConnection connection = player.getConnection(); - // No need to do things below as the client should be despawned by default + // 1.20.2+ can ignore this, as it should be despawned by default if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { return; } - // Should never happen with default Velocity - if (!connection.eventLoop().inEventLoop()) { - connection.eventLoop().execute(() -> this.hookPlaySession(event)); - return; - } - - // As Velocity automatically replaces custom handlers with ClientPlaySessionHandler, do it before - if (!(connection.getActiveSessionHandler() instanceof ClientPlaySessionHandler)) { - try { - ClientPlaySessionHandler playHandler = new ClientPlaySessionHandler(this.server, player); - SPAWNED_FIELD.invokeExact(playHandler, this.plugin.isLimboJoined(player)); - connection.setActiveSessionHandler(connection.getState(), playHandler); - } catch (Throwable e) { - throw new ReflectionException(e); + connection.eventLoop().execute(() -> { + if (!(connection.getActiveSessionHandler() instanceof ClientPlaySessionHandler)) { + try { + ClientPlaySessionHandler playHandler = new ClientPlaySessionHandler(this.server, player); + SPAWNED_FIELD.invokeExact(playHandler, this.plugin.isLimboJoined(player)); + connection.setActiveSessionHandler(connection.getState(), playHandler); + } catch (Throwable e) { + throw new ReflectionException(e); + } } - } + }); } static { diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index d8afbc8f..fd6e4011 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -263,7 +263,7 @@ private void connectToServer(Logger logger, ConnectedPlayer player, MinecraftCon throw new ReflectionException(e); } } else if (connection.getState() == StateRegistry.PLAY) { - // Synchronize to make sure that nothing from PLAY state received in CONFIG state + // Synchronize with the client to ensure that it will not corrupt CONFIG state with PLAY packets ((LimboSessionHandlerImpl) connection.getActiveSessionHandler()) .disconnectToConfig(() -> this.connectToServer(logger, player, connection)); diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java index 73622811..c796a844 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/confirmation/LoginConfirmHandler.java @@ -76,7 +76,8 @@ public void waitForConfirmation(Runnable runnable) { try { this.connection.channelRead(ctx, packet); } catch (Throwable throwable) { - LimboAPI.getLogger().error("{}: exception handling exception in {}", ctx.channel().remoteAddress(), this, throwable); + LimboAPI.getLogger().error("{}: exception handling exception in {}", ctx.channel().remoteAddress(), + this.connection.getActiveSessionHandler(), throwable); } } @@ -97,7 +98,6 @@ public boolean handle(LoginAcknowledged packet) { @Override public void handleGeneric(MinecraftPacket packet) { // As Velocity/LimboAPI can easly skip packets due to random delays, packets should be queued - // (executing events or spamming EventLoop::execute cause delays) if (this.connection.getState() == StateRegistry.CONFIG) { this.queuedPackets.add(ReferenceCountUtil.retain(packet)); } diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index 1dffee23..cc26119f 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -319,23 +319,25 @@ public void spawnPlayer(Player apiPlayer, LimboSessionHandler handler) { } } - protected void spawnPlayerConfirmed(Class handlerClass, + protected void spawnPlayerLocal(Class handlerClass, LimboSessionHandlerImpl sessionHandler, ConnectedPlayer player, MinecraftConnection connection) { if (connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { if (connection.getState() != StateRegistry.CONFIG) { if (this.shouldRejoin) { + // Switch to PLAY state connection.write(this.configTransitionPackets); - // Continue state switching on the handler side + // Continue transition on the handler side connection.setActiveSessionHandler(connection.getState(), sessionHandler); return; } } else { - // Send configuration packets and transition to PLAY state + // Switch to PLAY state connection.delayedWrite(this.configPackets); - // As the client still send CONFIG packets but "in PLAY state at protocol level", - // change state to PLAY to ensure that sent packets are not corrupted + // Ensure that encoder will send packets from PLAY state + // Client is not yet switched to the PLAY state, + // but at packet level it's already PLAY state this.plugin.setEncoderState(connection, this.localStateRegistry); } } @@ -422,16 +424,15 @@ private void spawnPlayerLocal(ConnectedPlayer player, LimboSessionHandler handle ); if (connection.getActiveSessionHandler() instanceof LoginConfirmHandler confirm) { - confirm.waitForConfirmation(() -> this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection)); + confirm.waitForConfirmation(() -> this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection)); } else { - this.spawnPlayerConfirmed(handlerClass, sessionHandler, player, connection); + this.spawnPlayerLocal(handlerClass, sessionHandler, player, connection); } }); } protected void onSpawn(Class handlerClass, - MinecraftConnection connection, ConnectedPlayer player, - LimboSessionHandlerImpl sessionHandler) { + MinecraftConnection connection, ConnectedPlayer player, LimboSessionHandlerImpl sessionHandler) { if (this.plugin.isLimboJoined(player)) { if (this.shouldRejoin) { if (connection.getType() == ConnectionTypes.LEGACY_FORGE) { diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java index cd008b24..5ef31e61 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboPlayerImpl.java @@ -275,7 +275,7 @@ private void deject() { private void sendToRegisteredServer(RegisteredServer server) { this.deject(); - this.plugin.setState(this.connection, StateRegistry.PLAY); + this.connection.setState(StateRegistry.PLAY); if (this.connection.getProtocolVersion().compareTo(ProtocolVersion.MINECRAFT_1_20_2) >= 0) { this.sessionHandler.disconnectToConfig(() -> { diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index 854c6483..baa5bb12 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -75,7 +75,7 @@ public class LimboSessionHandlerImpl implements MinecraftSessionHandler { private final Supplier limboName; private final CompletableFuture playTransition = new CompletableFuture<>(); private final CompletableFuture configTransition = new CompletableFuture<>(); - private final CompletableFuture keyFetching = new CompletableFuture<>(); + private final CompletableFuture chatSession = new CompletableFuture<>(); private LimboPlayer limboPlayer; private ScheduledFuture keepAliveTask; @@ -154,8 +154,10 @@ public void disconnectToConfig(Runnable runnable) { this.loaded = false; if (this.player.isOnlineMode()) { - // Wait for PlayerChatSessionPacket to ensure that client created a chat session and so can't break state switching - this.keyFetching.thenRunAsync(() -> { + // As a client sends PlayerChatSessionPacket asynchronously, + // we should wait for it to ensure that it will not be sent + // while switching CONFIG to PLAY state, and so didn't break the connection + this.chatSession.thenRunAsync(() -> { this.player.getConnection().write(new StartUpdate()); this.configTransition.thenRun(this::disconnected).thenRun(runnable); }, this.player.getConnection().eventLoop()); @@ -167,12 +169,12 @@ public void disconnectToConfig(Runnable runnable) { @Override public boolean handle(FinishedUpdate packet) { - // Switching to the CONFIG state + // Switching to CONFIG state if (this.player.getConnection().getState() != StateRegistry.CONFIG) { this.plugin.setActiveSessionHandler(this.player.getConnection(), StateRegistry.CONFIG, this); if (!this.loaded && !this.disconnecting) { - this.limbo.spawnPlayerConfirmed(this.callback.getClass(), this, this.player, this.player.getConnection()); + this.limbo.spawnPlayerLocal(this.callback.getClass(), this, this.player, this.player.getConnection()); } else if (this.switching) { this.switching = false; this.configTransition.complete(this); @@ -318,8 +320,8 @@ public void handleUnknown(ByteBuf packet) { @Override public void handleGeneric(MinecraftPacket packet) { - if (packet instanceof PlayerChatSessionPacket chatSession) { - this.keyFetching.complete(this); + if (packet instanceof PlayerChatSessionPacket) { + this.chatSession.complete(this); } if (packet instanceof PluginMessage pluginMessage) { From 59ea194d83510e6d64229baab26d19489f8f9c3c Mon Sep 17 00:00:00 2001 From: UserNugget Date: Sat, 6 Jan 2024 17:40:26 +0300 Subject: [PATCH 16/17] Fix race condition mitigation then rejoin is disabled --- .../src/main/java/net/elytrium/limboapi/server/LimboImpl.java | 4 ++++ .../net/elytrium/limboapi/server/LimboSessionHandlerImpl.java | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java index cc26119f..7339b78f 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboImpl.java @@ -544,6 +544,10 @@ public Limbo setGameMode(GameMode gameMode) { return this; } + protected boolean isShouldRejoin() { + return this.shouldRejoin; + } + @Override public Limbo setShouldRejoin(boolean shouldRejoin) { this.shouldRejoin = shouldRejoin; diff --git a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java index baa5bb12..8b26d5ad 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java +++ b/plugin/src/main/java/net/elytrium/limboapi/server/LimboSessionHandlerImpl.java @@ -153,7 +153,7 @@ public void disconnectToConfig(Runnable runnable) { this.switching = true; this.loaded = false; - if (this.player.isOnlineMode()) { + if (this.player.isOnlineMode() && this.limbo.isShouldRejoin()) { // As a client sends PlayerChatSessionPacket asynchronously, // we should wait for it to ensure that it will not be sent // while switching CONFIG to PLAY state, and so didn't break the connection From 36bc4d6645dff07f17594579a59068cdde27114a Mon Sep 17 00:00:00 2001 From: UserNugget Date: Fri, 12 Jan 2024 01:42:15 +0300 Subject: [PATCH 17/17] Fix LoginEvent sending a LOGIN disconnect instead of CONFIG/PLAY one --- .../elytrium/limboapi/injection/login/LoginTasksQueue.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java index fd6e4011..579e433d 100644 --- a/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java +++ b/plugin/src/main/java/net/elytrium/limboapi/injection/login/LoginTasksQueue.java @@ -234,7 +234,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { } else { Optional reason = event.getResult().getReasonComponent(); if (reason.isPresent()) { - this.player.disconnect0(reason.get(), true); + this.player.disconnect0(reason.get(), false); } else { if (this.server.registerConnection(this.player)) { if (connection.getActiveSessionHandler() instanceof LoginConfirmHandler confirm) { @@ -243,7 +243,7 @@ private void initialize(MinecraftConnection connection) throws Throwable { this.connectToServer(logger, this.player, connection); } } else { - this.player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), true); + this.player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), false); } } }