512 lines
19 KiB
Go
512 lines
19 KiB
Go
|
|
package session
|
||
|
|
|
||
|
|
import (
|
||
|
|
"fmt"
|
||
|
|
"github.com/df-mc/dragonfly/server/block"
|
||
|
|
"github.com/df-mc/dragonfly/server/entity"
|
||
|
|
"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"
|
||
|
|
"math"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ItemStackRequestHandler handles the ItemStackRequest packet. It handles the actions done within the
|
||
|
|
// inventory.
|
||
|
|
type ItemStackRequestHandler struct {
|
||
|
|
currentRequest int32
|
||
|
|
|
||
|
|
changes map[byte]map[byte]changeInfo
|
||
|
|
responseChanges map[int32]map[*inventory.Inventory]map[byte]responseChange
|
||
|
|
|
||
|
|
pendingResults []item.Stack
|
||
|
|
|
||
|
|
current time.Time
|
||
|
|
ignoreDestroy bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// responseChange represents a change in a specific item stack response. It holds the timestamp of the
|
||
|
|
// response which is used to get rid of changes that the client will have received.
|
||
|
|
type responseChange struct {
|
||
|
|
id int32
|
||
|
|
timestamp time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
// changeInfo holds information on a slot change initiated by an item stack request. It holds both the new and the old
|
||
|
|
// item information and is used for reverting and verifying.
|
||
|
|
type changeInfo struct {
|
||
|
|
after protocol.StackResponseSlotInfo
|
||
|
|
before item.Stack
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle ...
|
||
|
|
func (h *ItemStackRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
pk := p.(*packet.ItemStackRequest)
|
||
|
|
h.current = time.Now()
|
||
|
|
|
||
|
|
s.inTransaction.Store(true)
|
||
|
|
defer s.inTransaction.Store(false)
|
||
|
|
|
||
|
|
for _, req := range pk.Requests {
|
||
|
|
if err := h.handleRequest(req, s, tx, c); err != nil {
|
||
|
|
// Item stacks being out of sync isn't uncommon, so don't error. Just debug the error and let the
|
||
|
|
// revert do its work.
|
||
|
|
s.conf.Log.Debug("process packet: ItemStackRequest: resolve item stack request: " + err.Error())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleRequest resolves a single item stack request from the client.
|
||
|
|
func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s *Session, tx *world.Tx, c Controllable) (err error) {
|
||
|
|
h.currentRequest = req.RequestID
|
||
|
|
defer func() {
|
||
|
|
if err != nil {
|
||
|
|
h.reject(req.RequestID, s, tx)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
h.resolve(req.RequestID, s)
|
||
|
|
h.ignoreDestroy = false
|
||
|
|
}()
|
||
|
|
|
||
|
|
for _, action := range req.Actions {
|
||
|
|
switch a := action.(type) {
|
||
|
|
case *protocol.TakeStackRequestAction:
|
||
|
|
err = h.handleTake(a, s, tx, c)
|
||
|
|
case *protocol.PlaceStackRequestAction:
|
||
|
|
err = h.handlePlace(a, s, tx, c)
|
||
|
|
case *protocol.SwapStackRequestAction:
|
||
|
|
err = h.handleSwap(a, s, tx, c)
|
||
|
|
case *protocol.DestroyStackRequestAction:
|
||
|
|
err = h.handleDestroy(a, s, tx, c)
|
||
|
|
case *protocol.DropStackRequestAction:
|
||
|
|
err = h.handleDrop(a, s, tx, c)
|
||
|
|
case *protocol.BeaconPaymentStackRequestAction:
|
||
|
|
err = h.handleBeaconPayment(a, s, tx)
|
||
|
|
case *protocol.CraftRecipeStackRequestAction:
|
||
|
|
if s.containerOpened.Load() {
|
||
|
|
var special bool
|
||
|
|
switch tx.Block(*s.openedPos.Load()).(type) {
|
||
|
|
case block.SmithingTable:
|
||
|
|
err, special = h.handleSmithing(a, s, tx), true
|
||
|
|
case block.Stonecutter:
|
||
|
|
err, special = h.handleStonecutting(a, s, tx), true
|
||
|
|
case block.EnchantingTable:
|
||
|
|
err, special = h.handleEnchant(a, s, tx, c), true
|
||
|
|
}
|
||
|
|
if special {
|
||
|
|
// This was a "special action" and was handled, so we can move onto the next action.
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
err = h.handleCraft(a, s, tx)
|
||
|
|
case *protocol.AutoCraftRecipeStackRequestAction:
|
||
|
|
err = h.handleAutoCraft(a, s, tx)
|
||
|
|
case *protocol.CraftRecipeOptionalStackRequestAction:
|
||
|
|
err = h.handleCraftRecipeOptional(a, s, req.FilterStrings, c, tx)
|
||
|
|
case *protocol.CraftLoomRecipeStackRequestAction:
|
||
|
|
err = h.handleLoomCraft(a, s, tx)
|
||
|
|
case *protocol.CraftGrindstoneRecipeStackRequestAction:
|
||
|
|
err = h.handleGrindstoneCraft(s, tx, c)
|
||
|
|
case *protocol.CraftCreativeStackRequestAction:
|
||
|
|
err = h.handleCreativeCraft(a, s, tx, c)
|
||
|
|
case *protocol.MineBlockStackRequestAction:
|
||
|
|
err = h.handleMineBlock(a, s, tx)
|
||
|
|
case *protocol.CreateStackRequestAction:
|
||
|
|
err = h.handleCreate(a, s, tx)
|
||
|
|
case *protocol.ConsumeStackRequestAction, *protocol.CraftResultsDeprecatedStackRequestAction:
|
||
|
|
// Don't do anything with this.
|
||
|
|
default:
|
||
|
|
return fmt.Errorf("unhandled stack request action %#v", action)
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
err = fmt.Errorf("%T: %w", action, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleTake handles a Take stack request action.
|
||
|
|
func (h *ItemStackRequestHandler) handleTake(a *protocol.TakeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
return h.handleTransfer(a.Source, a.Destination, a.Count, s, tx, c)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handlePlace handles a Place stack request action.
|
||
|
|
func (h *ItemStackRequestHandler) handlePlace(a *protocol.PlaceStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
return h.handleTransfer(a.Source, a.Destination, a.Count, s, tx, c)
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleTransfer handles the transferring of x count from a source slot to a destination slot.
|
||
|
|
func (h *ItemStackRequestHandler) handleTransfer(from, to protocol.StackRequestSlotInfo, count byte, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
if err := h.verifySlots(s, tx, from, to); err != nil {
|
||
|
|
return fmt.Errorf("source slot out of sync: %w", err)
|
||
|
|
}
|
||
|
|
i, _ := h.itemInSlot(from, s, tx)
|
||
|
|
dest, _ := h.itemInSlot(to, s, tx)
|
||
|
|
if !i.Comparable(dest) {
|
||
|
|
return fmt.Errorf("client tried transferring %v to %v, but the stacks are incomparable", i, dest)
|
||
|
|
}
|
||
|
|
if i.Count() < int(count) {
|
||
|
|
return fmt.Errorf("client tried subtracting %v from item count, but there are only %v", count, i.Count())
|
||
|
|
}
|
||
|
|
if (dest.Count()+int(count) > dest.MaxCount()) && !dest.Empty() {
|
||
|
|
return fmt.Errorf("client tried adding %v to item count %v, but max is %v", count, dest.Count(), dest.MaxCount())
|
||
|
|
}
|
||
|
|
if dest.Empty() {
|
||
|
|
dest = i.Grow(-math.MaxInt32)
|
||
|
|
}
|
||
|
|
|
||
|
|
invA, _ := s.invByID(int32(from.Container.ContainerID), tx)
|
||
|
|
invB, _ := s.invByID(int32(to.Container.ContainerID), tx)
|
||
|
|
|
||
|
|
ctx := event.C(inventory.Holder(c))
|
||
|
|
_ = call(ctx, int(from.Slot), i.Grow(int(count)-i.Count()), invA.Handler().HandleTake)
|
||
|
|
err := call(ctx, int(to.Slot), i.Grow(int(count)-i.Count()), invB.Handler().HandlePlace)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
h.setItemInSlot(from, i.Grow(-int(count)), s, tx)
|
||
|
|
h.setItemInSlot(to, dest.Grow(int(count)), s, tx)
|
||
|
|
h.collectRewards(s, invA, int(from.Slot), tx, c)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleSwap handles a Swap stack request action.
|
||
|
|
func (h *ItemStackRequestHandler) handleSwap(a *protocol.SwapStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
if err := h.verifySlots(s, tx, a.Source, a.Destination); err != nil {
|
||
|
|
return fmt.Errorf("slot out of sync: %w", err)
|
||
|
|
}
|
||
|
|
i, _ := h.itemInSlot(a.Source, s, tx)
|
||
|
|
dest, _ := h.itemInSlot(a.Destination, s, tx)
|
||
|
|
|
||
|
|
invA, _ := s.invByID(int32(a.Source.Container.ContainerID), tx)
|
||
|
|
invB, _ := s.invByID(int32(a.Destination.Container.ContainerID), tx)
|
||
|
|
|
||
|
|
ctx := event.C(inventory.Holder(c))
|
||
|
|
_ = call(ctx, int(a.Source.Slot), i, invA.Handler().HandleTake)
|
||
|
|
_ = call(ctx, int(a.Source.Slot), dest, invA.Handler().HandlePlace)
|
||
|
|
_ = call(ctx, int(a.Destination.Slot), dest, invB.Handler().HandleTake)
|
||
|
|
err := call(ctx, int(a.Destination.Slot), i, invB.Handler().HandlePlace)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
h.setItemInSlot(a.Source, dest, s, tx)
|
||
|
|
h.setItemInSlot(a.Destination, i, s, tx)
|
||
|
|
h.collectRewards(s, invA, int(a.Source.Slot), tx, c)
|
||
|
|
h.collectRewards(s, invA, int(a.Destination.Slot), tx, c)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// collectRewards checks if the source inventory has rewards for the player, for example, experience rewards when
|
||
|
|
// smelting. If it does, it will drop the rewards at the player's location.
|
||
|
|
func (h *ItemStackRequestHandler) collectRewards(s *Session, inv *inventory.Inventory, slot int, tx *world.Tx, c Controllable) {
|
||
|
|
if inv == s.openedWindow.Load() && s.containerOpened.Load() && slot == inv.Size()-1 {
|
||
|
|
if f, ok := tx.Block(*s.openedPos.Load()).(smelter); ok {
|
||
|
|
for _, o := range entity.NewExperienceOrbs(entity.EyePosition(c), f.ResetExperience()) {
|
||
|
|
tx.AddEntity(o)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleDestroy handles the destroying of an item by moving it into the creative inventory.
|
||
|
|
func (h *ItemStackRequestHandler) handleDestroy(a *protocol.DestroyStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
if h.ignoreDestroy {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if !c.GameMode().CreativeInventory() {
|
||
|
|
return fmt.Errorf("can only destroy items in gamemode creative/spectator")
|
||
|
|
}
|
||
|
|
if err := h.verifySlot(a.Source, s, tx); err != nil {
|
||
|
|
return fmt.Errorf("source slot out of sync: %w", err)
|
||
|
|
}
|
||
|
|
i, _ := h.itemInSlot(a.Source, s, tx)
|
||
|
|
if i.Count() < int(a.Count) {
|
||
|
|
return fmt.Errorf("client attempted to destroy %v items, but only %v present", a.Count, i.Count())
|
||
|
|
}
|
||
|
|
|
||
|
|
h.setItemInSlot(a.Source, i.Grow(-int(a.Count)), s, tx)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleDrop handles the dropping of an item by moving it outside the inventory while having the
|
||
|
|
// inventory opened.
|
||
|
|
func (h *ItemStackRequestHandler) handleDrop(a *protocol.DropStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||
|
|
if err := h.verifySlot(a.Source, s, tx); err != nil {
|
||
|
|
return fmt.Errorf("source slot out of sync: %w", err)
|
||
|
|
}
|
||
|
|
i, _ := h.itemInSlot(a.Source, s, tx)
|
||
|
|
if i.Count() < int(a.Count) {
|
||
|
|
return fmt.Errorf("client attempted to drop %v items, but only %v present", a.Count, i.Count())
|
||
|
|
}
|
||
|
|
|
||
|
|
inv, _ := s.invByID(int32(a.Source.Container.ContainerID), tx)
|
||
|
|
if err := call(event.C(inventory.Holder(c)), int(a.Source.Slot), i.Grow(int(a.Count)-i.Count()), inv.Handler().HandleDrop); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
n := c.Drop(i.Grow(int(a.Count) - i.Count()))
|
||
|
|
h.setItemInSlot(a.Source, i.Grow(-n), s, tx)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleMineBlock handles the action associated with a block being mined by the player. This seems to be a workaround
|
||
|
|
// by Mojang to deal with the durability changes client-side.
|
||
|
|
func (h *ItemStackRequestHandler) handleMineBlock(a *protocol.MineBlockStackRequestAction, s *Session, tx *world.Tx) error {
|
||
|
|
slot := protocol.StackRequestSlotInfo{
|
||
|
|
Container: protocol.FullContainerName{ContainerID: protocol.ContainerInventory},
|
||
|
|
Slot: byte(a.HotbarSlot),
|
||
|
|
StackNetworkID: a.StackNetworkID,
|
||
|
|
}
|
||
|
|
if err := h.verifySlot(slot, s, tx); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update the slots through ItemStackResponses, don't actually do anything special with this action.
|
||
|
|
i, _ := h.itemInSlot(slot, s, tx)
|
||
|
|
h.setItemInSlot(slot, i, s, tx)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// handleCreate handles the CreateStackRequestAction sent by the client when a recipe outputs more than one item. It
|
||
|
|
// contains a result slot, which should map to one of the output items. From there, the server should create the relevant
|
||
|
|
// output as usual.
|
||
|
|
func (h *ItemStackRequestHandler) handleCreate(a *protocol.CreateStackRequestAction, s *Session, tx *world.Tx) error {
|
||
|
|
slot := int(a.ResultsSlot)
|
||
|
|
if slot >= len(h.pendingResults) {
|
||
|
|
return fmt.Errorf("invalid pending result slot: %v", a.ResultsSlot)
|
||
|
|
}
|
||
|
|
|
||
|
|
res := h.pendingResults[slot]
|
||
|
|
if res.Empty() {
|
||
|
|
return fmt.Errorf("tried duplicating created result: %v", slot)
|
||
|
|
}
|
||
|
|
h.pendingResults[slot] = item.Stack{}
|
||
|
|
|
||
|
|
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||
|
|
Container: protocol.FullContainerName{ContainerID: protocol.ContainerCreatedOutput},
|
||
|
|
Slot: craftingResult,
|
||
|
|
}, res, s, tx)
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// defaultCreation represents the CreateStackRequestAction used for single-result crafts.
|
||
|
|
var defaultCreation = &protocol.CreateStackRequestAction{}
|
||
|
|
|
||
|
|
// createResults creates a new craft result and adds it to the list of pending craft results.
|
||
|
|
func (h *ItemStackRequestHandler) createResults(s *Session, tx *world.Tx, result ...item.Stack) error {
|
||
|
|
h.pendingResults = append(h.pendingResults, result...)
|
||
|
|
if len(result) > 1 {
|
||
|
|
// With multiple results, the client notifies the server on when to create the results.
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
return h.handleCreate(defaultCreation, s, tx)
|
||
|
|
}
|
||
|
|
|
||
|
|
// verifySlots verifies a list of slots passed.
|
||
|
|
func (h *ItemStackRequestHandler) verifySlots(s *Session, tx *world.Tx, slots ...protocol.StackRequestSlotInfo) error {
|
||
|
|
for _, slot := range slots {
|
||
|
|
if err := h.verifySlot(slot, s, tx); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// verifySlot checks if the slot passed by the client is the same as that expected by the server.
|
||
|
|
func (h *ItemStackRequestHandler) verifySlot(slot protocol.StackRequestSlotInfo, s *Session, tx *world.Tx) error {
|
||
|
|
if err := h.tryAcknowledgeChanges(s, tx, slot); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if len(h.responseChanges) > 256 {
|
||
|
|
return fmt.Errorf("too many unacknowledged request slot changes")
|
||
|
|
}
|
||
|
|
inv, _ := s.invByID(int32(slot.Container.ContainerID), tx)
|
||
|
|
|
||
|
|
i, err := h.itemInSlot(slot, s, tx)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
clientID, err := h.resolveID(inv, slot)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
// The client seems to send negative stack network IDs for predictions, which we can ignore. We'll simply
|
||
|
|
// override this network ID later.
|
||
|
|
if id := item_id(i); id != clientID {
|
||
|
|
return fmt.Errorf("stack ID mismatch: client expected %v, but server had %v", clientID, id)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// resolveID resolves the stack network ID in the slot passed. If it is negative, it points to an earlier
|
||
|
|
// request, in which case it will look it up in the changes of an earlier response to a request to find the
|
||
|
|
// actual stack network ID in the slot. If it is positive, the ID will be returned again.
|
||
|
|
func (h *ItemStackRequestHandler) resolveID(inv *inventory.Inventory, slot protocol.StackRequestSlotInfo) (int32, error) {
|
||
|
|
if slot.StackNetworkID >= 0 {
|
||
|
|
return slot.StackNetworkID, nil
|
||
|
|
}
|
||
|
|
containerChanges, ok := h.responseChanges[slot.StackNetworkID]
|
||
|
|
if !ok {
|
||
|
|
return 0, fmt.Errorf("slot pointed to stack request %v, but request could not be found", slot.StackNetworkID)
|
||
|
|
}
|
||
|
|
changes, ok := containerChanges[inv]
|
||
|
|
if !ok {
|
||
|
|
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.Container.ContainerID)
|
||
|
|
}
|
||
|
|
actual, ok := changes[slot.Slot]
|
||
|
|
if !ok {
|
||
|
|
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.Container.ContainerID, slot.Slot)
|
||
|
|
}
|
||
|
|
return actual.id, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// tryAcknowledgeChanges iterates through all cached response changes and checks if the stack request slot
|
||
|
|
// info passed from the client has the right stack network ID in any of the stored slots. If this is the case,
|
||
|
|
// that entry is removed, so that the maps are cleaned up eventually.
|
||
|
|
func (h *ItemStackRequestHandler) tryAcknowledgeChanges(s *Session, tx *world.Tx, slot protocol.StackRequestSlotInfo) error {
|
||
|
|
inv, ok := s.invByID(int32(slot.Container.ContainerID), tx)
|
||
|
|
if !ok {
|
||
|
|
return fmt.Errorf("could not find container with id %v", slot.Container.ContainerID)
|
||
|
|
}
|
||
|
|
|
||
|
|
for requestID, containerChanges := range h.responseChanges {
|
||
|
|
for newInv, changes := range containerChanges {
|
||
|
|
for slotIndex, val := range changes {
|
||
|
|
if (slot.Slot == slotIndex && slot.StackNetworkID >= 0 && newInv == inv) || h.current.Sub(val.timestamp) > time.Second*5 {
|
||
|
|
delete(changes, slotIndex)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(changes) == 0 {
|
||
|
|
delete(containerChanges, newInv)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(containerChanges) == 0 {
|
||
|
|
delete(h.responseChanges, requestID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// itemInSlot looks for the item in the slot as indicated by the slot info passed.
|
||
|
|
func (h *ItemStackRequestHandler) itemInSlot(slot protocol.StackRequestSlotInfo, s *Session, tx *world.Tx) (item.Stack, error) {
|
||
|
|
inv, ok := s.invByID(int32(slot.Container.ContainerID), tx)
|
||
|
|
if !ok {
|
||
|
|
return item.Stack{}, fmt.Errorf("unable to find container with ID %v", slot.Container.ContainerID)
|
||
|
|
}
|
||
|
|
|
||
|
|
sl := int(slot.Slot)
|
||
|
|
if inv == s.offHand {
|
||
|
|
sl = 0
|
||
|
|
}
|
||
|
|
|
||
|
|
i, err := inv.Item(sl)
|
||
|
|
if err != nil {
|
||
|
|
return i, err
|
||
|
|
}
|
||
|
|
return i, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// setItemInSlot sets an item stack in the slot of a container present in the slot info.
|
||
|
|
func (h *ItemStackRequestHandler) setItemInSlot(slot protocol.StackRequestSlotInfo, i item.Stack, s *Session, tx *world.Tx) {
|
||
|
|
inv, _ := s.invByID(int32(slot.Container.ContainerID), tx)
|
||
|
|
|
||
|
|
sl := int(slot.Slot)
|
||
|
|
if inv == s.offHand {
|
||
|
|
sl = 0
|
||
|
|
}
|
||
|
|
|
||
|
|
before, _ := inv.Item(sl)
|
||
|
|
_ = inv.SetItem(sl, i)
|
||
|
|
|
||
|
|
respSlot := protocol.StackResponseSlotInfo{
|
||
|
|
Slot: slot.Slot,
|
||
|
|
HotbarSlot: slot.Slot,
|
||
|
|
Count: byte(i.Count()),
|
||
|
|
StackNetworkID: item_id(i),
|
||
|
|
DurabilityCorrection: int32(i.MaxDurability() - i.Durability()),
|
||
|
|
}
|
||
|
|
|
||
|
|
if h.changes[slot.Container.ContainerID] == nil {
|
||
|
|
h.changes[slot.Container.ContainerID] = map[byte]changeInfo{}
|
||
|
|
}
|
||
|
|
h.changes[slot.Container.ContainerID][slot.Slot] = changeInfo{
|
||
|
|
after: respSlot,
|
||
|
|
before: before,
|
||
|
|
}
|
||
|
|
|
||
|
|
if h.responseChanges[h.currentRequest] == nil {
|
||
|
|
h.responseChanges[h.currentRequest] = map[*inventory.Inventory]map[byte]responseChange{}
|
||
|
|
}
|
||
|
|
if h.responseChanges[h.currentRequest][inv] == nil {
|
||
|
|
h.responseChanges[h.currentRequest][inv] = map[byte]responseChange{}
|
||
|
|
}
|
||
|
|
h.responseChanges[h.currentRequest][inv][slot.Slot] = responseChange{
|
||
|
|
id: respSlot.StackNetworkID,
|
||
|
|
timestamp: h.current,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// resolve resolves the request with the ID passed.
|
||
|
|
func (h *ItemStackRequestHandler) resolve(id int32, s *Session) {
|
||
|
|
info := make([]protocol.StackResponseContainerInfo, 0, len(h.changes))
|
||
|
|
for container, slotInfo := range h.changes {
|
||
|
|
slots := make([]protocol.StackResponseSlotInfo, 0, len(slotInfo))
|
||
|
|
for _, slot := range slotInfo {
|
||
|
|
slots = append(slots, slot.after)
|
||
|
|
}
|
||
|
|
info = append(info, protocol.StackResponseContainerInfo{
|
||
|
|
Container: protocol.FullContainerName{ContainerID: container},
|
||
|
|
SlotInfo: slots,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
s.writePacket(&packet.ItemStackResponse{Responses: []protocol.ItemStackResponse{{
|
||
|
|
Status: protocol.ItemStackResponseStatusOK,
|
||
|
|
RequestID: id,
|
||
|
|
ContainerInfo: info,
|
||
|
|
}}})
|
||
|
|
|
||
|
|
h.changes = map[byte]map[byte]changeInfo{}
|
||
|
|
h.pendingResults = nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// reject rejects the item stack request sent by the client so that it is reverted client-side.
|
||
|
|
func (h *ItemStackRequestHandler) reject(id int32, s *Session, tx *world.Tx) {
|
||
|
|
s.writePacket(&packet.ItemStackResponse{
|
||
|
|
Responses: []protocol.ItemStackResponse{{
|
||
|
|
Status: protocol.ItemStackResponseStatusError,
|
||
|
|
RequestID: id,
|
||
|
|
}},
|
||
|
|
})
|
||
|
|
|
||
|
|
// Revert changes that we already made for valid actions.
|
||
|
|
for container, slots := range h.changes {
|
||
|
|
for slot, info := range slots {
|
||
|
|
inv, _ := s.invByID(int32(container), tx)
|
||
|
|
_ = inv.SetItem(int(slot), info.before)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
h.changes = map[byte]map[byte]changeInfo{}
|
||
|
|
h.pendingResults = nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// call uses an event.Context, slot and item.Stack to call the event handler function passed. An error is returned if
|
||
|
|
// the event.Context was cancelled either before or after the call.
|
||
|
|
func call(ctx *inventory.Context, slot int, it item.Stack, f func(ctx *inventory.Context, slot int, it item.Stack)) error {
|
||
|
|
if ctx.Cancelled() {
|
||
|
|
return fmt.Errorf("action was cancelled")
|
||
|
|
}
|
||
|
|
f(ctx, slot, it)
|
||
|
|
if ctx.Cancelled() {
|
||
|
|
return fmt.Errorf("action was cancelled")
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|