Skip to content

Fix inventory desync for <1.17 clients on 1.17+ servers#1243

Open
JobsonMarinho wants to merge 1 commit intoViaVersion:masterfrom
JobsonMarinho:fix/inventory-desync-pre-1.17
Open

Fix inventory desync for <1.17 clients on 1.17+ servers#1243
JobsonMarinho wants to merge 1 commit intoViaVersion:masterfrom
JobsonMarinho:fix/inventory-desync-pre-1.17

Conversation

@JobsonMarinho
Copy link
Copy Markdown

Summary

Fixes the known issue where <1.17 clients on 1.17+ servers experience inventory desyncs on certain inventory click actions, most notably when the server cancels an InventoryClickEvent.

The problem

When a <1.16.5 client clicks an item in an inventory and the server cancels the click (e.g. a plugin GUI), the clicked item visually disappears from the slot. Clicking another item makes the first one reappear, but the newly clicked item disappears — creating a persistent desync loop. This does not affect 1.17+ clients.

Root cause analysis

The desync is caused by three separate issues in the 1.17→1.16.4 and 1.17.1→1.17 protocol translation:

1. Missing CONTAINER_SET_CONTENT item remapping (1.17→1.16.4)

BlockItemPacketRewriter1_17 had no handler registered for CONTAINER_SET_CONTENT. The packet passed through without item ID remapping, causing items to show up with wrong IDs (or as air) on <1.17 clients when the server sent inventory resyncs.

Fix: Register a replaceClientbound handler that reads the item array, applies handleItemToClient to each item, and writes it back.

2. Carried item stripped but never forwarded (1.17.1→1.17)

In 1.17.1+, CONTAINER_SET_CONTENT includes a carried item (cursor) field. The 1.17.1→1.17 handler correctly strips this field (since 1.17.0 doesn't have it) and stores it in PlayerLastCursorItem, but never sends it to the client. This means the <1.17 client's cursor is never updated when the server resyncs the inventory — causing ghost items on the cursor after cancelled clicks, which led to item duplication on subsequent clicks.

Fix: After reading the carried item, create a CONTAINER_SET_SLOT packet (windowId=-1, slot=-1) containing the carried item and schedule it via scheduleSend. This packet goes through the 1.17→1.16.4 handler for proper item remapping before reaching the client.

3. Empty changedSlots array in CONTAINER_CLICK translation (1.17→1.16.4)

The serverbound CONTAINER_CLICK was translated with changedSlots = [] (empty array). In 1.17+, the server uses this array to compare the client's predicted inventory state against the actual state and sends corrections for any mismatches. With an empty array, the server assumed no slots were modified and skipped sending corrections entirely — even for cancelled events.

This was the primary cause of the "item disappears" bug: the client applied its click prediction locally (removing the item from the slot), but the server never sent a correction because it didn't know the client had changed anything.

Fix (PICKUP mode, mode 0): Include the clicked slot in the changedSlots array with a null predicted value (item picked up to cursor). When the event is cancelled, the server detects the mismatch (client says empty, server has the item) and sends a CONTAINER_SET_SLOT correction.

Fix (non-PICKUP modes — shift-click, swap, throw, drag, etc.): These modes affect multiple slots that are difficult to predict without replicating vanilla logic. Instead, force a state ID mismatch by invalidating the stored state ID in InventoryStateIds. This causes the server to detect a global desync and send a full CONTAINER_SET_CONTENT resync, which correctly restores all affected slots.

Testing

Tested on a AdvancedSlimePaper/ASP (Paper fork) 1.21.11 server with a 1.8.9 client (via ViaVersion + ViaBackwards + ViaRewind):

  • Left-click on cancelled InventoryClickEvent → item no longer disappears ✅
  • Shift-click on cancelled InventoryClickEvent → item no longer desyncs ✅
  • Cursor state properly synced after cancelled clicks (no more ghost items or duplication) ✅
  • Normal inventory operations (moving items, crafting, etc.) continue to work correctly ✅

Files changed

  • Protocol1_17_1To1_17.java — Forward carried item as CONTAINER_SET_SLOT in CONTAINER_SET_CONTENT handler
  • BlockItemPacketRewriter1_17.java — Add CONTAINER_SET_CONTENT item remapping, populate changedSlots array, and force state ID mismatch for complex click modes

@toidicakhia
Copy link
Copy Markdown

Can you record your test?

@JobsonMarinho
Copy link
Copy Markdown
Author

2026-04-07.23-31-56.mp4

Hi @toidicakhia I’ve recorded the test and attached the video.

However, due to confidentiality agreements with the company I work for, I’m not allowed to show my full screen. For that reason, the recording is slightly cropped to focus only on the relevant part.

Please let me know if that works for you or if you’d like me to adjust anything.

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.
@JobsonMarinho JobsonMarinho force-pushed the fix/inventory-desync-pre-1.17 branch from 520eb91 to c18612b Compare April 8, 2026 09:05
@JobsonMarinho JobsonMarinho requested a review from kennytv April 8, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants