Files
mc/server/item/stack.go
T

494 lines
15 KiB
Go
Raw Permalink Normal View History

2026-07-09 08:33:57 +08:00
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<nil> 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")
}