github.com/df-mc/dragonfly@v0.9.13/server/session/handler_inventory_transaction.go (about)

     1  package session
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/df-mc/dragonfly/server/block/cube"
     6  	"github.com/df-mc/dragonfly/server/event"
     7  	"github.com/df-mc/dragonfly/server/item"
     8  	"github.com/sandertv/gophertunnel/minecraft/protocol"
     9  	"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
    10  )
    11  
    12  // InventoryTransactionHandler handles the InventoryTransaction packet.
    13  type InventoryTransactionHandler struct{}
    14  
    15  // Handle ...
    16  func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session) error {
    17  	pk := p.(*packet.InventoryTransaction)
    18  
    19  	switch data := pk.TransactionData.(type) {
    20  	case *protocol.NormalTransactionData:
    21  		h.resendInventories(s)
    22  		// Always resend inventories with normal transactions. Most of the time we do not use these
    23  		// transactions, so we're best off making sure the client and server stay in sync.
    24  		if err := h.handleNormalTransaction(pk, s); err != nil {
    25  			s.log.Debugf("failed processing packet from %v (%v): InventoryTransaction: failed verifying actions in Normal transaction: %v\n", s.conn.RemoteAddr(), s.c.Name(), err)
    26  			return nil
    27  		}
    28  		return nil
    29  	case *protocol.MismatchTransactionData:
    30  		// Just resend the inventory and don't do anything.
    31  		h.resendInventories(s)
    32  		return nil
    33  	case *protocol.UseItemOnEntityTransactionData:
    34  		if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil {
    35  			return err
    36  		}
    37  		return h.handleUseItemOnEntityTransaction(data, s)
    38  	case *protocol.UseItemTransactionData:
    39  		if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil {
    40  			return err
    41  		}
    42  		return h.handleUseItemTransaction(data, s)
    43  	case *protocol.ReleaseItemTransactionData:
    44  		if err := s.UpdateHeldSlot(int(data.HotBarSlot), stackToItem(data.HeldItem.Stack)); err != nil {
    45  			return err
    46  		}
    47  		return h.handleReleaseItemTransaction(s)
    48  	}
    49  	return fmt.Errorf("unhandled inventory transaction type %T", pk.TransactionData)
    50  }
    51  
    52  // resendInventories resends all inventories of the player.
    53  func (h *InventoryTransactionHandler) resendInventories(s *Session) {
    54  	s.sendInv(s.inv, protocol.WindowIDInventory)
    55  	s.sendInv(s.ui, protocol.WindowIDUI)
    56  	s.sendInv(s.offHand, protocol.WindowIDOffHand)
    57  	s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour)
    58  }
    59  
    60  // handleNormalTransaction ...
    61  func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.InventoryTransaction, s *Session) error {
    62  	if len(pk.Actions) != 2 {
    63  		return fmt.Errorf("expected two actions for dropping an item, got %d", len(pk.Actions))
    64  	}
    65  
    66  	var (
    67  		slot     int
    68  		count    int
    69  		expected item.Stack
    70  	)
    71  	for _, action := range pk.Actions {
    72  		if action.SourceType == protocol.InventoryActionSourceWorld && action.InventorySlot == 0 {
    73  			if old := stackToItem(action.OldItem.Stack); !old.Empty() {
    74  				return fmt.Errorf("unexpected non-empty old item in transaction action: %#v", action.OldItem)
    75  			}
    76  			count = int(action.NewItem.Stack.Count)
    77  		} else if action.SourceType == protocol.InventoryActionSourceContainer && action.WindowID == protocol.WindowIDInventory {
    78  			if expected = stackToItem(action.OldItem.Stack); expected.Empty() {
    79  				return fmt.Errorf("unexpected empty old item in transaction action: %#v", action.OldItem)
    80  			}
    81  			slot = int(action.InventorySlot)
    82  		} else {
    83  			return fmt.Errorf("unexpected action type in drop item transaction")
    84  		}
    85  	}
    86  
    87  	actual, _ := s.inv.Item(slot)
    88  	if count < 1 {
    89  		return fmt.Errorf("expected at least one item to be dropped, got %d", count)
    90  	}
    91  	if count > actual.Count() {
    92  		return fmt.Errorf("tried to throw %v items, but held only %v in slot", count, actual.Count())
    93  	}
    94  	if !expected.Equal(actual) {
    95  		return fmt.Errorf("different item thrown than held in slot: %#v was thrown but held %#v", expected, actual)
    96  	}
    97  
    98  	// Explicitly don't re-use the thrown variable. This item was supplied by the user, and if some
    99  	// logic in the Comparable() method was flawed, users would be able to cheat with item properties.
   100  	// Only grow or shrink the held item to prevent any such issues.
   101  	res := actual.Grow(count - actual.Count())
   102  	if err := call(event.C(), int(s.heldSlot.Load()), res, s.inv.Handler().HandleDrop); err != nil {
   103  		return err
   104  	}
   105  
   106  	n := s.c.Drop(res)
   107  	_ = s.inv.SetItem(slot, actual.Grow(-n))
   108  	return nil
   109  }
   110  
   111  // handleUseItemOnEntityTransaction ...
   112  func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *protocol.UseItemOnEntityTransactionData, s *Session) error {
   113  	s.swingingArm.Store(true)
   114  	defer s.swingingArm.Store(false)
   115  
   116  	e, ok := s.entityFromRuntimeID(data.TargetEntityRuntimeID)
   117  	if !ok {
   118  		// In some cases, for example when a falling block entity solidifies, latency may allow attacking an entity that
   119  		// no longer exists server side. This is expected, so we shouldn't kick the player.
   120  		s.log.Debugf("invalid entity interaction: no entity found with runtime ID %v", data.TargetEntityRuntimeID)
   121  		return nil
   122  	}
   123  	if data.TargetEntityRuntimeID == selfEntityRuntimeID {
   124  		return fmt.Errorf("invalid entity interaction: players cannot interact with themselves")
   125  	}
   126  
   127  	var valid bool
   128  	switch data.ActionType {
   129  	case protocol.UseItemOnEntityActionInteract:
   130  		valid = s.c.UseItemOnEntity(e)
   131  	case protocol.UseItemOnEntityActionAttack:
   132  		valid = s.c.AttackEntity(e)
   133  	default:
   134  		return fmt.Errorf("unhandled UseItemOnEntity ActionType %v", data.ActionType)
   135  	}
   136  	if !valid {
   137  		slot := int(s.heldSlot.Load())
   138  		item, _ := s.inv.Item(slot)
   139  		s.sendItem(item, slot, protocol.WindowIDInventory)
   140  	}
   141  	return nil
   142  }
   143  
   144  // handleUseItemTransaction ...
   145  func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.UseItemTransactionData, s *Session) error {
   146  	pos := cube.Pos{int(data.BlockPosition[0]), int(data.BlockPosition[1]), int(data.BlockPosition[2])}
   147  	s.swingingArm.Store(true)
   148  	defer s.swingingArm.Store(false)
   149  
   150  	// We reset the inventory so that we can send the held item update without the client already
   151  	// having done that client-side.
   152  	// Because of the new inventory system, the client will expect a transaction confirmation, but instead of doing that
   153  	// it's much easier to just resend the inventory.
   154  	h.resendInventories(s)
   155  
   156  	switch data.ActionType {
   157  	case protocol.UseItemActionBreakBlock:
   158  		s.c.BreakBlock(pos)
   159  	case protocol.UseItemActionClickBlock:
   160  		s.c.UseItemOnBlock(pos, cube.Face(data.BlockFace), vec32To64(data.ClickedPosition))
   161  	case protocol.UseItemActionClickAir:
   162  		s.c.UseItem()
   163  	default:
   164  		return fmt.Errorf("unhandled UseItem ActionType %v", data.ActionType)
   165  	}
   166  	return nil
   167  }
   168  
   169  // handleReleaseItemTransaction ...
   170  func (h *InventoryTransactionHandler) handleReleaseItemTransaction(s *Session) error {
   171  	s.c.ReleaseItem()
   172  	return nil
   173  }