package item import ( "fmt" "maps" "reflect" "slices" "sort" "strings" "sync/atomic" "github.com/df-mc/dragonfly/server/world" ) // Stack represents a stack of items. The stack shares the same item type and has a count which specifies the // size of the stack. type Stack struct { id int32 item world.Item count int customName string lore []string damage int unbreakable bool anvilCost int data map[string]any enchantments map[EnchantmentType]Enchantment } // NewStack returns a new stack using the item type and the count passed. NewStack panics if the count passed // is negative or if the item type passed is nil. func NewStack(t world.Item, count int) Stack { if count < 0 { panic("cannot use negative count for item stack") } if t == nil { panic("cannot have a stack with item type nil") } return Stack{item: t, count: count, id: newID()} } // Count returns the amount of items that is present on the stack. The count is guaranteed never to be // negative. func (s Stack) Count() int { return s.count } // MaxCount returns the maximum count that the stack is able to hold when added to an inventory or when added // to an item entity. func (s Stack) MaxCount() int { if counter, ok := s.item.(MaxCounter); ok { return counter.MaxCount() } return 64 } // Grow grows the Stack's count by n, returning the resulting Stack. If a positive number is passed, the stack // is grown, whereas if a negative size is passed, the resulting Stack will have a lower count. The count of // the returned Stack will never be negative. func (s Stack) Grow(n int) Stack { s.count += n if s.count < 0 { s.count = 0 } s.id = newID() return s } // Durability returns the current durability of the item stack. If the item is not one that implements the // Durable interface, BaseDurability will always return -1. // The closer the durability returned is to 0, the closer the item is to being broken. func (s Stack) Durability() int { if durable, ok := s.Item().(Durable); ok { return durable.DurabilityInfo().MaxDurability - s.damage } return -1 } // MaxDurability returns the maximum durability that the item stack is able to have. If the item does not // implement the Durable interface, MaxDurability will always return -1. func (s Stack) MaxDurability() int { if durable, ok := s.Item().(Durable); ok { return durable.DurabilityInfo().MaxDurability } return -1 } // Damage returns a new stack that is damaged by the amount passed. (Meaning, its durability lowered by the // amount passed.) If the item does not implement the Durable interface, the original stack is returned. // The damage passed may be negative to add durability. // If the final durability reaches 0 or below, the item returned is the resulting item of the breaking of the // item. If the final durability reaches a number higher than the maximum durability, the stack returned will // get the maximum durability. func (s Stack) Damage(d int) Stack { durable, ok := s.Item().(Durable) if !ok || s.unbreakable { return s } durability := s.Durability() info := durable.DurabilityInfo() if durability-d <= 0 { if info.Persistent { // Persistent items can't be broken. return s } // A durability of 0, so the item is broken. return info.BrokenItem() } if durability-d > info.MaxDurability { // We've passed the maximum durability, so we just need to make sure the final durability of the item // will be equal to the max. s.damage, d = 0, 0 } s.damage += d return s } // WithDurability returns a new item stack with the durability passed. If the item does not implement the // Durable interface, the original stack is returned. // The closer the durability d is to 0, the closer the item is to being broken. If a durability of 0 is passed, // a stack with the item type of the BrokenItem is returned. If a durability is passed that exceeds the // maximum durability, the stack returned will have the maximum durability. func (s Stack) WithDurability(d int) Stack { durable, ok := s.Item().(Durable) if !ok { return s } maxDurability := durable.DurabilityInfo().MaxDurability if d > maxDurability { // A durability bigger than the max, so the item has no damage at all. s.damage = 0 return s } if d == 0 { // A durability of 0, so the item is broken. return durable.DurabilityInfo().BrokenItem() } s.damage = maxDurability - d return s } // Unbreakable checks if the item stack is unbreakable. func (s Stack) Unbreakable() bool { return s.unbreakable } // AsUnbreakable returns a copy of the Stack with the unbreakable tag set. If the item does not implement the // Durable interface, the original stack is returned. func (s Stack) AsUnbreakable() Stack { if _, ok := s.Item().(Durable); !ok { return s } s.unbreakable = true return s } // AsBreakable returns a copy of the Stack without the unbreakable tag set. If the item does not implement the // Durable interface, the original stack is returned. func (s Stack) AsBreakable() Stack { if _, ok := s.Item().(Durable); !ok { return s } s.unbreakable = false return s } // Empty checks if the stack is empty (has a count of 0). func (s Stack) Empty() bool { if s.Count() == 0 || s.item == nil { return true } name, _ := s.item.EncodeItem() return name == "minecraft:air" } // Item returns the item that the stack holds. If the stack is considered empty (Stack.Empty()), Item will // always return nil. func (s Stack) Item() world.Item { if s.Empty() || s.item == nil { return nil } return s.item } // AttackDamage returns the attack damage to the stack. By default, the value returned is 1.0. If the item // held implements the item.Weapon interface, this damage may be different. func (s Stack) AttackDamage() float64 { if weapon, ok := s.Item().(Weapon); ok { // Bonus attack damage from weapons is a bit quirky in Bedrock Edition: Even though tools say they // have, for example, + 5 Attack Damage, it is actually 1 + 5, while punching with a hand in Bedrock // Edition deals 2 damage, not 1 like in Java Edition. // The tooltip displayed in-game is therefore not exactly correct. return weapon.AttackDamage() + 1 } return 1.0 } // WithCustomName returns a copy of the Stack with the custom name passed. The custom name is formatted // according to the rules of fmt.Sprintln. func (s Stack) WithCustomName(a ...any) Stack { s.customName = format(a) if nameable, ok := s.Item().(nameable); ok { s.item = nameable.WithName(a...) } return s } // CustomName returns the custom name set for the Stack. An empty string is returned if the Stack has no // custom name set. func (s Stack) CustomName() string { return s.customName } // WithLore returns a copy of the Stack with the lore passed. Each string passed is put on a different line, // where the first string is at the top and the last at the bottom. // The lore may be cleared by passing no lines into the Stack. func (s Stack) WithLore(lines ...string) Stack { s.lore = lines return s } // Lore returns the lore set for the Stack. If no lore is present, the slice returned has a len of 0. func (s Stack) Lore() []string { if s.Empty() { return nil } return s.lore } // WithValue returns the current Stack with a value set at a specific key. This method may be used to // associate custom data with the item stack, which will persist through server restarts. // The value stored may later be obtained by making a call to Stack.Value(). // // WithValue may be called with a nil value, in which case the value at the key will be cleared. // // WithValue stores Values by encoding them using the encoding/gob package. Users of WithValue must ensure // that their value is valid for encoding with this package. func (s Stack) WithValue(key string, val any) Stack { s.data = cloneMap(s.data) if val != nil { s.data[key] = val } else { delete(s.data, key) if len(s.data) == 0 { s.data = nil } } return s } // Value attempts to return a value set to the Stack using Stack.WithValue(). If a value is found by the key // passed, it is returned and ok is true. If not found, the value returned is nil and ok is false. func (s Stack) Value(key string) (val any, ok bool) { if s.Empty() { return nil, false } val, ok = s.data[key] return val, ok } // WithEnchantments returns the current stack with the passed enchantments. If an enchantment is not compatible // with the item stack, it will not be applied. func (s Stack) WithEnchantments(enchants ...Enchantment) Stack { if _, ok := s.item.(Book); ok { s.item = EnchantedBook{} } s.enchantments = cloneMap(s.enchantments) for _, enchant := range enchants { if _, ok := s.Item().(EnchantedBook); !ok && !enchant.t.CompatibleWithItem(s.item) { // Enchantment is not compatible with the item. continue } compatible := true for _, otherEnchant := range s.enchantments { addingType := enchant.t existingType := otherEnchant.Type() addingAcceptsExisting := addingType.CompatibleWithEnchantment(existingType) existingAcceptsAdding := existingType.CompatibleWithEnchantment(addingType) if addingType != existingType && (!addingAcceptsExisting || !existingAcceptsAdding) { compatible = false break } } if !compatible { // Enchantment is not compatible with another enchantment on the item. continue } s.enchantments[enchant.t] = enchant } return s } // WithForcedEnchantments returns the current stack with the passed enchantments applied, // bypassing compatibility checks that would normally prevent incompatible enchantments // from being applied together. func (s Stack) WithForcedEnchantments(enchants ...Enchantment) Stack { s.enchantments = cloneMap(s.enchantments) for _, enchant := range enchants { s.enchantments[enchant.t] = enchant } return s } // WithoutEnchantments returns the current stack but with the passed enchantments removed. func (s Stack) WithoutEnchantments(enchants ...EnchantmentType) Stack { s.enchantments = cloneMap(s.enchantments) for _, enchant := range enchants { delete(s.enchantments, enchant) } if _, ok := s.item.(EnchantedBook); ok && len(s.enchantments) == 0 { s.item = Book{} } return s } // Enchantment attempts to return an Enchantment set to the Stack using Stack.WithEnchantment(). If an Enchantment // is found by the EnchantmentType, the enchantment and the bool true is returned. func (s Stack) Enchantment(enchant EnchantmentType) (Enchantment, bool) { if s.Empty() { return Enchantment{}, false } ench, ok := s.enchantments[enchant] return ench, ok } // Enchantments returns an array of all Enchantments on the item. Enchantments returns the enchantments of a Stack in a // deterministic order. func (s Stack) Enchantments() []Enchantment { if s.Empty() { return nil } e := slices.Collect(maps.Values(s.enchantments)) sort.Slice(e, func(i, j int) bool { id1, _ := EnchantmentID(e[i].t) id2, _ := EnchantmentID(e[j].t) return id1 < id2 }) return e } // AnvilCost returns the number of experience levels to add to the base level cost when repairing, combining, or // renaming this item with an anvil. func (s Stack) AnvilCost() int { return s.anvilCost } // WithAnvilCost returns the current Stack with the anvil cost set to the passed value. func (s Stack) WithAnvilCost(anvilCost int) Stack { i := s.Item() _, repairable := i.(Repairable) _, enchantedBook := i.(EnchantedBook) if !repairable && !enchantedBook { // This item can't have a repair cost. return s } s.anvilCost = anvilCost return s } // WithItem returns a Stack with the item type passed, copying all the // properties from s to the new stack. Damage to an item, enchantments and anvil // costs are only copied if they are still applicable to the new item type. func (s Stack) WithItem(t world.Item) Stack { cp := NewStack(t, s.count). Damage(s.damage). WithCustomName(s.customName). WithLore(s.lore...). WithEnchantments(s.Enchantments()...). WithAnvilCost(s.anvilCost) cp.unbreakable = s.unbreakable && s.MaxDurability() != -1 cp.data = s.data return cp } // AddStack adds another stack to the stack and returns both stacks. The first stack returned will have as // many items in it as possible to fit in the stack, according to a max count of either 64 or otherwise as // returned by Item.MaxCount(). The second stack will have the leftover items: It may be empty if the count of // both stacks together don't exceed the max count. // If the two stacks are not comparable, AddStack will return both the original stack and the stack passed. func (s Stack) AddStack(s2 Stack) (a, b Stack) { if s.Count() >= s.MaxCount() { // No more items could be added to the original stack. return s, s2 } if !s.Comparable(s2) { // The items are not comparable and thus cannot be stacked together. return s, s2 } diff := s.MaxCount() - s.Count() if s2.Count() < diff { diff = s2.Count() } s.count, s2.count = s.count+diff, s2.count-diff s.id, s2.id = newID(), newID() return s, s2 } // Equal checks if the two stacks are equal. Equal is equivalent to a Stack.Comparable check while also // checking the count and durability. func (s Stack) Equal(s2 Stack) bool { return s.Comparable(s2) && s.count == s2.count && s.damage == s2.damage } // Comparable checks if two stacks can be considered comparable. True is returned if the two stacks have an // equal item type and have equal enchantments, lore and custom names, or if one of the stacks is empty. // Comparable does not check if the two stacks have the same durability. func (s Stack) Comparable(s2 Stack) bool { if s.Empty() || s2.Empty() { return true } name, meta := s.Item().EncodeItem() name2, meta2 := s2.Item().EncodeItem() if name != name2 || meta != meta2 || s.anvilCost != s2.anvilCost || s.customName != s2.customName { return false } for !slices.Equal(s.lore, s2.lore) { return false } if len(s.enchantments) != len(s2.enchantments) { return false } for i := range s.enchantments { if s.enchantments[i] != s2.enchantments[i] { return false } } if !reflect.DeepEqual(s.data, s2.data) { return false } if nbt, ok := s.Item().(world.NBTer); ok { nbt2, ok := s2.Item().(world.NBTer) return ok && reflect.DeepEqual(nbt.EncodeNBT(), nbt2.EncodeNBT()) } return true } // String implements the fmt.Stringer interface. func (s Stack) String() string { if s.item == nil { return fmt.Sprintf("Stack x%v", s.count) } return fmt.Sprintf("Stack<%T%+v>(custom name='%v', lore='%v', damage=%v, anvilCost=%v) x%v", s.item, s.item, s.customName, s.lore, s.damage, s.anvilCost, s.count) } // Values returns all values associated with the stack by users. The map returned is a copy of the original: // Modifying it will not modify the item stack. func (s Stack) Values() map[string]any { if s.Empty() { return nil } return maps.Clone(s.data) } // cloneMap calls maps.Clone, but initialises m if it does not yet exist. func cloneMap[M ~map[K]V, K comparable, V any](m M) M { if m == nil { m = make(M) } return maps.Clone(m) } // stackID is a counter for unique stack IDs. var stackID = new(int32) // newID returns a new unique stack ID. func newID() int32 { return atomic.AddInt32(stackID, 1) } // id returns the unique ID of the stack passed. // noinspection GoUnusedFunction // //lint:ignore U1000 Function is used through compiler directives. func id(s Stack) int32 { if s.Empty() { return 0 } return s.id } // format is a utility function to format a list of Values to have spaces between them, but no newline at the // end, which is typically used for sending messages, popups and tips. func format(a []any) string { return strings.TrimSuffix(fmt.Sprintln(a...), "\n") }