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 }