diff --git a/server/player/handler.go b/server/player/handler.go index 18011eccb..6db3a6343 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -126,6 +126,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 *Context, i *item.Stack) + // HandleHeldSlotChange handles the player changing the slot they are currently holding. + HandleHeldSlotChange(ctx *Context, from, to 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. @@ -154,6 +156,7 @@ type NopHandler struct{} var _ Handler = NopHandler{} func (NopHandler) HandleItemDrop(*Context, item.Stack) {} +func (NopHandler) HandleHeldSlotChange(*Context, int, int) {} func (NopHandler) HandleMove(*Context, mgl64.Vec3, cube.Rotation) {} func (NopHandler) HandleJump(*Player) {} func (NopHandler) HandleTeleport(*Context, mgl64.Vec3) {} diff --git a/server/player/player.go b/server/player/player.go index 3c4ff033c..9db3a2b2b 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1231,6 +1231,37 @@ func (p *Player) SetHeldItems(mainHand, offHand item.Stack) { _ = p.offHand.SetItem(0, offHand) } +// SetHeldSlot updates the held slot of the player to the slot provided. The +// slot must be between 0 and 8. +func (p *Player) SetHeldSlot(to 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 to < 0 || to > 8 { + return fmt.Errorf("held slot exceeds hotbar range 0-8: slot is %v", to) + } + from := int(*p.heldSlot) + if from == to { + // Old slot was the same as new slot, so don't do anything. + return nil + } + + ctx := event.C(p) + p.Handler().HandleHeldSlotChange(ctx, from, to) + if ctx.Cancelled() { + // The slot change was cancelled, resend held slot. + p.session().SendHeldSlot(from, p, true) + return nil + } + *p.heldSlot = uint32(to) + p.usingItem = false + + for _, viewer := range p.viewers() { + viewer.ViewEntityItems(p) + } + p.session().SendHeldSlot(to, p, false) + return nil +} + // EnderChestInventory returns the player's ender chest inventory. Its accessed by the player when opening // ender chests anywhere. func (p *Player) EnderChestInventory() *inventory.Inventory { @@ -1908,7 +1939,7 @@ func (p *Player) PickBlock(pos cube.Pos) { if found { if slot < 9 { - _ = p.session().SetHeldSlot(slot, p.tx, p) + _ = p.SetHeldSlot(slot) return } _ = p.Inventory().Swap(slot, int(*p.heldSlot)) @@ -1921,7 +1952,7 @@ func (p *Player) PickBlock(pos cube.Pos) { return } if firstEmpty < 8 { - _ = p.session().SetHeldSlot(firstEmpty, p.tx, p) + _ = p.SetHeldSlot(firstEmpty) _ = p.Inventory().SetItem(firstEmpty, pickedItem) return } diff --git a/server/session/controllable.go b/server/session/controllable.go index b085d9a2c..71e4a0503 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -29,6 +29,7 @@ type Controllable interface { Locale() language.Tag SetHeldItems(right, left item.Stack) + SetHeldSlot(slot int) error Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) diff --git a/server/session/handler_inventory_transaction.go b/server/session/handler_inventory_transaction.go index 46144bd03..2d1cecb03 100644 --- a/server/session/handler_inventory_transaction.go +++ b/server/session/handler_inventory_transaction.go @@ -33,17 +33,17 @@ func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *wo h.resendInventories(s) return nil case *protocol.UseItemOnEntityTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), tx, c); err != nil { + if err := s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), c); err != nil { return err } return h.handleUseItemOnEntityTransaction(data, s, tx, c) case *protocol.UseItemTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), tx, c); err != nil { + if err := s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), c); err != nil { return err } return h.handleUseItemTransaction(data, s, c) case *protocol.ReleaseItemTransactionData: - if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), tx, c); err != nil { + if err := s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack), c); err != nil { return err } return h.handleReleaseItemTransaction(c) diff --git a/server/session/handler_mob_equipment.go b/server/session/handler_mob_equipment.go index 19c130cf0..00dda84d1 100644 --- a/server/session/handler_mob_equipment.go +++ b/server/session/handler_mob_equipment.go @@ -22,8 +22,8 @@ func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c // 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), tx, c) + return s.VerifyAndSetHeldSlot(int(pk.InventorySlot), stackToItem(pk.NewItem.Stack), c) default: - return fmt.Errorf("only main inventory should be involved in slot chnage, got window ID %v", pk.WindowID) + return fmt.Errorf("only main inventory should be involved in slot change, got window ID %v", pk.WindowID) } } diff --git a/server/session/player.go b/server/session/player.go index a03095d10..9aafa6831 100644 --- a/server/session/player.go +++ b/server/session/player.go @@ -607,18 +607,11 @@ func (s *Session) uiInventoryFunc(tx *world.Tx, c Controllable) inventory.SlotFu } } -// SetHeldSlot sets the currently held hotbar slot. -func (s *Session) SetHeldSlot(slot int, tx *world.Tx, c Controllable) error { - if slot > 8 { - return fmt.Errorf("slot exceeds hotbar range 0-8: slot is %v", slot) - } - - *s.heldSlot = uint32(slot) - - for _, viewer := range tx.Viewers(c.Position()) { - viewer.ViewEntityItems(c) +// SendHeldSlot sends the currently held hotbar slot. +func (s *Session) SendHeldSlot(slot int, c Controllable, force bool) { + if s.changingSlot.Load() && !force { + return } - mainHand, _ := c.HeldItems() s.writePacket(&packet.MobEquipment{ EntityRuntimeID: selfEntityRuntimeID, @@ -626,37 +619,37 @@ func (s *Session) SetHeldSlot(slot int, tx *world.Tx, c Controllable) error { InventorySlot: byte(slot), HotBarSlot: byte(slot), }) - 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, tx *world.Tx, c Controllable) 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) +// VerifyAndSetHeldSlot verifies if the slot passed is a valid hotbar slot and +// if the expected item.Stack is in it. Afterwards, it changes the held slot +// of the player. +func (s *Session) VerifyAndSetHeldSlot(slot int, expected item.Stack, c Controllable) error { + if err := s.VerifySlot(slot, expected); err != nil { + return err } - if *s.heldSlot == uint32(slot) { - // Old slot was the same as new slot, so don't do anything. - return nil - } - // The user swapped changed held slots so stop using item right away. - c.ReleaseItem() - - *s.heldSlot = uint32(slot) + s.changingSlot.Store(true) + defer s.changingSlot.Store(false) + return c.SetHeldSlot(slot) +} +// VerifySlot verifies if the slot passed is a valid hotbar slot and if the +// expected item.Stack is in it. +func (s *Session) VerifySlot(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 < 0 || slot > 8 { + return fmt.Errorf("slot exceeds hotbar range 0-8: slot is %v", slot) + } 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. + // 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.conf.Log.Debug("update held slot: client-side item must be identical to server-side item, but got differences", "client-held", clientSideItem.String(), "server-held", actual.String()) - } - for _, viewer := range tx.Viewers(c.Position()) { - viewer.ViewEntityItems(c) + // Only ever debug these as they are frequent and expected to happen + // whenever client and server get out of sync. + s.conf.Log.Debug("verify slot: client-side item was not equal to server-side item", "client-held", clientSideItem.String(), "server-held", actual.String()) } return nil } diff --git a/server/session/session.go b/server/session/session.go index c8baad75d..dfb4d946c 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -72,6 +72,7 @@ type Session struct { openedWindow atomic.Pointer[inventory.Inventory] openedPos atomic.Pointer[cube.Pos] swingingArm atomic.Bool + changingSlot atomic.Bool changingDimension atomic.Bool recipes map[uint32]recipe.Recipe