up3
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user