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,433 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"math"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Inventory represents an inventory containing items. These inventories may be carried by entities or may be
|
||||
// held by blocks such as chests.
|
||||
// The size of an inventory may be specified upon construction, but cannot be changed after. The zero value of
|
||||
// an inventory is invalid. Use New() to obtain a new inventory.
|
||||
// Inventory is safe for concurrent usage: Its values are protected by a mutex.
|
||||
type Inventory struct {
|
||||
mu sync.RWMutex
|
||||
h Handler
|
||||
slots []item.Stack
|
||||
|
||||
f SlotFunc
|
||||
validator SlotValidatorFunc
|
||||
}
|
||||
|
||||
// SlotFunc is a function called for each item changed in an Inventory.
|
||||
type SlotFunc func(slot int, before, after item.Stack)
|
||||
|
||||
// SlotValidatorFunc is a function that limits changes in the Inventory slot.
|
||||
type SlotValidatorFunc func(s item.Stack, slot int) bool
|
||||
|
||||
// ErrSlotOutOfRange is returned by any methods on inventory when a slot is passed which is not within the
|
||||
// range of valid values for the inventory.
|
||||
var ErrSlotOutOfRange = errors.New("slot is out of range: must be in range 0 <= slot < inventory.Size()")
|
||||
|
||||
// New creates a new inventory with the size passed. The inventory size cannot be changed after it has been
|
||||
// constructed.
|
||||
// A function may be passed which is called every time a slot is changed. The function may also be nil, if
|
||||
// nothing needs to be done.
|
||||
func New(size int, f SlotFunc) *Inventory {
|
||||
if size <= 0 {
|
||||
panic("inventory size must be at least 1")
|
||||
}
|
||||
if f == nil {
|
||||
f = func(slot int, before, after item.Stack) {}
|
||||
}
|
||||
return &Inventory{h: NopHandler{}, slots: make([]item.Stack, size), f: f, validator: func(s item.Stack, slot int) bool { return true }}
|
||||
}
|
||||
|
||||
// Clone copies an Inventory and returns it, calling the SlotFunc passed for any
|
||||
// slots changed in the new inventory.
|
||||
func (inv *Inventory) Clone(f SlotFunc) *Inventory {
|
||||
if f == nil {
|
||||
f = func(slot int, before, after item.Stack) {}
|
||||
}
|
||||
return &Inventory{h: NopHandler{}, slots: inv.Slots(), f: f, validator: func(s item.Stack, slot int) bool { return true }}
|
||||
}
|
||||
|
||||
// SlotFunc changes the function called when a slot in the inventory is changed.
|
||||
func (inv *Inventory) SlotFunc(f SlotFunc) {
|
||||
inv.mu.Lock()
|
||||
defer inv.mu.Unlock()
|
||||
inv.f = f
|
||||
}
|
||||
|
||||
// SlotValidatorFunc changes the function that limits item placement in the inventory slot.
|
||||
func (inv *Inventory) SlotValidatorFunc(f SlotValidatorFunc) {
|
||||
inv.mu.Lock()
|
||||
defer inv.mu.Unlock()
|
||||
inv.validator = f
|
||||
}
|
||||
|
||||
// Item attempts to obtain an item from a specific slot in the inventory. If an item was present in that slot,
|
||||
// the item is returned and the error is nil. If no item was present in the slot, a Stack with air as its item
|
||||
// and a count of 0 is returned. Stack.Empty() may be called to check if this is the case.
|
||||
// Item only returns an error if the slot passed is out of range. (0 <= slot < inventory.Size())
|
||||
func (inv *Inventory) Item(slot int) (item.Stack, error) {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
|
||||
inv.check()
|
||||
if !inv.validSlot(slot) {
|
||||
return item.Stack{}, ErrSlotOutOfRange
|
||||
}
|
||||
return inv.slots[slot], nil
|
||||
}
|
||||
|
||||
// SetItem sets a stack of items to a specific slot in the inventory. If an item is already present in the
|
||||
// slot, that item will be overwritten.
|
||||
// SetItem will return an error if the slot passed is out of range. (0 <= slot < inventory.Size())
|
||||
func (inv *Inventory) SetItem(slot int, item item.Stack) error {
|
||||
inv.mu.Lock()
|
||||
|
||||
inv.check()
|
||||
if !inv.validSlot(slot) {
|
||||
inv.mu.Unlock()
|
||||
return ErrSlotOutOfRange
|
||||
}
|
||||
f := inv.setItem(slot, item)
|
||||
|
||||
inv.mu.Unlock()
|
||||
|
||||
f()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Slots returns the all slots in the inventory as a slice. The index in the slice is the slot of the inventory that a
|
||||
// specific item.Stack is in. Note that this item.Stack might be empty.
|
||||
func (inv *Inventory) Slots() []item.Stack {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
return slices.Clone(inv.slots)
|
||||
}
|
||||
|
||||
// Items returns a list of all contents of the inventory. This method excludes air items, so the method
|
||||
// only ever returns item stacks which actually represent an item.
|
||||
func (inv *Inventory) Items() []item.Stack {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
|
||||
items := make([]item.Stack, 0, len(inv.slots))
|
||||
for _, it := range inv.slots {
|
||||
if !it.Empty() {
|
||||
items = append(items, it)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// First returns the first slot with an item if found. Second return value describes whether the item was found.
|
||||
func (inv *Inventory) First(item item.Stack) (int, bool) {
|
||||
return inv.FirstFunc(item.Comparable)
|
||||
}
|
||||
|
||||
// FirstFunc finds the first slot with an item.Stack that results in the comparable function passed returning true. The
|
||||
// function returns false if no such item was found.
|
||||
func (inv *Inventory) FirstFunc(comparable func(stack item.Stack) bool) (int, bool) {
|
||||
for slot, it := range inv.Slots() {
|
||||
if !it.Empty() && comparable(it) {
|
||||
return slot, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// FirstEmpty returns the first empty slot if found. Second return value describes whether an empty slot was found.
|
||||
func (inv *Inventory) FirstEmpty() (int, bool) {
|
||||
for slot, it := range inv.Slots() {
|
||||
if it.Empty() {
|
||||
return slot, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// Swap swaps the items between two slots. Returns an error if either slot A or B are invalid.
|
||||
func (inv *Inventory) Swap(slotA, slotB int) error {
|
||||
inv.mu.Lock()
|
||||
|
||||
inv.check()
|
||||
if !inv.validSlot(slotA) || !inv.validSlot(slotB) {
|
||||
inv.mu.Unlock()
|
||||
return ErrSlotOutOfRange
|
||||
}
|
||||
a, b := inv.slots[slotA], inv.slots[slotB]
|
||||
fa, fb := inv.setItem(slotA, b), inv.setItem(slotB, a)
|
||||
|
||||
inv.mu.Unlock()
|
||||
|
||||
fa()
|
||||
fb()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddItem attempts to add an item to the inventory. It does so in a couple of steps: It first iterates over
|
||||
// the inventory to make sure no existing stacks of the same type exist. If these stacks do exist, the item
|
||||
// added is first added on top of those stacks to make sure they are fully filled.
|
||||
// If no existing stacks with leftover space are left, empty slots will be filled up with the remainder of the
|
||||
// item added.
|
||||
// If the item could not be fully added to the inventory, an error is returned along with the count that was
|
||||
// added to the inventory.
|
||||
func (inv *Inventory) AddItem(it item.Stack) (n int, err error) {
|
||||
if it.Empty() {
|
||||
return 0, nil
|
||||
}
|
||||
first := it.Count()
|
||||
emptySlots := make([]int, 0, 16)
|
||||
|
||||
inv.mu.Lock()
|
||||
|
||||
inv.check()
|
||||
for slot, invIt := range inv.slots {
|
||||
if invIt.Empty() {
|
||||
// This slot was empty, and we should first try to add the item stack to existing stacks.
|
||||
emptySlots = append(emptySlots, slot)
|
||||
continue
|
||||
}
|
||||
a, b := invIt.AddStack(it)
|
||||
if it.Count() == b.Count() {
|
||||
// Count stayed the same, meaning this slot either wasn't equal to this stack or was max size.
|
||||
continue
|
||||
}
|
||||
f := inv.setItem(slot, a)
|
||||
//noinspection GoDeferInLoop
|
||||
defer f()
|
||||
|
||||
if it = b; it.Empty() {
|
||||
inv.mu.Unlock()
|
||||
// We were able to add the entire stack to existing stacks in the inventory.
|
||||
return first, nil
|
||||
}
|
||||
}
|
||||
for _, slot := range emptySlots {
|
||||
a, b := it.Grow(-math.MaxInt32).AddStack(it)
|
||||
|
||||
f := inv.setItem(slot, a)
|
||||
//noinspection GoDeferInLoop
|
||||
defer f()
|
||||
|
||||
if it = b; it.Empty() {
|
||||
inv.mu.Unlock()
|
||||
// We were able to add the entire stack to empty slots.
|
||||
return first, nil
|
||||
}
|
||||
}
|
||||
inv.mu.Unlock()
|
||||
// We were unable to clear out the entire stack to be added to the inventory: There wasn't enough space.
|
||||
return first - it.Count(), fmt.Errorf("could not add full item stack to inventory")
|
||||
}
|
||||
|
||||
// RemoveItem attempts to remove an item from the inventory. It will visit all slots in the inventory and
|
||||
// empties them until it.Count() items have been removed from the inventory.
|
||||
// If less than it.Count() items were removed from the inventory, an error is returned.
|
||||
func (inv *Inventory) RemoveItem(it item.Stack) error {
|
||||
return inv.RemoveItemFunc(it.Count(), it.Comparable)
|
||||
}
|
||||
|
||||
// RemoveItemFunc removes up to n items from the Inventory. It will visit all slots in the inventory and empties them
|
||||
// until n items have been removed from the inventory, assuming the comparable function returns true for the slots
|
||||
// visited. No items will be deducted from slots if the comparable function returns false.
|
||||
// If less than n items were removed, an error is returned.
|
||||
func (inv *Inventory) RemoveItemFunc(n int, comparable func(stack item.Stack) bool) error {
|
||||
inv.mu.Lock()
|
||||
inv.check()
|
||||
for slot, slotIt := range inv.slots {
|
||||
if slotIt.Empty() || !comparable(slotIt) {
|
||||
continue
|
||||
}
|
||||
c := slotIt.Count() - n
|
||||
|
||||
var f func()
|
||||
if c <= 0 {
|
||||
f = inv.setItem(slot, item.Stack{})
|
||||
} else {
|
||||
f = inv.setItem(slot, slotIt.Grow(-n))
|
||||
}
|
||||
|
||||
//noinspection GoDeferInLoop
|
||||
defer f()
|
||||
|
||||
if n -= slotIt.Count(); n <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
inv.mu.Unlock()
|
||||
|
||||
if n > 0 {
|
||||
return fmt.Errorf("could not remove all items from the inventory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ContainsItem checks if the Inventory contains an item.Stack. It will visit all slots in the Inventory until it finds
|
||||
// at enough items. If enough were found, true is returned.
|
||||
func (inv *Inventory) ContainsItem(it item.Stack) bool {
|
||||
return inv.ContainsItemFunc(it.Count(), it.Comparable)
|
||||
}
|
||||
|
||||
// ContainsItemFunc checks if the Inventory contains at least n items. It will visit all slots in the Inventory until it
|
||||
// finds n items on which the comparable function returns true. ContainsItemFunc returns true if this is the case.
|
||||
func (inv *Inventory) ContainsItemFunc(n int, comparable func(stack item.Stack) bool) bool {
|
||||
inv.mu.Lock()
|
||||
defer inv.mu.Unlock()
|
||||
|
||||
inv.check()
|
||||
for _, slotIt := range inv.slots {
|
||||
if !slotIt.Empty() && comparable(slotIt) {
|
||||
if n -= slotIt.Count(); n <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return n <= 0
|
||||
}
|
||||
|
||||
// Merge merges two inventories into one. The function passed is called for every slot change in the new inventory.
|
||||
func (inv *Inventory) Merge(inv2 *Inventory, f func(int, item.Stack, item.Stack)) *Inventory {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
inv2.mu.RLock()
|
||||
defer inv2.mu.RUnlock()
|
||||
|
||||
n := New(len(inv.slots)+len(inv2.slots), f)
|
||||
n.slots = make([]item.Stack, 0, len(inv.slots)+len(inv2.slots))
|
||||
n.slots = append(n.slots, inv.slots...)
|
||||
n.slots = append(n.slots, inv2.slots...)
|
||||
return n
|
||||
}
|
||||
|
||||
// Empty checks if the inventory is fully empty: It iterates over the inventory and makes sure every stack in
|
||||
// it is empty.
|
||||
func (inv *Inventory) Empty() bool {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
|
||||
inv.check()
|
||||
for _, it := range inv.slots {
|
||||
if !it.Empty() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Clear clears the entire inventory. All non-zero items are returned.
|
||||
func (inv *Inventory) Clear() []item.Stack {
|
||||
inv.mu.Lock()
|
||||
|
||||
inv.check()
|
||||
|
||||
items := make([]item.Stack, 0, inv.size())
|
||||
for slot, i := range inv.slots {
|
||||
if !i.Empty() {
|
||||
items = append(items, i)
|
||||
f := inv.setItem(slot, item.Stack{})
|
||||
//noinspection GoDeferInLoop
|
||||
defer f()
|
||||
}
|
||||
}
|
||||
inv.mu.Unlock()
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
// Handle assigns a Handler to an Inventory so that its methods are called for the respective events. Nil may be passed
|
||||
// to set the default NopHandler.
|
||||
func (inv *Inventory) Handle(h Handler) {
|
||||
inv.mu.Lock()
|
||||
defer inv.mu.Unlock()
|
||||
|
||||
inv.check()
|
||||
if h == nil {
|
||||
h = NopHandler{}
|
||||
}
|
||||
inv.h = h
|
||||
}
|
||||
|
||||
// Handler returns the Handler currently assigned to the Inventory. This is the NopHandler by default.
|
||||
func (inv *Inventory) Handler() Handler {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
|
||||
inv.check()
|
||||
return inv.h
|
||||
}
|
||||
|
||||
// setItem sets an item to a specific slot and overwrites the existing item. It calls the function which is
|
||||
// called for every item change and does so without locking the inventory.
|
||||
func (inv *Inventory) setItem(slot int, it item.Stack) func() {
|
||||
if !inv.validator(it, slot) {
|
||||
return func() {}
|
||||
}
|
||||
if it.Count() > it.MaxCount() {
|
||||
it = it.Grow(it.MaxCount() - it.Count())
|
||||
}
|
||||
before := inv.slots[slot]
|
||||
inv.slots[slot] = it
|
||||
return func() {
|
||||
inv.f(slot, before, it)
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the size of the inventory. It is always the same value as that passed in the call to New() and
|
||||
// is always at least 1.
|
||||
func (inv *Inventory) Size() int {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
return inv.size()
|
||||
}
|
||||
|
||||
// size returns the size of the inventory without locking.
|
||||
func (inv *Inventory) size() int {
|
||||
return len(inv.slots)
|
||||
}
|
||||
|
||||
// Close closes the inventory, freeing the function called for every slot change. It also clears any items
|
||||
// that may currently be in the inventory.
|
||||
// The returned error is always nil.
|
||||
func (inv *Inventory) Close() error {
|
||||
inv.mu.Lock()
|
||||
defer inv.mu.Unlock()
|
||||
|
||||
inv.check()
|
||||
inv.f = func(int, item.Stack, item.Stack) {}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String implements the fmt.Stringer interface.
|
||||
func (inv *Inventory) String() string {
|
||||
inv.mu.RLock()
|
||||
defer inv.mu.RUnlock()
|
||||
|
||||
s := make([]string, 0, inv.size())
|
||||
for _, it := range inv.slots {
|
||||
s = append(s, it.String())
|
||||
}
|
||||
return "(" + strings.Join(s, ", ") + ")"
|
||||
}
|
||||
|
||||
// validSlot checks if the slot passed is valid for the inventory. It returns false if the slot is either
|
||||
// smaller than 0 or bigger/equal to the size of the inventory's size.
|
||||
func (inv *Inventory) validSlot(slot int) bool {
|
||||
return slot >= 0 && slot < inv.size()
|
||||
}
|
||||
|
||||
// check panics if the inventory is valid, and panics if it is not. This typically happens if the inventory
|
||||
// was not created using New().
|
||||
func (inv *Inventory) check() {
|
||||
if inv.size() == 0 {
|
||||
panic("uninitialised inventory: inventory must be constructed using inventory.New()")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user