Files
mc/server/session/handler_inventory_transaction.go
T

206 lines
7.6 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
package session
import (
"fmt"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/event"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/inventory"
"github.com/df-mc/dragonfly/server/world"
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
)
// InventoryTransactionHandler handles the InventoryTransaction packet.
type InventoryTransactionHandler struct{}
// Handle ...
func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) (err error) {
pk := p.(*packet.InventoryTransaction)
if len(pk.LegacySetItemSlots) > 2 {
return fmt.Errorf("too many slot sync requests in inventory transaction")
}
defer func() {
// The client has requested the server to resend the specified slots even if they haven't changed server-side.
// Handling these requests is necessary to ensure the client's inventory remains in sync with the server.
for _, slot := range pk.LegacySetItemSlots {
if len(slot.Slots) > 2 {
err = fmt.Errorf("too many slots in slot sync request")
return
}
switch slot.ContainerID {
case protocol.ContainerOffhand:
s.sendInv(s.offHand, protocol.WindowIDOffHand)
case protocol.ContainerInventory:
for _, slot := range slot.Slots {
if i, err := s.inv.Item(int(slot)); err == nil {
s.sendItem(i, int(slot), protocol.WindowIDInventory)
}
}
}
}
}()
switch data := pk.TransactionData.(type) {
case *protocol.NormalTransactionData:
h.resendInventories(s)
// Always resend inventories with normal transactions. Most of the time we do not use these
// transactions, so we're best off making sure the client and server stay in sync.
if err := h.handleNormalTransaction(pk, s, c); err != nil {
s.conf.Log.Debug("process packet: InventoryTransaction: verify Normal transaction actions: " + err.Error())
}
return
case *protocol.MismatchTransactionData:
// Just resend the inventory and don't do anything.
h.resendInventories(s)
return
case *protocol.UseItemOnEntityTransactionData:
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
return
}
return h.handleUseItemOnEntityTransaction(data, s, tx, c)
case *protocol.UseItemTransactionData:
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
return
}
return h.handleUseItemTransaction(data, s, c)
case *protocol.ReleaseItemTransactionData:
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
return
}
return h.handleReleaseItemTransaction(c)
}
return fmt.Errorf("unhandled inventory transaction type %T", pk.TransactionData)
}
// resendInventories resends all inventories of the player.
func (h *InventoryTransactionHandler) resendInventories(s *Session) {
s.sendInv(s.inv, protocol.WindowIDInventory)
s.sendInv(s.ui, protocol.WindowIDUI)
s.sendInv(s.offHand, protocol.WindowIDOffHand)
s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour)
}
// handleNormalTransaction ...
func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.InventoryTransaction, s *Session, c Controllable) error {
if len(pk.Actions) != 2 {
return fmt.Errorf("expected two actions for dropping an item, got %d", len(pk.Actions))
}
var (
slot int
count int
expected item.Stack
)
for _, action := range pk.Actions {
switch {
case action.SourceType == protocol.InventoryActionSourceWorld && action.InventorySlot == 0:
if old := stackToItem(s.br, action.OldItem.Stack); !old.Empty() {
return fmt.Errorf("unexpected non-empty old item in transaction action: %#v", action.OldItem)
}
count = int(action.NewItem.Stack.Count)
case action.SourceType == protocol.InventoryActionSourceContainer && action.WindowID == protocol.WindowIDInventory:
if expected = stackToItem(s.br, action.OldItem.Stack); expected.Empty() {
return fmt.Errorf("unexpected empty old item in transaction action: %#v", action.OldItem)
}
slot = int(action.InventorySlot)
default:
return fmt.Errorf("unexpected action type in drop item transaction")
}
}
actual, _ := s.inv.Item(slot)
if count < 1 {
return fmt.Errorf("expected at least one item to be dropped, got %d", count)
}
if count > actual.Count() {
return fmt.Errorf("tried to throw %v items, but held only %v in slot", count, actual.Count())
}
if !expected.Equal(actual) {
return fmt.Errorf("different item thrown than held in slot: %#v was thrown but held %#v", expected, actual)
}
// Explicitly don't re-use the thrown variable. This item was supplied by the user, and if some
// logic in the Comparable() method was flawed, users would be able to cheat with item properties.
// Only grow or shrink the held item to prevent any such issues.
res := actual.Grow(count - actual.Count())
if err := call(event.C(inventory.Holder(c)), int(*s.heldSlot), res, s.inv.Handler().HandleDrop); err != nil {
return err
}
n := c.Drop(res)
_ = s.inv.SetItem(slot, actual.Grow(-n))
return nil
}
// handleUseItemOnEntityTransaction ...
func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *protocol.UseItemOnEntityTransactionData, s *Session, tx *world.Tx, c Controllable) error {
s.swingingArm.Store(true)
defer s.swingingArm.Store(false)
if data.TargetEntityRuntimeID == selfEntityRuntimeID {
return fmt.Errorf("invalid entity interaction: players cannot interact with themselves")
}
handle, ok := s.entityFromRuntimeID(data.TargetEntityRuntimeID)
if !ok {
// In some cases, for example when a falling block entity solidifies, latency may allow attacking an entity that
// no longer exists server side. This is expected, so we shouldn't kick the player.
s.conf.Log.Debug("invalid entity interaction: no entity with runtime ID", "ID", data.TargetEntityRuntimeID)
return nil
}
e, ok := handle.Entity(tx)
if !ok {
s.conf.Log.Debug("invalid entity interaction: entity is not in the same world (anymore)", "ID", data.TargetEntityRuntimeID)
return nil
}
var valid bool
switch data.ActionType {
case protocol.UseItemOnEntityActionInteract:
valid = c.UseItemOnEntity(e)
case protocol.UseItemOnEntityActionAttack:
valid = c.AttackEntity(e)
default:
return fmt.Errorf("unhandled UseItemOnEntity ActionType %v", data.ActionType)
}
if !valid {
slot := int(*s.heldSlot)
it, _ := s.inv.Item(slot)
s.sendItem(it, slot, protocol.WindowIDInventory)
}
return nil
}
// handleUseItemTransaction ...
func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.UseItemTransactionData, s *Session, c Controllable) error {
pos := cube.Pos{int(data.BlockPosition[0]), int(data.BlockPosition[1]), int(data.BlockPosition[2])}
s.swingingArm.Store(true)
defer s.swingingArm.Store(false)
// We reset the inventory so that we can send the held item update without the client already
// having done that client-side.
// Because of the new inventory system, the client will expect a transaction confirmation, but instead of doing that
// it's much easier to just resend the inventory.
h.resendInventories(s)
switch data.ActionType {
case protocol.UseItemActionBreakBlock:
c.BreakBlock(pos)
case protocol.UseItemActionClickBlock:
c.UseItemOnBlock(pos, cube.Face(data.BlockFace), vec32To64(data.ClickedPosition))
case protocol.UseItemActionClickAir:
c.UseItem()
default:
return fmt.Errorf("unhandled UseItem ActionType %v", data.ActionType)
}
return nil
}
// handleReleaseItemTransaction ...
func (h *InventoryTransactionHandler) handleReleaseItemTransaction(c Controllable) error {
c.ReleaseItem()
return nil
}