From c18612bc13eb02ec307b2d270e0e1c40c40830b2 Mon Sep 17 00:00:00 2001 From: Jobs Date: Tue, 7 Apr 2026 22:28:41 -0300 Subject: [PATCH] Fix inventory desync for <1.17 clients on 1.17+ servers This addresses the known issue where <1.17 clients experience inventory desyncs on certain inventory click actions when connected to 1.17+ servers. The root cause is threefold: 1. Carried item not forwarded from CONTAINER_SET_CONTENT (1.17.1->1.17) In 1.17.1+, CONTAINER_SET_CONTENT includes a carried/cursor item field that doesn't exist in <1.17 formats. The handler stripped this field and stored it internally, but never forwarded it to the client. This caused the cursor to remain desynced after cancelled clicks. Fix: Send the carried item as a separate CONTAINER_SET_SLOT packet (windowId=-1, slot=-1) so the client's cursor state is properly synced. 2. Empty changedSlots array in CONTAINER_CLICK translation (1.17->1.16.4) The serverbound CONTAINER_CLICK was translated with an empty changedSlots array. The server uses this to detect client-server desync and send corrections. With an empty array, the server skipped sending corrections for cancelled InventoryClickEvents. Fix: For PICKUP mode (mode 0), include the clicked slot with a null predicted value so the server detects the desync and sends a SET_SLOT correction. For non-PICKUP modes (shift-click, swap, throw, drag, etc.) which affect multiple unpredictable slots, force a state ID mismatch to trigger a full CONTAINER_SET_CONTENT resync. --- .../v1_17_1to1_17/Protocol1_17_1To1_17.java | 12 +++++++++- .../rewriter/BlockItemPacketRewriter1_17.java | 23 +++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17_1to1_17/Protocol1_17_1To1_17.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17_1to1_17/Protocol1_17_1To1_17.java index 92040ff2..b7a1dfbc 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17_1to1_17/Protocol1_17_1To1_17.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17_1to1_17/Protocol1_17_1To1_17.java @@ -71,7 +71,7 @@ protected void registerPackets() { // Length is encoded as a var int in 1.17.1 wrapper.write(Types.ITEM1_13_2_SHORT_ARRAY, wrapper.read(Types.ITEM1_13_2_ARRAY)); - // Carried item - should work without adding it to the array above + // Carried item - forward as CONTAINER_SET_SLOT for <1.17 clients Item carried = wrapper.read(Types.ITEM1_13_2); PlayerLastCursorItem lastCursorItem = wrapper.user().get(PlayerLastCursorItem.class); @@ -82,6 +82,16 @@ protected void registerPackets() { // for a subsequent drag lastCursorItem.setLastCursorItem(carried); + + // In 1.17.1+, the carried/cursor item is part of CONTAINER_SET_CONTENT, + // but <1.17 clients don't have this field. Send it as a separate + // CONTAINER_SET_SLOT packet so the client's cursor state is properly synced. + // This fixes inventory desyncs when the server cancels InventoryClickEvents. + PacketWrapper cursorPacket = wrapper.create(ClientboundPackets1_17.CONTAINER_SET_SLOT); + cursorPacket.write(Types.BYTE, (byte) -1); // Window ID: -1 for cursor + cursorPacket.write(Types.SHORT, (short) -1); // Slot: -1 for cursor + cursorPacket.write(Types.ITEM1_13_2, carried); + cursorPacket.send(Protocol1_17_1To1_17.class); } }); diff --git a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17to1_16_4/rewriter/BlockItemPacketRewriter1_17.java b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17to1_16_4/rewriter/BlockItemPacketRewriter1_17.java index 266defe7..5b2c7215 100644 --- a/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17to1_16_4/rewriter/BlockItemPacketRewriter1_17.java +++ b/common/src/main/java/com/viaversion/viabackwards/protocol/v1_17to1_16_4/rewriter/BlockItemPacketRewriter1_17.java @@ -26,6 +26,7 @@ import com.viaversion.viabackwards.api.rewriters.MapColorRewriter; import com.viaversion.viabackwards.protocol.v1_17to1_16_4.Protocol1_17To1_16_4; import com.viaversion.viabackwards.protocol.v1_17to1_16_4.data.MapColorMappings1_16_4; +import com.viaversion.viabackwards.protocol.v1_17_1to1_17.storage.InventoryStateIds; import com.viaversion.viabackwards.protocol.v1_17to1_16_4.storage.PlayerLastCursorItem; import com.viaversion.viaversion.api.data.entity.EntityTracker; import com.viaversion.viaversion.api.minecraft.BlockChangeRecord; @@ -83,8 +84,26 @@ public void register() { int mode = wrapper.passthrough(Types.VAR_INT); // Mode Item clicked = handleItemToServer(wrapper.user(), wrapper.read(Types.ITEM1_13_2)); // Clicked item - // The 1.17 client would check the entire inventory for changes before -> after a click and send the changed slots here - wrapper.write(Types.VAR_INT, 0); // Empty array of slot+item + // The 1.17 client would check the entire inventory for changes before -> after a click + // and send the changed slots here. We include the clicked slot so the server + // detects any desync (e.g. cancelled InventoryClickEvent) and sends corrections. + if (slot >= 0 && clicked != null) { + wrapper.write(Types.VAR_INT, 1); // One modified slot + wrapper.write(Types.SHORT, slot); // The clicked slot + wrapper.write(Types.ITEM1_13_2, (Item) null); // Predicted: empty (item picked up) + } else { + wrapper.write(Types.VAR_INT, 0); // Empty array of slot+item + } + + // Non-PICKUP modes (shift-click, swap, throw, drag, etc.) affect multiple slots + // that we can't easily predict. Force a state ID mismatch so the server + // sends a full inventory resync instead of individual slot corrections. + if (mode != 0) { + InventoryStateIds stateIds = wrapper.user().get(InventoryStateIds.class); + if (stateIds != null) { + stateIds.setStateId(wrapper.get(Types.BYTE, 0), -1); + } + } PlayerLastCursorItem state = wrapper.user().get(PlayerLastCursorItem.class); if (mode == 0 && button == 0 && clicked != null) {