From fde667713a3c7342110aa0fae8b919f7f6fc4efa Mon Sep 17 00:00:00 2001 From: "Natanael [Root]" Date: Mon, 22 Jun 2026 23:42:23 +0200 Subject: [PATCH] server/session: Resend inventories on item release to keep the client in sync When a player releases an item such as a bow, the client predicts the result and, for example, decrements the arrow it expects to be consumed. If the server does not actually consume anything (the release is cancelled, the bow is not drawn long enough, or there are no arrows), it never corrected the client, leaving the displayed inventory out of sync with the server. Resend the inventories before handling the release in both release paths, mirroring what handleUseItemTransaction already does for item use. The existing slot updates from an actual consumption are still sent afterwards, so a successful shot ends up with the correct count either way. Fixes #1001. Co-Authored-By: Claude Opus 4.8 --- server/session/handler_inventory_transaction.go | 16 ++++++++++------ server/session/handler_player_action.go | 5 +++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/server/session/handler_inventory_transaction.go b/server/session/handler_inventory_transaction.go index 9cb4c3820a..fefc1096a7 100644 --- a/server/session/handler_inventory_transaction.go +++ b/server/session/handler_inventory_transaction.go @@ -45,7 +45,7 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *wo switch data := pk.TransactionData.(type) { case *protocol.NormalTransactionData: - h.resendInventories(s) + s.resendInventories() // Always resend inventories with normal transactions. Most of the time we do not use these // transactions, so we're best off making sure the client and server stay in sync. if err := h.handleNormalTransaction(pk, s, c); err != nil { @@ -54,7 +54,7 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *wo return case *protocol.MismatchTransactionData: // Just resend the inventory and don't do anything. - h.resendInventories(s) + s.resendInventories() return case *protocol.UseItemOnEntityTransactionData: if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil { @@ -70,13 +70,13 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *wo if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil { return } - return h.handleReleaseItemTransaction(c) + return h.handleReleaseItemTransaction(s, c) } return fmt.Errorf("unhandled inventory transaction type %T", pk.TransactionData) } // resendInventories resends all inventories of the player. -func (h *InventoryTransactionHandler) resendInventories(s *Session) { +func (s *Session) resendInventories() { s.sendInv(s.inv, protocol.WindowIDInventory) s.sendInv(s.ui, protocol.WindowIDUI) s.sendInv(s.offHand, protocol.WindowIDOffHand) @@ -183,7 +183,7 @@ func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.Us // having done that client-side. // Because of the new inventory system, the client will expect a transaction confirmation, but instead of doing that // it's much easier to just resend the inventory. - h.resendInventories(s) + s.resendInventories() switch data.ActionType { case protocol.UseItemActionBreakBlock: @@ -199,7 +199,11 @@ func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.Us } // handleReleaseItemTransaction ... -func (h *InventoryTransactionHandler) handleReleaseItemTransaction(c Controllable) error { +func (h *InventoryTransactionHandler) handleReleaseItemTransaction(s *Session, c Controllable) error { + // The client predicts the result of releasing an item, such as an arrow being removed from the + // inventory when a bow is fired. Resend the inventories so that the client stays in sync if the + // release does not actually consume the predicted items, for example when it is cancelled. + s.resendInventories() c.ReleaseItem() return nil } diff --git a/server/session/handler_player_action.go b/server/session/handler_player_action.go index 4d38d9906f..76ab833130 100644 --- a/server/session/handler_player_action.go +++ b/server/session/handler_player_action.go @@ -47,6 +47,11 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR case protocol.PlayerActionStartItemUseOn: // TODO: Properly utilize these actions. case protocol.PlayerActionStopItemUseOn: + // The client predicts the result of releasing an item, such as an arrow being removed from + // the inventory when a bow is fired. Resend the inventories so that the client stays in sync + // if the release does not actually consume the predicted items, for example when it is + // cancelled. + s.resendInventories() c.ReleaseItem() case protocol.PlayerActionStartBuildingBlock: // Don't do anything for this action.