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

     1  package inventory
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"github.com/df-mc/dragonfly/server/item"
     7  	"math"
     8  	"slices"
     9  	"strings"
    10  	"sync"
    11  )
    12  
    13  // Inventory represents an inventory containing items. These inventories may be carried by entities or may be
    14  // held by blocks such as chests.
    15  // The size of an inventory may be specified upon construction, but cannot be changed after. The zero value of
    16  // an inventory is invalid. Use New() to obtain a new inventory.
    17  // Inventory is safe for concurrent usage: Its values are protected by a mutex.
    18  type Inventory struct {
    19  	mu    sync.RWMutex
    20  	h     Handler
    21  	slots []item.Stack
    22  
    23  	f      func(slot int, before, after item.Stack)
    24  	canAdd func(s item.Stack, slot int) bool
    25  }
    26  
    27  // ErrSlotOutOfRange is returned by any methods on inventory when a slot is passed which is not within the
    28  // range of valid values for the inventory.
    29  var ErrSlotOutOfRange = errors.New("slot is out of range: must be in range 0 <= slot < inventory.Size()")
    30  
    31  // New creates a new inventory with the size passed. The inventory size cannot be changed after it has been
    32  // constructed.
    33  // A function may be passed which is called every time a slot is changed. The function may also be nil, if
    34  // nothing needs to be done.
    35  func New(size int, f func(slot int, before, after item.Stack)) *Inventory {
    36  	if size <= 0 {
    37  		panic("inventory size must be at least 1")
    38  	}
    39  	if f == nil {
    40  		f = func(slot int, before, after item.Stack) {}
    41  	}
    42  	return &Inventory{h: NopHandler{}, slots: make([]item.Stack, size), f: f, canAdd: func(s item.Stack, slot int) bool { return true }}
    43  }
    44  
    45  // Item attempts to obtain an item from a specific slot in the inventory. If an item was present in that slot,
    46  // the item is returned and the error is nil. If no item was present in the slot, a Stack with air as its item
    47  // and a count of 0 is returned. Stack.Empty() may be called to check if this is the case.
    48  // Item only returns an error if the slot passed is out of range. (0 <= slot < inventory.Size())
    49  func (inv *Inventory) Item(slot int) (item.Stack, error) {
    50  	inv.mu.RLock()
    51  	defer inv.mu.RUnlock()
    52  
    53  	inv.check()
    54  	if !inv.validSlot(slot) {
    55  		return item.Stack{}, ErrSlotOutOfRange
    56  	}
    57  	return inv.slots[slot], nil
    58  }
    59  
    60  // SetItem sets a stack of items to a specific slot in the inventory. If an item is already present in the
    61  // slot, that item will be overwritten.
    62  // SetItem will return an error if the slot passed is out of range. (0 <= slot < inventory.Size())
    63  func (inv *Inventory) SetItem(slot int, item item.Stack) error {
    64  	inv.mu.Lock()
    65  
    66  	inv.check()
    67  	if !inv.validSlot(slot) {
    68  		inv.mu.Unlock()
    69  		return ErrSlotOutOfRange
    70  	}
    71  	f := inv.setItem(slot, item)
    72  
    73  	inv.mu.Unlock()
    74  
    75  	f()
    76  	return nil
    77  }
    78  
    79  // Slots returns the all slots in the inventory as a slice. The index in the slice is the slot of the inventory that a
    80  // specific item.Stack is in. Note that this item.Stack might be empty.
    81  func (inv *Inventory) Slots() []item.Stack {
    82  	inv.mu.RLock()
    83  	defer inv.mu.RUnlock()
    84  	return slices.Clone(inv.slots)
    85  }
    86  
    87  // Items returns a list of all contents of the inventory. This method excludes air items, so the method
    88  // only ever returns item stacks which actually represent an item.
    89  func (inv *Inventory) Items() []item.Stack {
    90  	inv.mu.RLock()
    91  	defer inv.mu.RUnlock()
    92  
    93  	items := make([]item.Stack, 0, len(inv.slots))
    94  	for _, it := range inv.slots {
    95  		if !it.Empty() {
    96  			items = append(items, it)
    97  		}
    98  	}
    99  	return items
   100  }
   101  
   102  // First returns the first slot with an item if found. Second return value describes whether the item was found.
   103  func (inv *Inventory) First(item item.Stack) (int, bool) {
   104  	return inv.FirstFunc(item.Comparable)
   105  }
   106  
   107  // FirstFunc finds the first slot with an item.Stack that results in the comparable function passed returning true. The
   108  // function returns false if no such item was found.
   109  func (inv *Inventory) FirstFunc(comparable func(stack item.Stack) bool) (int, bool) {
   110  	for slot, it := range inv.Slots() {
   111  		if !it.Empty() && comparable(it) {
   112  			return slot, true
   113  		}
   114  	}
   115  	return -1, false
   116  }
   117  
   118  // FirstEmpty returns the first empty slot if found. Second return value describes whether an empty slot was found.
   119  func (inv *Inventory) FirstEmpty() (int, bool) {
   120  	for slot, it := range inv.Slots() {
   121  		if it.Empty() {
   122  			return slot, true
   123  		}
   124  	}
   125  	return -1, false
   126  }
   127  
   128  // Swap swaps the items between two slots. Returns an error if either slot A or B are invalid.
   129  func (inv *Inventory) Swap(slotA, slotB int) error {
   130  	inv.mu.Lock()
   131  
   132  	inv.check()
   133  	if !inv.validSlot(slotA) || !inv.validSlot(slotB) {
   134  		inv.mu.Unlock()
   135  		return ErrSlotOutOfRange
   136  	}
   137  	a, b := inv.slots[slotA], inv.slots[slotB]
   138  	fa, fb := inv.setItem(slotA, b), inv.setItem(slotB, a)
   139  
   140  	inv.mu.Unlock()
   141  
   142  	fa()
   143  	fb()
   144  	return nil
   145  }
   146  
   147  // AddItem attempts to add an item to the inventory. It does so in a couple of steps: It first iterates over
   148  // the inventory to make sure no existing stacks of the same type exist. If these stacks do exist, the item
   149  // added is first added on top of those stacks to make sure they are fully filled.
   150  // If no existing stacks with leftover space are left, empty slots will be filled up with the remainder of the
   151  // item added.
   152  // If the item could not be fully added to the inventory, an error is returned along with the count that was
   153  // added to the inventory.
   154  func (inv *Inventory) AddItem(it item.Stack) (n int, err error) {
   155  	if it.Empty() {
   156  		return 0, nil
   157  	}
   158  	first := it.Count()
   159  	emptySlots := make([]int, 0, 16)
   160  
   161  	inv.mu.Lock()
   162  
   163  	inv.check()
   164  	for slot, invIt := range inv.slots {
   165  		if invIt.Empty() {
   166  			// This slot was empty, and we should first try to add the item stack to existing stacks.
   167  			emptySlots = append(emptySlots, slot)
   168  			continue
   169  		}
   170  		a, b := invIt.AddStack(it)
   171  		if it.Count() == b.Count() {
   172  			// Count stayed the same, meaning this slot either wasn't equal to this stack or was max size.
   173  			continue
   174  		}
   175  		f := inv.setItem(slot, a)
   176  		//noinspection GoDeferInLoop
   177  		defer f()
   178  
   179  		if it = b; it.Empty() {
   180  			inv.mu.Unlock()
   181  			// We were able to add the entire stack to existing stacks in the inventory.
   182  			return first, nil
   183  		}
   184  	}
   185  	for _, slot := range emptySlots {
   186  		a, b := it.Grow(-math.MaxInt32).AddStack(it)
   187  
   188  		f := inv.setItem(slot, a)
   189  		//noinspection GoDeferInLoop
   190  		defer f()
   191  
   192  		if it = b; it.Empty() {
   193  			inv.mu.Unlock()
   194  			// We were able to add the entire stack to empty slots.
   195  			return first, nil
   196  		}
   197  	}
   198  	inv.mu.Unlock()
   199  	// We were unable to clear out the entire stack to be added to the inventory: There wasn't enough space.
   200  	return first - it.Count(), fmt.Errorf("could not add full item stack to inventory")
   201  }
   202  
   203  // RemoveItem attempts to remove an item from the inventory. It will visit all slots in the inventory and
   204  // empties them until it.Count() items have been removed from the inventory.
   205  // If less than it.Count() items were removed from the inventory, an error is returned.
   206  func (inv *Inventory) RemoveItem(it item.Stack) error {
   207  	return inv.RemoveItemFunc(it.Count(), it.Comparable)
   208  }
   209  
   210  // RemoveItemFunc removes up to n items from the Inventory. It will visit all slots in the inventory and empties them
   211  // until n items have been removed from the inventory, assuming the comparable function returns true for the slots
   212  // visited. No items will be deducted from slots if the comparable function returns false.
   213  // If less than n items were removed, an error is returned.
   214  func (inv *Inventory) RemoveItemFunc(n int, comparable func(stack item.Stack) bool) error {
   215  	inv.mu.Lock()
   216  	inv.check()
   217  	for slot, slotIt := range inv.slots {
   218  		if slotIt.Empty() || !comparable(slotIt) {
   219  			continue
   220  		}
   221  		f := inv.setItem(slot, slotIt.Grow(-n))
   222  		//noinspection GoDeferInLoop
   223  		defer f()
   224  
   225  		if n -= slotIt.Count(); n <= 0 {
   226  			break
   227  		}
   228  	}
   229  	inv.mu.Unlock()
   230  
   231  	if n > 0 {
   232  		return fmt.Errorf("could not remove all items from the inventory")
   233  	}
   234  	return nil
   235  }
   236  
   237  // ContainsItem checks if the Inventory contains an item.Stack. It will visit all slots in the Inventory until it finds
   238  // at enough items. If enough were found, true is returned.
   239  func (inv *Inventory) ContainsItem(it item.Stack) bool {
   240  	return inv.ContainsItemFunc(it.Count(), it.Comparable)
   241  }
   242  
   243  // ContainsItemFunc checks if the Inventory contains at least n items. It will visit all slots in the Inventory until it
   244  // finds n items on which the comparable function returns true. ContainsItemFunc returns true if this is the case.
   245  func (inv *Inventory) ContainsItemFunc(n int, comparable func(stack item.Stack) bool) bool {
   246  	inv.mu.Lock()
   247  	defer inv.mu.Unlock()
   248  
   249  	inv.check()
   250  	for _, slotIt := range inv.slots {
   251  		if !slotIt.Empty() && comparable(slotIt) {
   252  			if n -= slotIt.Count(); n <= 0 {
   253  				break
   254  			}
   255  		}
   256  	}
   257  	return n <= 0
   258  }
   259  
   260  // Empty checks if the inventory is fully empty: It iterates over the inventory and makes sure every stack in
   261  // it is empty.
   262  func (inv *Inventory) Empty() bool {
   263  	inv.mu.RLock()
   264  	defer inv.mu.RUnlock()
   265  
   266  	inv.check()
   267  	for _, it := range inv.slots {
   268  		if !it.Empty() {
   269  			return false
   270  		}
   271  	}
   272  	return true
   273  }
   274  
   275  // Clear clears the entire inventory. All non-zero items are returned.
   276  func (inv *Inventory) Clear() []item.Stack {
   277  	inv.mu.Lock()
   278  
   279  	inv.check()
   280  
   281  	items := make([]item.Stack, 0, inv.size())
   282  	for slot, i := range inv.slots {
   283  		if !i.Empty() {
   284  			items = append(items, i)
   285  			f := inv.setItem(slot, item.Stack{})
   286  			//noinspection GoDeferInLoop
   287  			defer f()
   288  		}
   289  	}
   290  	inv.mu.Unlock()
   291  
   292  	return items
   293  }
   294  
   295  // Handle assigns a Handler to an Inventory so that its methods are called for the respective events. Nil may be passed
   296  // to set the default NopHandler.
   297  func (inv *Inventory) Handle(h Handler) {
   298  	inv.mu.Lock()
   299  	defer inv.mu.Unlock()
   300  
   301  	inv.check()
   302  	if h == nil {
   303  		h = NopHandler{}
   304  	}
   305  	inv.h = h
   306  }
   307  
   308  // Handler returns the Handler currently assigned to the Inventory. This is the NopHandler by default.
   309  func (inv *Inventory) Handler() Handler {
   310  	inv.mu.RLock()
   311  	defer inv.mu.RUnlock()
   312  
   313  	inv.check()
   314  	return inv.h
   315  }
   316  
   317  // setItem sets an item to a specific slot and overwrites the existing item. It calls the function which is
   318  // called for every item change and does so without locking the inventory.
   319  func (inv *Inventory) setItem(slot int, it item.Stack) func() {
   320  	if !inv.canAdd(it, slot) {
   321  		return func() {}
   322  	}
   323  	if it.Count() > it.MaxCount() {
   324  		it = it.Grow(it.MaxCount() - it.Count())
   325  	}
   326  	before := inv.slots[slot]
   327  	inv.slots[slot] = it
   328  	return func() {
   329  		inv.f(slot, before, it)
   330  	}
   331  }
   332  
   333  // Size returns the size of the inventory. It is always the same value as that passed in the call to New() and
   334  // is always at least 1.
   335  func (inv *Inventory) Size() int {
   336  	inv.mu.RLock()
   337  	defer inv.mu.RUnlock()
   338  	return inv.size()
   339  }
   340  
   341  // size returns the size of the inventory without locking.
   342  func (inv *Inventory) size() int {
   343  	return len(inv.slots)
   344  }
   345  
   346  // Close closes the inventory, freeing the function called for every slot change. It also clears any items
   347  // that may currently be in the inventory.
   348  // The returned error is always nil.
   349  func (inv *Inventory) Close() error {
   350  	inv.mu.Lock()
   351  	defer inv.mu.Unlock()
   352  
   353  	inv.check()
   354  	inv.f = func(int, item.Stack, item.Stack) {}
   355  	return nil
   356  }
   357  
   358  // String implements the fmt.Stringer interface.
   359  func (inv *Inventory) String() string {
   360  	inv.mu.RLock()
   361  	defer inv.mu.RUnlock()
   362  
   363  	s := make([]string, 0, inv.size())
   364  	for _, it := range inv.slots {
   365  		s = append(s, it.String())
   366  	}
   367  	return "(" + strings.Join(s, ", ") + ")"
   368  }
   369  
   370  // validSlot checks if the slot passed is valid for the inventory. It returns false if the slot is either
   371  // smaller than 0 or bigger/equal to the size of the inventory's size.
   372  func (inv *Inventory) validSlot(slot int) bool {
   373  	return slot >= 0 && slot < inv.size()
   374  }
   375  
   376  // check panics if the inventory is valid, and panics if it is not. This typically happens if the inventory
   377  // was not created using New().
   378  func (inv *Inventory) check() {
   379  	if inv.size() == 0 {
   380  		panic("uninitialised inventory: inventory must be constructed using inventory.New()")
   381  	}
   382  }