From 0e75efcb6799d558b62cb961e9e8334843595d17 Mon Sep 17 00:00:00 2001 From: ssquadteam Date: Sun, 16 Aug 2026 07:36:43 +0000 Subject: [PATCH 1/4] Implement client world preservation handling and related utilities --- .../api/player/ClientWorldSwitches.java | 108 ++++++++++++++++++ .../client/ClientPlaySessionHandler.java | 29 +++-- .../connection/client/ConnectedPlayer.java | 2 + 3 files changed, 129 insertions(+), 10 deletions(-) create mode 100644 api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java diff --git a/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java b/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java new file mode 100644 index 000000000..61ba839ba --- /dev/null +++ b/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2026 Velocity-CTD Contributors + * + * The Velocity API is licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in the api top-level directory. + */ + +package com.velocityctd.api.player; + +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Coordinates an explicitly requested client-world-preserving server switch. + * + *

A coordinating proxy plugin requests preservation immediately before it connects a player + * to its staged destination. ApiaryProxy consumes that one request when the destination sends its + * join-game packet. Requests expire quickly so a failed connection cannot affect a later, normal + * server switch. + */ +public final class ClientWorldSwitches { + + private static final long REQUEST_TTL_MILLIS = 5_000L; + private static final ConcurrentHashMap CLIENT_ENTITY_IDS = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap PRESERVATION_REQUESTS = new ConcurrentHashMap<>(); + + private ClientWorldSwitches() { + } + + /** + * Returns the entity ID the client currently uses for {@code playerId}, or {@code 0} when the + * player has not finished their initial join. + * + * @param playerId the player whose client entity ID is needed + * @return the client-visible entity ID, or {@code 0} + */ + public static int clientEntityId(UUID playerId) { + return CLIENT_ENTITY_IDS.getOrDefault(playerId, 0); + } + + /** + * Requests that the next destination join for {@code playerId} retain the client's loaded world. + * + *

The request only takes effect when ApiaryProxy's world-preservation setting is enabled and + * the destination reports the same dimension. It intentionally does not apply to ordinary + * server switches. + * + * @param playerId the player being transferred + * @return {@code true} when a request was recorded; {@code false} before the initial join + */ + public static boolean requestWorldPreservation(UUID playerId) { + if (clientEntityId(playerId) <= 0) { + return false; + } + + PRESERVATION_REQUESTS.put(playerId, System.currentTimeMillis() + REQUEST_TTL_MILLIS); + return true; + } + + /** + * Cancels a previously requested world-preserving switch. + * + * @param playerId the player whose pending request should be cancelled + */ + public static void cancelWorldPreservation(UUID playerId) { + PRESERVATION_REQUESTS.remove(playerId); + } + + /** + * Records the entity ID most recently presented to a client. + * + *

This method is used by ApiaryProxy's connection implementation. Coordinating plugins + * should use {@link #clientEntityId(UUID)} instead. + * + * @param playerId the client that received the ID + * @param entityId the entity ID from its join-game packet + */ + public static void rememberClientEntityId(UUID playerId, int entityId) { + if (entityId > 0) { + CLIENT_ENTITY_IDS.put(playerId, entityId); + } + } + + /** + * Clears all switch state for a disconnected player. + * + *

This method is used by ApiaryProxy's connection implementation. + * + * @param playerId the disconnected player + */ + public static void forget(UUID playerId) { + CLIENT_ENTITY_IDS.remove(playerId); + PRESERVATION_REQUESTS.remove(playerId); + } + + /** + * Consumes a pending request if it has not expired. + * + *

This method is used by ApiaryProxy's connection implementation. + * + * @param playerId the player whose request should be consumed + * @return whether a live request existed + */ + public static boolean consumeWorldPreservation(UUID playerId) { + Long deadline = PRESERVATION_REQUESTS.remove(playerId); + return deadline != null && System.currentTimeMillis() <= deadline; + } +} diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 864240c44..0d73b5696 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -22,6 +22,7 @@ import com.google.common.collect.ImmutableList; import com.mojang.brigadier.suggestion.Suggestion; import com.velocityctd.api.event.player.TabCompleteRequestEvent; +import com.velocityctd.api.player.ClientWorldSwitches; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.player.CookieReceiveEvent; import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent; @@ -671,6 +672,8 @@ public CompletableFuture doSwitch() { */ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnection destination) { MinecraftConnection serverMc = destination.ensureConnected(); + boolean worldPreservationRequested = ClientWorldSwitches.consumeWorldPreservation( + player.getUniqueId()); if (!spawned) { // The player wasn't spawned in yet, so we don't need to do anything special. @@ -680,10 +683,10 @@ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnect // Required for Legacy Forge player.getPhase().onFirstJoin(player); rememberClientWorld(joinGame); - } else if (canKeepClientWorld(joinGame)) { - // The destination reuses the entity id and dimension the client already has, so the client - // does not need to rebuild its level. Withholding the join game and respawn packets is what - // keeps the terrain loading screen from appearing. + } else if (canKeepClientWorld(joinGame, worldPreservationRequested)) { + // The destination can preserve the dimension the client already has, so the client does not + // need to rebuild its level. Withholding the join game and respawn packets is what keeps the + // terrain loading screen from appearing. player.getTabList().clearAll(); // Because the client never receives a join game, it never reports that it finished loading @@ -785,6 +788,7 @@ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnect private void rememberClientWorld(JoinGamePacket joinGame) { clientEntityId = joinGame.getEntityId(); clientDimension = dimensionKey(joinGame); + ClientWorldSwitches.rememberClientEntityId(player.getUniqueId(), clientEntityId); } private static @Nullable String dimensionKey(JoinGamePacket joinGame) { @@ -799,12 +803,12 @@ private void rememberClientWorld(JoinGamePacket joinGame) { /** * Decides whether the client can stay in the world it already has for this switch. * - *

Both the entity id and the dimension have to match what the client was last told. A backend - * that reuses the entity id is what makes this safe: the client keeps addressing its own entity - * by the same id the destination uses, so no packet rewriting is needed. Anything else falls back - * to the regular switch, which costs a loading screen but is always correct. + *

The dimension always has to match what the client was last told. Ordinary switches must also + * retain the entity id. A one-shot coordinated request may preserve the world with a different + * backend entity id; only a staging-aware proxy plugin can create that request. Other switches + * fall back to the regular, visible transition. */ - private boolean canKeepClientWorld(JoinGamePacket joinGame) { + private boolean canKeepClientWorld(JoinGamePacket joinGame, boolean worldPreservationRequested) { if (!server.getConfiguration().isKeepClientWorldOnSwitch()) { return false; } @@ -814,12 +818,17 @@ private boolean canKeepClientWorld(JoinGamePacket joinGame) { return false; } - if (joinGame.getEntityId() != clientEntityId) { + if (!worldPreservationRequested && joinGame.getEntityId() != clientEntityId) { LOGGER.debug("Not keeping the world for {}: destination assigned entity id {}, client has {}", player, joinGame.getEntityId(), clientEntityId); return false; } + if (worldPreservationRequested && joinGame.getEntityId() != clientEntityId) { + LOGGER.debug("Keeping the world for {} with the coordinated entity-id bridge: destination assigned {}, " + + "client keeps {}", player, joinGame.getEntityId(), clientEntityId); + } + String dimension = dimensionKey(joinGame); if (!Objects.equals(dimension, clientDimension)) { LOGGER.debug("Not keeping the world for {}: destination dimension {} differs from {}", diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index e491083ca..6355a7b29 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -17,6 +17,7 @@ package com.velocitypowered.proxy.connection.client; +import com.velocityctd.api.player.ClientWorldSwitches; import static com.velocityctd.proxy.permission.PermissionResolverAdapterFactory.createPermissionResolverAdapter; import static com.velocitypowered.api.proxy.ConnectionRequestBuilder.Status.ALREADY_CONNECTED; import static com.velocitypowered.proxy.connection.PlayerDataForwarding.LEGACY_MODERN_FORWARDING; @@ -1478,6 +1479,7 @@ void teardown() { connectedServer.disconnect(); } + ClientWorldSwitches.forget(getUniqueId()); server.getPlayerRegistry().unregisterConnection(this); } From ed60a4fe7fb866f54de0ff5303e1d3a10dfb79be Mon Sep 17 00:00:00 2001 From: ssquadteam Date: Sun, 16 Aug 2026 08:22:10 +0000 Subject: [PATCH 2/4] Add ClientboundSetPassengersPacket and handle passenger entity ID replacement --- .../api/player/ClientWorldSwitches.java | 7 +-- .../connection/MinecraftSessionHandler.java | 5 ++ .../backend/BackendPlaySessionHandler.java | 13 +++++ .../proxy/protocol/StateRegistry.java | 6 ++ .../ClientboundSetPassengersPacket.java | 58 +++++++++++++++++++ 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java diff --git a/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java b/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java index 61ba839ba..2f175315a 100644 --- a/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java +++ b/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java @@ -13,10 +13,9 @@ /** * Coordinates an explicitly requested client-world-preserving server switch. * - *

A coordinating proxy plugin requests preservation immediately before it connects a player - * to its staged destination. ApiaryProxy consumes that one request when the destination sends its - * join-game packet. Requests expire quickly so a failed connection cannot affect a later, normal - * server switch. + *

A coordinating proxy plugin requests preservation before it starts staging a destination. + * ApiaryProxy consumes that one request when the destination sends its join-game packet. Requests + * expire quickly so a failed connection cannot affect a later, normal server switch. */ public final class ClientWorldSwitches { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java index 0b4043f4d..3e9ca9873 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/MinecraftSessionHandler.java @@ -23,6 +23,7 @@ import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundSetPassengersPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; @@ -362,6 +363,10 @@ default boolean handle(ClientboundCookieRequestPacket packet) { return false; } + default boolean handle(ClientboundSetPassengersPacket packet) { + return false; + } + default boolean handle(ServerboundCookieResponsePacket packet) { return false; } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index 0b0440c27..b0d7e79e1 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -21,6 +21,7 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; +import com.velocityctd.api.player.ClientWorldSwitches; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; @@ -49,6 +50,7 @@ import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundSetPassengersPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; @@ -484,6 +486,17 @@ public boolean handle(ClientboundCookieRequestPacket packet) { return true; } + @Override + public boolean handle(ClientboundSetPassengersPacket packet) { + Integer backendEntityId = serverConn.getEntityId(); + int clientEntityId = ClientWorldSwitches.clientEntityId(serverConn.getPlayer().getUniqueId()); + if (backendEntityId != null && clientEntityId > 0 && backendEntityId != clientEntityId) { + packet.replacePassengerEntityId(backendEntityId, clientEntityId); + } + handleGeneric(packet); + return true; + } + @Override public void handleGeneric(MinecraftPacket packet) { if (packet instanceof PluginMessagePacket pluginMessage) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java index c46e33767..e55b6d46c 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java @@ -47,6 +47,7 @@ import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_1_9_4; import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_1; +import static com.velocitypowered.api.network.ProtocolVersion.MINECRAFT_26_2; import static com.velocitypowered.api.network.ProtocolVersion.MINIMUM_VERSION; import static com.velocitypowered.api.network.ProtocolVersion.SUPPORTED_VERSIONS; import static com.velocitypowered.proxy.connection.PlayerDataForwarding.LEGACY_MODERN_FORWARDING; @@ -61,6 +62,7 @@ import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; +import com.velocitypowered.proxy.protocol.packet.ClientboundSetPassengersPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundSoundEntityPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStopSoundPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; @@ -890,6 +892,10 @@ public enum StateRegistry { map(0x66, MINECRAFT_1_21_5, false), map(0x6B, MINECRAFT_1_21_9, false), map(0x6D, MINECRAFT_26_1, false)); + clientbound.register( + ClientboundSetPassengersPacket.class, + ClientboundSetPassengersPacket::new, + map(0x6B, MINECRAFT_26_2, false)); } }, diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java new file mode 100644 index 000000000..92ac4d135 --- /dev/null +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2018-2026 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.velocitypowered.proxy.protocol.packet; + +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.proxy.connection.MinecraftSessionHandler; +import com.velocitypowered.proxy.protocol.MinecraftPacket; +import com.velocitypowered.proxy.protocol.ProtocolUtils; +import io.netty.buffer.ByteBuf; + +public final class ClientboundSetPassengersPacket implements MinecraftPacket { + + private int vehicleEntityId; + private int[] passengerEntityIds; + + public ClientboundSetPassengersPacket() { + } + + @Override + public void decode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + vehicleEntityId = ProtocolUtils.readVarInt(buf); + passengerEntityIds = ProtocolUtils.readVarIntArray(buf); + } + + @Override + public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersion protocolVersion) { + ProtocolUtils.writeVarInt(buf, vehicleEntityId); + ProtocolUtils.writeVarIntArray(buf, passengerEntityIds); + } + + @Override + public boolean handle(MinecraftSessionHandler handler) { + return handler.handle(this); + } + + public void replacePassengerEntityId(int expectedEntityId, int replacementEntityId) { + for (int index = 0; index < passengerEntityIds.length; index++) { + if (passengerEntityIds[index] == expectedEntityId) { + passengerEntityIds[index] = replacementEntityId; + } + } + } +} From 7eb6eba5262aef963c6ee1c75b7789b824b1a335 Mon Sep 17 00:00:00 2001 From: ssquadteam Date: Sun, 16 Aug 2026 11:58:44 +0000 Subject: [PATCH 3/4] Refactor world preservation handling by removing unused methods and simplifying entity ID management --- .../api/player/ClientWorldSwitches.java | 51 +------------------ .../backend/BackendPlaySessionHandler.java | 13 ----- .../client/ClientPlaySessionHandler.java | 21 +++----- .../ClientboundSetPassengersPacket.java | 8 --- 4 files changed, 9 insertions(+), 84 deletions(-) diff --git a/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java b/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java index 2f175315a..3d91dbe32 100644 --- a/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java +++ b/api/src/main/java/com/velocityctd/api/player/ClientWorldSwitches.java @@ -11,17 +11,12 @@ import java.util.concurrent.ConcurrentHashMap; /** - * Coordinates an explicitly requested client-world-preserving server switch. - * - *

A coordinating proxy plugin requests preservation before it starts staging a destination. - * ApiaryProxy consumes that one request when the destination sends its join-game packet. Requests - * expire quickly so a failed connection cannot affect a later, normal server switch. + * Tracks the entity ID the client currently uses for its own player, so a coordinating proxy + * plugin can pass it to the destination server for a seamless, world-preserving switch. */ public final class ClientWorldSwitches { - private static final long REQUEST_TTL_MILLIS = 5_000L; private static final ConcurrentHashMap CLIENT_ENTITY_IDS = new ConcurrentHashMap<>(); - private static final ConcurrentHashMap PRESERVATION_REQUESTS = new ConcurrentHashMap<>(); private ClientWorldSwitches() { } @@ -37,34 +32,6 @@ public static int clientEntityId(UUID playerId) { return CLIENT_ENTITY_IDS.getOrDefault(playerId, 0); } - /** - * Requests that the next destination join for {@code playerId} retain the client's loaded world. - * - *

The request only takes effect when ApiaryProxy's world-preservation setting is enabled and - * the destination reports the same dimension. It intentionally does not apply to ordinary - * server switches. - * - * @param playerId the player being transferred - * @return {@code true} when a request was recorded; {@code false} before the initial join - */ - public static boolean requestWorldPreservation(UUID playerId) { - if (clientEntityId(playerId) <= 0) { - return false; - } - - PRESERVATION_REQUESTS.put(playerId, System.currentTimeMillis() + REQUEST_TTL_MILLIS); - return true; - } - - /** - * Cancels a previously requested world-preserving switch. - * - * @param playerId the player whose pending request should be cancelled - */ - public static void cancelWorldPreservation(UUID playerId) { - PRESERVATION_REQUESTS.remove(playerId); - } - /** * Records the entity ID most recently presented to a client. * @@ -89,19 +56,5 @@ public static void rememberClientEntityId(UUID playerId, int entityId) { */ public static void forget(UUID playerId) { CLIENT_ENTITY_IDS.remove(playerId); - PRESERVATION_REQUESTS.remove(playerId); - } - - /** - * Consumes a pending request if it has not expired. - * - *

This method is used by ApiaryProxy's connection implementation. - * - * @param playerId the player whose request should be consumed - * @return whether a live request existed - */ - public static boolean consumeWorldPreservation(UUID playerId) { - Long deadline = PRESERVATION_REQUESTS.remove(playerId); - return deadline != null && System.currentTimeMillis() <= deadline; } } diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java index b0d7e79e1..0b0440c27 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/BackendPlaySessionHandler.java @@ -21,7 +21,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; -import com.velocityctd.api.player.ClientWorldSwitches; import com.velocitypowered.api.event.connection.PluginMessageEvent; import com.velocitypowered.api.event.connection.PreTransferEvent; import com.velocitypowered.api.event.player.CookieRequestEvent; @@ -50,7 +49,6 @@ import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket; import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket; -import com.velocitypowered.proxy.protocol.packet.ClientboundSetPassengersPacket; import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket; import com.velocitypowered.proxy.protocol.packet.DisconnectPacket; import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket; @@ -486,17 +484,6 @@ public boolean handle(ClientboundCookieRequestPacket packet) { return true; } - @Override - public boolean handle(ClientboundSetPassengersPacket packet) { - Integer backendEntityId = serverConn.getEntityId(); - int clientEntityId = ClientWorldSwitches.clientEntityId(serverConn.getPlayer().getUniqueId()); - if (backendEntityId != null && clientEntityId > 0 && backendEntityId != clientEntityId) { - packet.replacePassengerEntityId(backendEntityId, clientEntityId); - } - handleGeneric(packet); - return true; - } - @Override public void handleGeneric(MinecraftPacket packet) { if (packet instanceof PluginMessagePacket pluginMessage) { diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java index 0d73b5696..be03fd9f0 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ClientPlaySessionHandler.java @@ -672,8 +672,6 @@ public CompletableFuture doSwitch() { */ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnection destination) { MinecraftConnection serverMc = destination.ensureConnected(); - boolean worldPreservationRequested = ClientWorldSwitches.consumeWorldPreservation( - player.getUniqueId()); if (!spawned) { // The player wasn't spawned in yet, so we don't need to do anything special. @@ -683,7 +681,7 @@ public void handleBackendJoinGame(JoinGamePacket joinGame, VelocityServerConnect // Required for Legacy Forge player.getPhase().onFirstJoin(player); rememberClientWorld(joinGame); - } else if (canKeepClientWorld(joinGame, worldPreservationRequested)) { + } else if (canKeepClientWorld(joinGame)) { // The destination can preserve the dimension the client already has, so the client does not // need to rebuild its level. Withholding the join game and respawn packets is what keeps the // terrain loading screen from appearing. @@ -803,12 +801,12 @@ private void rememberClientWorld(JoinGamePacket joinGame) { /** * Decides whether the client can stay in the world it already has for this switch. * - *

The dimension always has to match what the client was last told. Ordinary switches must also - * retain the entity id. A one-shot coordinated request may preserve the world with a different - * backend entity id; only a staging-aware proxy plugin can create that request. Other switches - * fall back to the regular, visible transition. + *

The dimension has to match what the client was last told, and the destination has to assign + * the entity id the client already has for the player. The destination now assigns that id itself + * via Paper's internal entity-id API, so the proxy no longer rewrites packets for a preserved + * world. */ - private boolean canKeepClientWorld(JoinGamePacket joinGame, boolean worldPreservationRequested) { + private boolean canKeepClientWorld(JoinGamePacket joinGame) { if (!server.getConfiguration().isKeepClientWorldOnSwitch()) { return false; } @@ -818,17 +816,12 @@ private boolean canKeepClientWorld(JoinGamePacket joinGame, boolean worldPreserv return false; } - if (!worldPreservationRequested && joinGame.getEntityId() != clientEntityId) { + if (joinGame.getEntityId() != clientEntityId) { LOGGER.debug("Not keeping the world for {}: destination assigned entity id {}, client has {}", player, joinGame.getEntityId(), clientEntityId); return false; } - if (worldPreservationRequested && joinGame.getEntityId() != clientEntityId) { - LOGGER.debug("Keeping the world for {} with the coordinated entity-id bridge: destination assigned {}, " - + "client keeps {}", player, joinGame.getEntityId(), clientEntityId); - } - String dimension = dimensionKey(joinGame); if (!Objects.equals(dimension, clientDimension)) { LOGGER.debug("Not keeping the world for {}: destination dimension {} differs from {}", diff --git a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java index 92ac4d135..e1beeffb4 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/protocol/packet/ClientboundSetPassengersPacket.java @@ -47,12 +47,4 @@ public void encode(ByteBuf buf, ProtocolUtils.Direction direction, ProtocolVersi public boolean handle(MinecraftSessionHandler handler) { return handler.handle(this); } - - public void replacePassengerEntityId(int expectedEntityId, int replacementEntityId) { - for (int index = 0; index < passengerEntityIds.length; index++) { - if (passengerEntityIds[index] == expectedEntityId) { - passengerEntityIds[index] = replacementEntityId; - } - } - } } From ba864f2dda245a80130d9720b6466035df69704d Mon Sep 17 00:00:00 2001 From: ssquadteam Date: Sun, 16 Aug 2026 12:39:47 +0000 Subject: [PATCH 4/4] Fix import order by moving ClientWorldSwitches import to the correct position --- .../proxy/connection/client/ConnectedPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java index 6355a7b29..ad1fdb8ad 100644 --- a/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java +++ b/proxy/src/main/java/com/velocitypowered/proxy/connection/client/ConnectedPlayer.java @@ -17,7 +17,6 @@ package com.velocitypowered.proxy.connection.client; -import com.velocityctd.api.player.ClientWorldSwitches; import static com.velocityctd.proxy.permission.PermissionResolverAdapterFactory.createPermissionResolverAdapter; import static com.velocitypowered.api.proxy.ConnectionRequestBuilder.Status.ALREADY_CONNECTED; import static com.velocitypowered.proxy.connection.PlayerDataForwarding.LEGACY_MODERN_FORWARDING; @@ -32,6 +31,7 @@ import com.mojang.brigadier.tree.RootCommandNode; import com.velocityctd.api.event.permission.PermissionsChangeEvent; import com.velocityctd.api.permission.PermissionResolver; +import com.velocityctd.api.player.ClientWorldSwitches; import com.velocityctd.api.queue.QueueState; import com.velocityctd.proxy.permission.PermissionUtils; import com.velocityctd.proxy.queue.VelocityQueue;