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 }