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 }