Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a held slot handler #585

Merged
merged 10 commits into from
Nov 26, 2024
3 changes: 3 additions & 0 deletions server/player/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {}
Expand Down
27 changes: 25 additions & 2 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 < 0 || 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)
Expand Down Expand Up @@ -1852,7 +1875,7 @@ func (p *Player) PickBlock(pos cube.Pos) {

if found {
if slot < 9 {
_ = p.session().SetHeldSlot(slot)
_ = p.SetHeldSlot(slot)
return
}
_ = p.Inventory().Swap(slot, int(p.heldSlot.Load()))
Expand All @@ -1865,7 +1888,7 @@ func (p *Player) PickBlock(pos cube.Pos) {
return
}
if firstEmpty < 8 {
_ = p.session().SetHeldSlot(firstEmpty)
_ = p.SetHeldSlot(firstEmpty)
_ = p.Inventory().SetItem(firstEmpty, pickedItem)
return
}
Expand Down
1 change: 1 addition & 0 deletions server/session/controllable.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions server/session/handler_inventory_transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
s.changingSlot.Store(true)
err := s.c.SetHeldSlot(int(data.HotBarSlot))
s.changingSlot.Store(false)
if 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 {
s.changingSlot.Store(true)
err := s.c.SetHeldSlot(int(data.HotBarSlot))
s.changingSlot.Store(false)
if 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 {
s.changingSlot.Store(true)
err := s.c.SetHeldSlot(int(data.HotBarSlot))
s.changingSlot.Store(false)
if err != nil {
return err
}
return h.handleReleaseItemTransaction(s)
Expand Down
12 changes: 11 additions & 1 deletion server/session/handler_mob_equipment.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ 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)
}
s.changingSlot.Store(true)
defer s.changingSlot.Store(false)
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)
}
Expand Down
34 changes: 4 additions & 30 deletions server/session/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -566,15 +566,17 @@ func (s *Session) HandleInventories() (inv, offHand, enderChest *inventory.Inven

// SetHeldSlot sets the currently held hotbar slot.
func (s *Session) SetHeldSlot(slot int) error {
if slot > 8 {
if slot < 0 || slot > 8 {
return fmt.Errorf("slot exceeds hotbar range 0-8: slot is %v", slot)
}

s.heldSlot.Store(uint32(slot))

for _, viewer := range s.c.World().Viewers(s.c.Position()) {
viewer.ViewEntityItems(s.c)
}
if s.changingSlot.Load() {
return nil
}

mainHand, _ := s.c.HeldItems()
s.writePacket(&packet.MobEquipment{
Expand All @@ -586,34 +588,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()
Expand Down
1 change: 1 addition & 0 deletions server/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ type Session struct {
openedWindow atomic.Value[*inventory.Inventory]
openedPos atomic.Value[cube.Pos]
swingingArm atomic.Bool
changingSlot atomic.Bool

recipes map[uint32]recipe.Recipe

Expand Down