diff --git a/server/player/handler.go b/server/player/handler.go index 4ae54a021..af054abb6 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -118,6 +118,8 @@ type Handler interface { // HandleItemPickup handles the player picking up an item from the ground. The item stack laying on the // ground is passed. ctx.Cancel() may be called to prevent the player from picking up the item. HandleItemPickup(ctx *event.Context, i item.Stack) + // HandleHeldSlotChange handles the player changing the slot they are currently holding. + HandleHeldSlotChange(ctx *event.Context, slot int) // HandleItemDrop handles the player dropping an item on the ground. The dropped item entity is passed. // ctx.Cancel() may be called to prevent the player from dropping the entity.Item passed on the ground. // e.Item() may be called to obtain the item stack dropped. @@ -142,6 +144,7 @@ type NopHandler struct{} var _ Handler = (*NopHandler)(nil) func (NopHandler) HandleItemDrop(*event.Context, *entity.Item) {} +func (NopHandler) HandleHeldSlotChange(*event.Context, int) {} func (NopHandler) HandleMove(*event.Context, mgl64.Vec3, float64, float64) {} func (NopHandler) HandleJump() {} func (NopHandler) HandleTeleport(*event.Context, mgl64.Vec3) {} diff --git a/server/player/player.go b/server/player/player.go index e1f35924e..f0f5b0d5b 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -367,6 +367,29 @@ func (p *Player) Transfer(address string) error { return nil } +// SetHeldSlot updates the held slot of the player using the slot provided. It also calls the slot change handler. +func (p *Player) SetHeldSlot(slot int) error { + // The slot that the player might have selected must be within the hotbar: The held item cannot be in a + // different place in the inventory. + if slot > 8 { + return fmt.Errorf("new held slot exceeds hotbar range 0-8: slot is %v", slot) + } + if p.heldSlot.Swap(uint32(slot)) == uint32(slot) { + // Old slot was the same as new slot, so don't do anything. + return nil + } + + p.usingItem.Store(false) + + ctx := event.C() + p.Handler().HandleHeldSlotChange(ctx, slot) + if ctx.Cancelled() { + // The slot change was cancelled, so don't do anything. + return nil + } + return p.session().SetHeldSlot(slot) +} + // SendCommandOutput sends the output of a command to the player. func (p *Player) SendCommandOutput(output *cmd.Output) { p.session().SendCommandOutput(output) diff --git a/server/session/controllable.go b/server/session/controllable.go index 9c7302418..86d1a6b75 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -28,6 +28,7 @@ type Controllable interface { Locale() language.Tag SetHeldItems(right, left item.Stack) + SetHeldSlot(slot int) error Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) Speed() float64 diff --git a/server/session/handler_inventory_transaction.go b/server/session/handler_inventory_transaction.go index b72f88ffe..b39e69dcc 100644 --- a/server/session/handler_inventory_transaction.go +++ b/server/session/handler_inventory_transaction.go @@ -14,7 +14,6 @@ type InventoryTransactionHandler struct{} // Handle ... func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session) error { pk := p.(*packet.InventoryTransaction) - switch data := pk.TransactionData.(type) { case *protocol.NormalTransactionData: h.resendInventories(s) @@ -30,17 +29,26 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session) error h.resendInventories(s) return nil case *protocol.UseItemOnEntityTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil { + if held, _ := s.c.HeldItems(); !held.Equal(stackToItem(data.HeldItem.Stack)) { + return nil + } + if err := s.c.SetHeldSlot(int(data.HotBarSlot)); err != nil { return err } return h.handleUseItemOnEntityTransaction(data, s) case *protocol.UseItemTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil { + if held, _ := s.c.HeldItems(); !held.Equal(stackToItem(data.HeldItem.Stack)) { + return nil + } + if err := s.SetHeldSlot(int(data.HotBarSlot)); err != nil { return err } return h.handleUseItemTransaction(data, s) case *protocol.ReleaseItemTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil { + if held, _ := s.c.HeldItems(); !held.Equal(stackToItem(data.HeldItem.Stack)) { + return nil + } + if err := s.SetHeldSlot(int(data.HotBarSlot)); err != nil { return err } return h.handleReleaseItemTransaction(s) diff --git a/server/session/handler_mob_equipment.go b/server/session/handler_mob_equipment.go index 1ef6fe96d..f0ad40020 100644 --- a/server/session/handler_mob_equipment.go +++ b/server/session/handler_mob_equipment.go @@ -21,7 +21,15 @@ func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session) error { // This window ID is expected, but we don't handle it. return nil case protocol.WindowIDInventory: - return s.UpdateHeldSlot(int(pk.InventorySlot), stackToItem(pk.NewItem.Stack)) + // The item the client claims to have must be identical to the one we have registered server-side. + actual, _ := s.inv.Item(int(pk.InventorySlot)) + clientSide := stackToItem(pk.NewItem.Stack) + if !actual.Equal(clientSide) { + // Only ever debug these as they are frequent and expected to happen whenever client and server get + // out of sync. + s.log.Debugf("failed processing packet from %v (%v): *packet.MobEquipment: client-side item must be identical to server-side item, but got differences: client: %v vs server: %v", s.conn.RemoteAddr(), s.c.Name(), clientSide, actual) + } + return s.c.SetHeldSlot(int(pk.InventorySlot)) default: return fmt.Errorf("only main inventory should be involved in slot chnage, got window ID %v", pk.WindowID) } diff --git a/server/session/player.go b/server/session/player.go index f1d22e262..b6e096f49 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -586,34 +586,6 @@ func (s *Session) SetHeldSlot(slot int) error { return nil } -// UpdateHeldSlot updates the held slot of the Session to the slot passed. It also verifies that the item in that slot -// matches an expected item stack. -func (s *Session) UpdateHeldSlot(slot int, expected item.Stack) error { - // The slot that the player might have selected must be within the hotbar: The held item cannot be in a - // different place in the inventory. - if slot > 8 { - return fmt.Errorf("new held slot exceeds hotbar range 0-8: slot is %v", slot) - } - if s.heldSlot.Swap(uint32(slot)) == uint32(slot) { - // Old slot was the same as new slot, so don't do anything. - return nil - } - - clientSideItem := expected - actual, _ := s.inv.Item(slot) - - // The item the client claims to have must be identical to the one we have registered server-side. - if !clientSideItem.Equal(actual) { - // Only ever debug these as they are frequent and expected to happen whenever client and server get - // out of sync. - s.log.Debugf("failed processing packet from %v (%v): failed changing held slot: client-side item must be identical to server-side item, but got differences: client: %v vs server: %v", s.conn.RemoteAddr(), s.c.Name(), clientSideItem, actual) - } - for _, viewer := range s.c.World().Viewers(s.c.Position()) { - viewer.ViewEntityItems(s.c) - } - return nil -} - // SendExperience sends the experience level and progress from the given experience manager to the player. func (s *Session) SendExperience(e *entity.ExperienceManager) { level, progress := e.Level(), e.Progress()