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

     1  package session
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/df-mc/dragonfly/server/block"
     6  	"github.com/df-mc/dragonfly/server/entity"
     7  	"github.com/df-mc/dragonfly/server/event"
     8  	"github.com/df-mc/dragonfly/server/item"
     9  	"github.com/df-mc/dragonfly/server/item/inventory"
    10  	"github.com/go-gl/mathgl/mgl64"
    11  	"github.com/sandertv/gophertunnel/minecraft/protocol"
    12  	"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
    13  	"math"
    14  	"math/rand"
    15  	"time"
    16  )
    17  
    18  // ItemStackRequestHandler handles the ItemStackRequest packet. It handles the actions done within the
    19  // inventory.
    20  type ItemStackRequestHandler struct {
    21  	currentRequest int32
    22  
    23  	changes         map[byte]map[byte]changeInfo
    24  	responseChanges map[int32]map[*inventory.Inventory]map[byte]responseChange
    25  
    26  	pendingResults []item.Stack
    27  
    28  	current       time.Time
    29  	ignoreDestroy bool
    30  }
    31  
    32  // responseChange represents a change in a specific item stack response. It holds the timestamp of the
    33  // response which is used to get rid of changes that the client will have received.
    34  type responseChange struct {
    35  	id        int32
    36  	timestamp time.Time
    37  }
    38  
    39  // changeInfo holds information on a slot change initiated by an item stack request. It holds both the new and the old
    40  // item information and is used for reverting and verifying.
    41  type changeInfo struct {
    42  	after  protocol.StackResponseSlotInfo
    43  	before item.Stack
    44  }
    45  
    46  // Handle ...
    47  func (h *ItemStackRequestHandler) Handle(p packet.Packet, s *Session) error {
    48  	pk := p.(*packet.ItemStackRequest)
    49  	h.current = time.Now()
    50  
    51  	s.inTransaction.Store(true)
    52  	defer s.inTransaction.Store(false)
    53  
    54  	for _, req := range pk.Requests {
    55  		if err := h.handleRequest(req, s); err != nil {
    56  			// Item stacks being out of sync isn't uncommon, so don't error. Just debug the error and let the
    57  			// revert do its work.
    58  			s.log.Debugf("failed processing packet from %v (%v): ItemStackRequest: error resolving item stack request: %v", s.conn.RemoteAddr(), s.c.Name(), err)
    59  		}
    60  	}
    61  	return nil
    62  }
    63  
    64  // handleRequest resolves a single item stack request from the client.
    65  func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s *Session) (err error) {
    66  	h.currentRequest = req.RequestID
    67  	defer func() {
    68  		if err != nil {
    69  			h.reject(req.RequestID, s)
    70  			return
    71  		}
    72  		h.resolve(req.RequestID, s)
    73  		h.ignoreDestroy = false
    74  	}()
    75  
    76  	for _, action := range req.Actions {
    77  		switch a := action.(type) {
    78  		case *protocol.TakeStackRequestAction:
    79  			err = h.handleTake(a, s)
    80  		case *protocol.PlaceStackRequestAction:
    81  			err = h.handlePlace(a, s)
    82  		case *protocol.SwapStackRequestAction:
    83  			err = h.handleSwap(a, s)
    84  		case *protocol.DestroyStackRequestAction:
    85  			err = h.handleDestroy(a, s)
    86  		case *protocol.DropStackRequestAction:
    87  			err = h.handleDrop(a, s)
    88  		case *protocol.BeaconPaymentStackRequestAction:
    89  			err = h.handleBeaconPayment(a, s)
    90  		case *protocol.CraftRecipeStackRequestAction:
    91  			if s.containerOpened.Load() {
    92  				var special bool
    93  				switch s.c.World().Block(s.openedPos.Load()).(type) {
    94  				case block.SmithingTable:
    95  					err, special = h.handleSmithing(a, s), true
    96  				case block.Stonecutter:
    97  					err, special = h.handleStonecutting(a, s), true
    98  				case block.EnchantingTable:
    99  					err, special = h.handleEnchant(a, s), true
   100  				}
   101  				if special {
   102  					// This was a "special action" and was handled, so we can move onto the next action.
   103  					break
   104  				}
   105  			}
   106  			err = h.handleCraft(a, s)
   107  		case *protocol.AutoCraftRecipeStackRequestAction:
   108  			err = h.handleAutoCraft(a, s)
   109  		case *protocol.CraftRecipeOptionalStackRequestAction:
   110  			err = h.handleCraftRecipeOptional(a, s, req.FilterStrings)
   111  		case *protocol.CraftLoomRecipeStackRequestAction:
   112  			err = h.handleLoomCraft(a, s)
   113  		case *protocol.CraftGrindstoneRecipeStackRequestAction:
   114  			err = h.handleGrindstoneCraft(s)
   115  		case *protocol.CraftCreativeStackRequestAction:
   116  			err = h.handleCreativeCraft(a, s)
   117  		case *protocol.MineBlockStackRequestAction:
   118  			err = h.handleMineBlock(a, s)
   119  		case *protocol.CreateStackRequestAction:
   120  			err = h.handleCreate(a, s)
   121  		case *protocol.ConsumeStackRequestAction, *protocol.CraftResultsDeprecatedStackRequestAction:
   122  			// Don't do anything with this.
   123  		default:
   124  			return fmt.Errorf("unhandled stack request action %#v", action)
   125  		}
   126  		if err != nil {
   127  			err = fmt.Errorf("%T: %w", action, err)
   128  			return
   129  		}
   130  	}
   131  	return
   132  }
   133  
   134  // handleTake handles a Take stack request action.
   135  func (h *ItemStackRequestHandler) handleTake(a *protocol.TakeStackRequestAction, s *Session) error {
   136  	return h.handleTransfer(a.Source, a.Destination, a.Count, s)
   137  }
   138  
   139  // handlePlace handles a Place stack request action.
   140  func (h *ItemStackRequestHandler) handlePlace(a *protocol.PlaceStackRequestAction, s *Session) error {
   141  	return h.handleTransfer(a.Source, a.Destination, a.Count, s)
   142  }
   143  
   144  // handleTransfer handles the transferring of x count from a source slot to a destination slot.
   145  func (h *ItemStackRequestHandler) handleTransfer(from, to protocol.StackRequestSlotInfo, count byte, s *Session) error {
   146  	if err := h.verifySlots(s, from, to); err != nil {
   147  		return fmt.Errorf("source slot out of sync: %w", err)
   148  	}
   149  	i, _ := h.itemInSlot(from, s)
   150  	dest, _ := h.itemInSlot(to, s)
   151  	if !i.Comparable(dest) {
   152  		return fmt.Errorf("client tried transferring %v to %v, but the stacks are incomparable", i, dest)
   153  	}
   154  	if i.Count() < int(count) {
   155  		return fmt.Errorf("client tried subtracting %v from item count, but there are only %v", count, i.Count())
   156  	}
   157  	if (dest.Count()+int(count) > dest.MaxCount()) && !dest.Empty() {
   158  		return fmt.Errorf("client tried adding %v to item count %v, but max is %v", count, dest.Count(), dest.MaxCount())
   159  	}
   160  	if dest.Empty() {
   161  		dest = i.Grow(-math.MaxInt32)
   162  	}
   163  
   164  	invA, _ := s.invByID(int32(from.ContainerID))
   165  	invB, _ := s.invByID(int32(to.ContainerID))
   166  
   167  	ctx := event.C()
   168  	_ = call(ctx, int(from.Slot), i.Grow(int(count)-i.Count()), invA.Handler().HandleTake)
   169  	err := call(ctx, int(to.Slot), i.Grow(int(count)-i.Count()), invB.Handler().HandlePlace)
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	h.setItemInSlot(from, i.Grow(-int(count)), s)
   175  	h.setItemInSlot(to, dest.Grow(int(count)), s)
   176  	h.collectRewards(s, invA, int(from.Slot))
   177  	return nil
   178  }
   179  
   180  // handleSwap handles a Swap stack request action.
   181  func (h *ItemStackRequestHandler) handleSwap(a *protocol.SwapStackRequestAction, s *Session) error {
   182  	if err := h.verifySlots(s, a.Source, a.Destination); err != nil {
   183  		return fmt.Errorf("slot out of sync: %w", err)
   184  	}
   185  	i, _ := h.itemInSlot(a.Source, s)
   186  	dest, _ := h.itemInSlot(a.Destination, s)
   187  
   188  	invA, _ := s.invByID(int32(a.Source.ContainerID))
   189  	invB, _ := s.invByID(int32(a.Destination.ContainerID))
   190  
   191  	ctx := event.C()
   192  	_ = call(ctx, int(a.Source.Slot), i, invA.Handler().HandleTake)
   193  	_ = call(ctx, int(a.Source.Slot), dest, invA.Handler().HandlePlace)
   194  	_ = call(ctx, int(a.Destination.Slot), dest, invB.Handler().HandleTake)
   195  	err := call(ctx, int(a.Destination.Slot), i, invB.Handler().HandlePlace)
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	h.setItemInSlot(a.Source, dest, s)
   201  	h.setItemInSlot(a.Destination, i, s)
   202  	h.collectRewards(s, invA, int(a.Source.Slot))
   203  	h.collectRewards(s, invA, int(a.Destination.Slot))
   204  	return nil
   205  }
   206  
   207  // collectRewards checks if the source inventory has rewards for the player, for example, experience rewards when
   208  // smelting. If it does, it will drop the rewards at the player's location.
   209  func (h *ItemStackRequestHandler) collectRewards(s *Session, inv *inventory.Inventory, slot int) {
   210  	w := s.c.World()
   211  	if inv == s.openedWindow.Load() && s.containerOpened.Load() && slot == inv.Size()-1 {
   212  		if f, ok := w.Block(s.openedPos.Load()).(smelter); ok {
   213  			for _, o := range entity.NewExperienceOrbs(entity.EyePosition(s.c), f.ResetExperience()) {
   214  				o.SetVelocity(mgl64.Vec3{(rand.Float64()*0.2 - 0.1) * 2, rand.Float64() * 0.4, (rand.Float64()*0.2 - 0.1) * 2})
   215  				w.AddEntity(o)
   216  			}
   217  		}
   218  	}
   219  }
   220  
   221  // handleDestroy handles the destroying of an item by moving it into the creative inventory.
   222  func (h *ItemStackRequestHandler) handleDestroy(a *protocol.DestroyStackRequestAction, s *Session) error {
   223  	if h.ignoreDestroy {
   224  		return nil
   225  	}
   226  	if !s.c.GameMode().CreativeInventory() {
   227  		return fmt.Errorf("can only destroy items in gamemode creative/spectator")
   228  	}
   229  	if err := h.verifySlot(a.Source, s); err != nil {
   230  		return fmt.Errorf("source slot out of sync: %w", err)
   231  	}
   232  	i, _ := h.itemInSlot(a.Source, s)
   233  	if i.Count() < int(a.Count) {
   234  		return fmt.Errorf("client attempted to destroy %v items, but only %v present", a.Count, i.Count())
   235  	}
   236  
   237  	h.setItemInSlot(a.Source, i.Grow(-int(a.Count)), s)
   238  	return nil
   239  }
   240  
   241  // handleDrop handles the dropping of an item by moving it outside the inventory while having the
   242  // inventory opened.
   243  func (h *ItemStackRequestHandler) handleDrop(a *protocol.DropStackRequestAction, s *Session) error {
   244  	if err := h.verifySlot(a.Source, s); err != nil {
   245  		return fmt.Errorf("source slot out of sync: %w", err)
   246  	}
   247  	i, _ := h.itemInSlot(a.Source, s)
   248  	if i.Count() < int(a.Count) {
   249  		return fmt.Errorf("client attempted to drop %v items, but only %v present", a.Count, i.Count())
   250  	}
   251  
   252  	inv, _ := s.invByID(int32(a.Source.ContainerID))
   253  	if err := call(event.C(), int(a.Source.Slot), i.Grow(int(a.Count)-i.Count()), inv.Handler().HandleDrop); err != nil {
   254  		return err
   255  	}
   256  
   257  	n := s.c.Drop(i.Grow(int(a.Count) - i.Count()))
   258  	h.setItemInSlot(a.Source, i.Grow(-n), s)
   259  	return nil
   260  }
   261  
   262  // handleMineBlock handles the action associated with a block being mined by the player. This seems to be a workaround
   263  // by Mojang to deal with the durability changes client-side.
   264  func (h *ItemStackRequestHandler) handleMineBlock(a *protocol.MineBlockStackRequestAction, s *Session) error {
   265  	slot := protocol.StackRequestSlotInfo{
   266  		ContainerID:    protocol.ContainerInventory,
   267  		Slot:           byte(a.HotbarSlot),
   268  		StackNetworkID: a.StackNetworkID,
   269  	}
   270  	if err := h.verifySlot(slot, s); err != nil {
   271  		return err
   272  	}
   273  
   274  	// Update the slots through ItemStackResponses, don't actually do anything special with this action.
   275  	i, _ := h.itemInSlot(slot, s)
   276  	h.setItemInSlot(slot, i, s)
   277  	return nil
   278  }
   279  
   280  // handleCreate handles the CreateStackRequestAction sent by the client when a recipe outputs more than one item. It
   281  // contains a result slot, which should map to one of the output items. From there, the server should create the relevant
   282  // output as usual.
   283  func (h *ItemStackRequestHandler) handleCreate(a *protocol.CreateStackRequestAction, s *Session) error {
   284  	slot := int(a.ResultsSlot)
   285  	if len(h.pendingResults) < slot {
   286  		return fmt.Errorf("invalid pending result slot: %v", a.ResultsSlot)
   287  	}
   288  
   289  	res := h.pendingResults[slot]
   290  	if res.Empty() {
   291  		return fmt.Errorf("tried duplicating created result: %v", slot)
   292  	}
   293  	h.pendingResults[slot] = item.Stack{}
   294  
   295  	h.setItemInSlot(protocol.StackRequestSlotInfo{
   296  		ContainerID: protocol.ContainerCreatedOutput,
   297  		Slot:        craftingResult,
   298  	}, res, s)
   299  	return nil
   300  }
   301  
   302  // defaultCreation represents the CreateStackRequestAction used for single-result crafts.
   303  var defaultCreation = &protocol.CreateStackRequestAction{}
   304  
   305  // createResults creates a new craft result and adds it to the list of pending craft results.
   306  func (h *ItemStackRequestHandler) createResults(s *Session, result ...item.Stack) error {
   307  	h.pendingResults = append(h.pendingResults, result...)
   308  	if len(result) > 1 {
   309  		// With multiple results, the client notifies the server on when to create the results.
   310  		return nil
   311  	}
   312  	return h.handleCreate(defaultCreation, s)
   313  }
   314  
   315  // verifySlots verifies a list of slots passed.
   316  func (h *ItemStackRequestHandler) verifySlots(s *Session, slots ...protocol.StackRequestSlotInfo) error {
   317  	for _, slot := range slots {
   318  		if err := h.verifySlot(slot, s); err != nil {
   319  			return err
   320  		}
   321  	}
   322  	return nil
   323  }
   324  
   325  // verifySlot checks if the slot passed by the client is the same as that expected by the server.
   326  func (h *ItemStackRequestHandler) verifySlot(slot protocol.StackRequestSlotInfo, s *Session) error {
   327  	if err := h.tryAcknowledgeChanges(s, slot); err != nil {
   328  		return err
   329  	}
   330  	if len(h.responseChanges) > 256 {
   331  		return fmt.Errorf("too many unacknowledged request slot changes")
   332  	}
   333  	inv, _ := s.invByID(int32(slot.ContainerID))
   334  
   335  	i, err := h.itemInSlot(slot, s)
   336  	if err != nil {
   337  		return err
   338  	}
   339  	clientID, err := h.resolveID(inv, slot)
   340  	if err != nil {
   341  		return err
   342  	}
   343  	// The client seems to send negative stack network IDs for predictions, which we can ignore. We'll simply
   344  	// override this network ID later.
   345  	if id := item_id(i); id != clientID {
   346  		return fmt.Errorf("stack ID mismatch: client expected %v, but server had %v", clientID, id)
   347  	}
   348  	return nil
   349  }
   350  
   351  // resolveID resolves the stack network ID in the slot passed. If it is negative, it points to an earlier
   352  // request, in which case it will look it up in the changes of an earlier response to a request to find the
   353  // actual stack network ID in the slot. If it is positive, the ID will be returned again.
   354  func (h *ItemStackRequestHandler) resolveID(inv *inventory.Inventory, slot protocol.StackRequestSlotInfo) (int32, error) {
   355  	if slot.StackNetworkID >= 0 {
   356  		return slot.StackNetworkID, nil
   357  	}
   358  	containerChanges, ok := h.responseChanges[slot.StackNetworkID]
   359  	if !ok {
   360  		return 0, fmt.Errorf("slot pointed to stack request %v, but request could not be found", slot.StackNetworkID)
   361  	}
   362  	changes, ok := containerChanges[inv]
   363  	if !ok {
   364  		return 0, fmt.Errorf("slot pointed to stack request %v with container %v, but that container was not changed in the request", slot.StackNetworkID, slot.ContainerID)
   365  	}
   366  	actual, ok := changes[slot.Slot]
   367  	if !ok {
   368  		return 0, fmt.Errorf("slot pointed to stack request %v with container %v and slot %v, but that slot was not changed in the request", slot.StackNetworkID, slot.ContainerID, slot.Slot)
   369  	}
   370  	return actual.id, nil
   371  }
   372  
   373  // tryAcknowledgeChanges iterates through all cached response changes and checks if the stack request slot
   374  // info passed from the client has the right stack network ID in any of the stored slots. If this is the case,
   375  // that entry is removed, so that the maps are cleaned up eventually.
   376  func (h *ItemStackRequestHandler) tryAcknowledgeChanges(s *Session, slot protocol.StackRequestSlotInfo) error {
   377  	inv, ok := s.invByID(int32(slot.ContainerID))
   378  	if !ok {
   379  		return fmt.Errorf("could not find container with id %v", slot.ContainerID)
   380  	}
   381  
   382  	for requestID, containerChanges := range h.responseChanges {
   383  		for newInv, changes := range containerChanges {
   384  			for slotIndex, val := range changes {
   385  				if (slot.Slot == slotIndex && slot.StackNetworkID >= 0 && newInv == inv) || h.current.Sub(val.timestamp) > time.Second*5 {
   386  					delete(changes, slotIndex)
   387  				}
   388  			}
   389  			if len(changes) == 0 {
   390  				delete(containerChanges, newInv)
   391  			}
   392  		}
   393  		if len(containerChanges) == 0 {
   394  			delete(h.responseChanges, requestID)
   395  		}
   396  	}
   397  	return nil
   398  }
   399  
   400  // itemInSlot looks for the item in the slot as indicated by the slot info passed.
   401  func (h *ItemStackRequestHandler) itemInSlot(slot protocol.StackRequestSlotInfo, s *Session) (item.Stack, error) {
   402  	inv, ok := s.invByID(int32(slot.ContainerID))
   403  	if !ok {
   404  		return item.Stack{}, fmt.Errorf("unable to find container with ID %v", slot.ContainerID)
   405  	}
   406  
   407  	sl := int(slot.Slot)
   408  	if inv == s.offHand {
   409  		sl = 0
   410  	}
   411  
   412  	i, err := inv.Item(sl)
   413  	if err != nil {
   414  		return i, err
   415  	}
   416  	return i, nil
   417  }
   418  
   419  // setItemInSlot sets an item stack in the slot of a container present in the slot info.
   420  func (h *ItemStackRequestHandler) setItemInSlot(slot protocol.StackRequestSlotInfo, i item.Stack, s *Session) {
   421  	inv, _ := s.invByID(int32(slot.ContainerID))
   422  
   423  	sl := int(slot.Slot)
   424  	if inv == s.offHand {
   425  		sl = 0
   426  	}
   427  
   428  	before, _ := inv.Item(sl)
   429  	_ = inv.SetItem(sl, i)
   430  
   431  	respSlot := protocol.StackResponseSlotInfo{
   432  		Slot:                 slot.Slot,
   433  		HotbarSlot:           slot.Slot,
   434  		Count:                byte(i.Count()),
   435  		StackNetworkID:       item_id(i),
   436  		DurabilityCorrection: int32(i.MaxDurability() - i.Durability()),
   437  	}
   438  
   439  	if h.changes[slot.ContainerID] == nil {
   440  		h.changes[slot.ContainerID] = map[byte]changeInfo{}
   441  	}
   442  	h.changes[slot.ContainerID][slot.Slot] = changeInfo{
   443  		after:  respSlot,
   444  		before: before,
   445  	}
   446  
   447  	if h.responseChanges[h.currentRequest] == nil {
   448  		h.responseChanges[h.currentRequest] = map[*inventory.Inventory]map[byte]responseChange{}
   449  	}
   450  	if h.responseChanges[h.currentRequest][inv] == nil {
   451  		h.responseChanges[h.currentRequest][inv] = map[byte]responseChange{}
   452  	}
   453  	h.responseChanges[h.currentRequest][inv][slot.Slot] = responseChange{
   454  		id:        respSlot.StackNetworkID,
   455  		timestamp: h.current,
   456  	}
   457  }
   458  
   459  // resolve resolves the request with the ID passed.
   460  func (h *ItemStackRequestHandler) resolve(id int32, s *Session) {
   461  	info := make([]protocol.StackResponseContainerInfo, 0, len(h.changes))
   462  	for container, slotInfo := range h.changes {
   463  		slots := make([]protocol.StackResponseSlotInfo, 0, len(slotInfo))
   464  		for _, slot := range slotInfo {
   465  			slots = append(slots, slot.after)
   466  		}
   467  		info = append(info, protocol.StackResponseContainerInfo{
   468  			ContainerID: container,
   469  			SlotInfo:    slots,
   470  		})
   471  	}
   472  	s.writePacket(&packet.ItemStackResponse{Responses: []protocol.ItemStackResponse{{
   473  		Status:        protocol.ItemStackResponseStatusOK,
   474  		RequestID:     id,
   475  		ContainerInfo: info,
   476  	}}})
   477  
   478  	h.changes = map[byte]map[byte]changeInfo{}
   479  	h.pendingResults = nil
   480  }
   481  
   482  // reject rejects the item stack request sent by the client so that it is reverted client-side.
   483  func (h *ItemStackRequestHandler) reject(id int32, s *Session) {
   484  	s.writePacket(&packet.ItemStackResponse{
   485  		Responses: []protocol.ItemStackResponse{{
   486  			Status:    protocol.ItemStackResponseStatusError,
   487  			RequestID: id,
   488  		}},
   489  	})
   490  
   491  	// Revert changes that we already made for valid actions.
   492  	for container, slots := range h.changes {
   493  		for slot, info := range slots {
   494  			inv, _ := s.invByID(int32(container))
   495  			_ = inv.SetItem(int(slot), info.before)
   496  		}
   497  	}
   498  
   499  	h.changes = map[byte]map[byte]changeInfo{}
   500  	h.pendingResults = nil
   501  }
   502  
   503  // call uses an event.Context, slot and item.Stack to call the event handler function passed. An error is returned if
   504  // the event.Context was cancelled either before or after the call.
   505  func call(ctx *event.Context, slot int, it item.Stack, f func(ctx *event.Context, slot int, it item.Stack)) error {
   506  	if ctx.Cancelled() {
   507  		return fmt.Errorf("action was cancelled")
   508  	}
   509  	f(ctx, slot, it)
   510  	if ctx.Cancelled() {
   511  		return fmt.Errorf("action was cancelled")
   512  	}
   513  	return nil
   514  }