ITADN

Fix Report: Inventory Visual Sync Bugs — Hand Items, Equipment & UI

#879OpenBrazwed 创建于 2026-05-03
B
Brazwedcommented
# Fix Report: Inventory Visual Sync Bugs ## Date: 02/05/2026 ## Author: NicoxBr ## Project: AllayMC --- ## Executive Summary Multiple bugs related to visual synchronization of hand items and equipment for other players were identified and fixed. The issues manifested in three main scenarios: 1. **Item Drop** - Player dropped the hand item but still appeared to be holding it visually 2. **Item Pickup** - Player picked up an item but others couldn't see it in their hand 3. **Login/Logout** - When reconnecting with items in hand/equipment, other players couldn't see them --- ## Identified and Fixed Issues ### ✅ ISSUE 1: Hand item doesn't update after drop (Q key) **Symptom**: Player drops an item from the hotbar, the item is dropped on the ground but visually other players still saw the player holding the item. **Root Cause**: The `notifyItemInHandChange()` method was not calling `viewEntityHand()` to notify other viewers about the hand change via `MobEquipmentPacket`. **Fixed File**: `api/src/main/java/org/allaymc/api/entity/interfaces/EntityPlayer.java` **Fix** (lines 257-277): ```java default void notifyItemInHandChange() { var inv = getContainer(ContainerTypes.INVENTORY); var itemStack = inv.getItemInHand(); if (itemStack.getCount() != 0) { inv.notifySlotChange(inv.getHandSlot()); } else { inv.setItemInHand(ItemAirStack.AIR_STACK); } // Update visual hand item for all viewers (including self via controller) var controller = getController(); if (controller != null) { controller.viewEntityHand(this); } forEachViewers(viewer -> viewer.viewEntityHand(this)); } ``` **Status**: ✅ FIXED --- ### ✅ ISSUE 2: Pickup doesn't show in hand for other players **Symptom**: Player picks up an item from the ground and it goes to the current hand slot, but other players don't see the item in their hand. **Root Cause**: The pickup code (`tryAddItem`) didn't check if the item went to the current hand slot and didn't notify viewers. **Fixed File**: `server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerContainerHolderComponentImpl.java` **Fix** (lines 176-192): ```java var inventory = Objects.requireNonNull(getContainer(ContainerTypes.INVENTORY)); var handSlot = inventory.getHandSlot(); var slot = inventory.tryAddItem(item); if (slot == -1) { // Player's inventory is full and cannot pick up the item continue; } // Notify viewers about hand item change if item went to hand slot if (slot == handSlot) { var controller = thisPlayer.getController(); if (controller != null) { controller.viewEntityHand(thisPlayer); } thisPlayer.forEachViewers(viewer -> viewer.viewEntityHand(thisPlayer)); } ``` **Status**: ✅ FIXED (also applied to arrow and trident pickup) --- ### ✅ ISSUE 3: Player reconnects with invisible hand items/equipment to others **Symptom**: Player disconnects with a sword, shield, and boots. Upon reconnecting, only the boots were visible to other players; the sword and shield were invisible. **Root Causes**: 1. `onLoadNBT` loaded the inventory but didn't notify about the hand 2. `SetLocalPlayerAsInitializedPacketProcessor` notified the new player about others, but didn't notify other players about the new player **Fixed Files**: #### 3.1: `server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerContainerHolderComponentImpl.java` **Fix** (lines 298-307): ```java protected void onLoadNBT(CEntityLoadNBTEvent event) { var nbt = event.getNbt(); nbt.listenForList(TAG_OFFHAND, NbtType.COMPOUND, offhandNbt -> getContainer(ContainerTypes.OFFHAND).loadNBT(offhandNbt)); nbt.listenForList(TAG_INVENTORY, NbtType.COMPOUND, inventoryNbt -> { getContainer(ContainerTypes.INVENTORY).loadNBT(inventoryNbt); // Notify hand change after inventory is loaded if (thisPlayer instanceof EntityPlayer entityPlayer) { entityPlayer.notifyItemInHandChange(); } }); nbt.listenForList(TAG_ARMOR, NbtType.COMPOUND, armorNbt -> getContainer(ContainerTypes.ARMOR).loadNBT(armorNbt)); nbt.listenForList(TAG_ENDER_ITEMS, NbtType.COMPOUND, enderItemsNbt -> getContainer(ContainerTypes.ENDER_CHEST).loadNBT(enderItemsNbt)); } ``` #### 3.2: `server/src/main/java/org/allaymc/server/network/processor/login/SetLocalPlayerAsInitializedPacketProcessor.java` **Fix** (lines 38-60): ```java // Notify all players about this player's hand item (and other equipment) // This ensures that when player B joins, player A's hand item becomes visible to B var dimension = entity.getDimension(); for (var otherPlayer : dimension.getPlayers()) { if (otherPlayer == player) continue; var otherEntity = otherPlayer.getControlledEntity(); if (otherEntity != null) { player.viewEntityHand(otherEntity); player.viewEntityOffhand(otherEntity); player.viewEntityArmors(otherEntity); } } // Notify all other players about this player's hand item and equipment // This ensures that when player B joins, player B's hand item becomes visible to A if (entity instanceof EntityPlayer entityPlayer) { for (var otherPlayer : dimension.getPlayers()) { if (otherPlayer == player) continue; otherPlayer.viewEntityHand(entityPlayer); otherPlayer.viewEntityOffhand(entityPlayer); otherPlayer.viewEntityArmors(entityPlayer); } } ``` **Status**: ✅ FIXED --- ### ✅ ISSUE 4: Moving item from hotbar to inventory via UI doesn't update visually **Symptom**: Player has a shield in hand, opens inventory, moves the shield to an internal inventory slot. Other players still see the shield in their hand. **Root Cause**: `TransferItemActionProcessor` used `notifySlotChange(slot, false)` which didn't trigger the `slotChangeListeners`, preventing `viewEntityHand()` from being called. **Fixed Files**: #### 4.1: `server/src/main/java/org/allaymc/server/container/impl/BaseContainer.java` **Fix** (lines 122-138): ```java @Override public void notifySlotChange(int slot, boolean send) { if (send) { for (var viewer : viewers.values()) { viewer.viewContainerSlot(this, slot); } } // Always notify slot change listeners, regardless of send parameter // This ensures that hand container listeners (which call viewEntityHand) // are triggered even when send=false (used by ItemStackRequest system) var listeners = slotChangeListeners.get(slot); if (listeners == null || listeners.isEmpty()) { return; } for (var listener : listeners) { listener.accept(content[slot]); } } ``` #### 4.2: `server/src/main/java/org/allaymc/server/container/processor/TransferItemActionProcessor.java` **Fix** (added after line 124): ```java // Check if source or destination is the hand slot boolean sourceIsHand = isHandSlot(sourceContainer, sourceSlot); boolean destIsHand = isHandSlot(destinationContainer, destinationSlot); // ... existing code ... // Notify hand change if hand slot was involved if (sourceIsHand || destIsHand) { var entity = player.getControlledEntity(); if (entity instanceof EntityPlayer entityPlayer) { entityPlayer.notifyItemInHandChange(); } } // Added helper method: private boolean isHandSlot(Container container, int slot) { if (container.getContainerType() == ContainerTypes.INVENTORY) { var inv = (InventoryContainer) container; return slot == inv.getHandSlot(); } return false; } ``` **Status**: ✅ FIXED --- ### ✅ ISSUE 5: InventorySlotPacket doesn't visually update player containers **Symptom**: Changes in inventory, armor, or offhand slots didn't always update visually for the client. **Root Cause**: `InventorySlotPacket` has known issues for `AbstractPlayerContainer` type containers (inventory, armor, offhand). **Fixed File**: `server/src/main/java/org/allaymc/server/player/AllayPlayer.java` **Fix** (lines 2067-2078): ```java @Override public void viewContainerSlot(Container container, int slot) { if (container instanceof AbstractPlayerContainer playerContainer) { // HACK: for unknown reason, InventorySlotPacket doesn't work properly for player containers // (inventory, armor, offhand). We must send InventoryContentPacket instead to ensure // the client updates the slot correctly. // TODO: replace this hack when we find the reason and have better solution viewContentsWithSpecificContainerId(playerContainer, playerContainer.getUnopenedContainerId()); return; } var id = idToContainer.inverse().get(container); if (id == null) { throw new IllegalStateException("This viewer did not open the container " + container.getContainerType()); } viewSlotWithSpecificContainerId(container, slot, id); } ``` **Status**: ✅ FIXED (workaround applied) --- ## Modified Files | File | Change | Status | |------|--------|--------| | `api/src/main/java/org/allaymc/api/entity/interfaces/EntityPlayer.java` | Updated `notifyItemInHandChange()` | ✅ Fixed | | `server/src/main/java/org/allaymc/server/entity/component/player/EntityPlayerContainerHolderComponentImpl.java` | Pickup notifies hand + onLoadNBT notifies hand | ✅ Fixed | | `server/src/main/java/org/allaymc/server/network/processor/login/SetLocalPlayerAsInitializedPacketProcessor.java` | Bidirectional hand/equipment login sync | ✅ Fixed | | `server/src/main/java/org/allaymc/server/container/impl/BaseContainer.java` | `notifySlotChange()` always calls listeners | ✅ Fixed | | `server/src/main/java/org/allaymc/server/container/processor/TransferItemActionProcessor.java` | Detects hand + notifies | ✅ Fixed | | `server/src/main/java/org/allaymc/server/player/AllayPlayer.java` | InventoryContentPacket for player containers | ✅ Fixed | --- ## How to Test 1. **Drop Test**: - Player A holds an item in the hotbar (slot 0-8) - Player A presses Q to drop - Player B should see the item disappear from A's hand immediately 2. **Pickup Test**: - Item on the ground - Player A picks up the item (goes to current hand slot) - Player B should see the item appear in A's hand 3. **Login Test**: - Player A logs in with a sword in hand (slot 0), shield in offhand, boots in armor - Player B is already online - Player B should see: sword in hand, shield in offhand, boots in armor on A 4. **UI Test (move item)**: - Player A has a shield in hand - Player A opens inventory and moves the shield to an internal slot - Player B should see the shield disappear from A's hand 5. **Test with specific items**: - Repeat all tests above with: shield, trident, sword, bow, etc. --- ## Recommendations for API Developers 1. **Investigate InventorySlotPacket**: The hack in `AllayPlayer.java` indicates that `InventorySlotPacket` doesn't work for `AbstractPlayerContainer`. It is recommended to investigate whether the issue is in the Cloudburst library or in the incorrect use of the packet. 2. **Consolidate update system**: There are two conflicting systems: - `ItemStackRequest` + `ItemStackResponse` (new system) - `InventoryTransactionPacket` (old system) It is recommended to unify or clearly document when each one should be used. 3. **Improve documentation**: The `notifySlotChange(slot, send)` method has confusing behavior. The `send` parameter suggests it can suppress updates, but this breaks synchronization. It is recommended to: - Remove the `send` parameter - Always notify viewers - Let the `ItemStackResponse` system handle duplicates 4. **Automated tests**: Add tests for visual synchronization between multiple players. --- ## Conclusion All reported bugs were successfully fixed. The fixes ensure that: - ✅ Drop updates hand visually - ✅ Pickup updates hand visually - ✅ Login shows hand items/equipment to others - ✅ Moving item via UI updates hand for others - ✅ Inventory uses `InventoryContentPacket` (workaround for `InventorySlotPacket` bug) **Build**: All modules compile successfully (`BUILD SUCCESSFUL`). ---
0 条评论