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

This commit is contained in:
2026-07-09 08:33:57 +08:00
commit 26ed99fda6
845 changed files with 75419 additions and 0 deletions
+249
View File
@@ -0,0 +1,249 @@
package inventory
import (
"fmt"
"github.com/df-mc/dragonfly/server/item"
"github.com/df-mc/dragonfly/server/item/enchantment"
"github.com/df-mc/dragonfly/server/world"
"math"
"math/rand/v2"
)
// Armour represents an inventory for armour. It has 4 slots, one for a helmet, chestplate, leggings and
// boots respectively. NewArmour() must be used to create a valid armour inventory.
// Armour inventories, like normal Inventories, are safe for concurrent usage.
type Armour struct {
inv *Inventory
}
// NewArmour returns an armour inventory that is ready to be used. The zero value of an inventory.Armour is
// not valid for usage.
// The function passed is called when a slot is changed. It may be nil to not call anything.
func NewArmour(f func(slot int, before, after item.Stack)) *Armour {
inv := New(4, f)
inv.validator = canAddArmour
return &Armour{inv: inv}
}
// canAddArmour checks if the item passed can be worn as armour in the slot passed.
func canAddArmour(s item.Stack, slot int) bool {
if s.Empty() {
return true
}
switch slot {
case 0:
if h, ok := s.Item().(item.HelmetType); ok {
return h.Helmet()
}
case 1:
if c, ok := s.Item().(item.ChestplateType); ok {
return c.Chestplate()
}
case 2:
if l, ok := s.Item().(item.LeggingsType); ok {
return l.Leggings()
}
case 3:
if b, ok := s.Item().(item.BootsType); ok {
return b.Boots()
}
}
return false
}
// Set sets all individual pieces of armour in one go. It is equivalent to calling SetHelmet, SetChestplate, SetLeggings
// and SetBoots sequentially.
func (a *Armour) Set(helmet, chestplate, leggings, boots item.Stack) {
a.SetHelmet(helmet)
a.SetChestplate(chestplate)
a.SetLeggings(leggings)
a.SetBoots(boots)
}
// SetHelmet sets the item stack passed as the helmet in the inventory.
func (a *Armour) SetHelmet(helmet item.Stack) {
_ = a.inv.SetItem(0, helmet)
}
// Helmet returns the item stack set as helmet in the inventory.
func (a *Armour) Helmet() item.Stack {
i, _ := a.inv.Item(0)
return i
}
// SetChestplate sets the item stack passed as the chestplate in the inventory.
func (a *Armour) SetChestplate(chestplate item.Stack) {
_ = a.inv.SetItem(1, chestplate)
}
// Chestplate returns the item stack set as chestplate in the inventory.
func (a *Armour) Chestplate() item.Stack {
i, _ := a.inv.Item(1)
return i
}
// SetLeggings sets the item stack passed as the leggings in the inventory.
func (a *Armour) SetLeggings(leggings item.Stack) {
_ = a.inv.SetItem(2, leggings)
}
// Leggings returns the item stack set as leggings in the inventory.
func (a *Armour) Leggings() item.Stack {
i, _ := a.inv.Item(2)
return i
}
// SetBoots sets the item stack passed as the boots in the inventory.
func (a *Armour) SetBoots(boots item.Stack) {
_ = a.inv.SetItem(3, boots)
}
// Boots returns the item stack set as boots in the inventory.
func (a *Armour) Boots() item.Stack {
i, _ := a.inv.Item(3)
return i
}
// DamageReduction returns the amount of damage that is reduced by the Armour for
// an amount of damage and damage source. The value returned takes into account
// the armour itself and its enchantments.
func (a *Armour) DamageReduction(dmg float64, src world.DamageSource) float64 {
var (
original = dmg
defencePoints, toughness float64
enchantments []item.Enchantment
)
for _, it := range a.Items() {
enchantments = append(enchantments, it.Enchantments()...)
if armour, ok := it.Item().(item.Armour); ok {
defencePoints += armour.DefencePoints()
toughness += armour.Toughness()
}
}
dmg -= dmg * enchantment.ProtectionFactor(src, enchantments)
if src.ReducedByArmour() {
// Armour in Bedrock edition reduces the damage taken by 4% for each effective armour point. Effective
// armour point decreases as damage increases, with 1 point lost for every 2 HP of damage. The defence
// reduction is decreased by the toughness armour value. Effective armour points will at minimum be 20% of
// armour points.
dmg -= dmg * 0.04 * math.Max(defencePoints*0.2, defencePoints-dmg/(2+toughness/4))
}
return original - dmg
}
// HighestEnchantmentLevel looks up the highest level of an item.EnchantmentType
// that any of the Armour items have and returns it, or 0 if none of the items
// have the enchantment.
func (a *Armour) HighestEnchantmentLevel(t item.EnchantmentType) int {
lvl := 0
for _, it := range a.Items() {
if e, ok := it.Enchantment(t); ok && e.Level() > lvl {
lvl = e.Level()
}
}
return lvl
}
// DamageFunc is a function that deals d damage points to an item stack s. The
// resulting item.Stack is returned. Depending on the game mode of a player,
// damage may not be dealt at all.
type DamageFunc func(s item.Stack, d int) item.Stack
// Damage deals damage (hearts) to Armour. The resulting item damage depends on the
// dmg passed and the DamageFunc used.
func (a *Armour) Damage(dmg float64, f DamageFunc) {
armourDamage := int(math.Max(math.Floor(dmg/4), 1))
for slot, it := range a.Slots() {
_ = a.inv.SetItem(slot, f(it, armourDamage))
}
}
// ThornsDamage checks if any of the Armour items are enchanted with thorns. If
// this is the case and the thorns enchantment activates (15% chance per level),
// a random Armour piece is damaged. The damage to be dealt to the attacker is
// returned.
func (a *Armour) ThornsDamage(f DamageFunc) float64 {
slots := a.Slots()
dmg := 0.0
for _, i := range slots {
thorns, _ := i.Enchantment(enchantment.Thorns)
if level := float64(thorns.Level()); rand.Float64() < level*0.15 {
// 15%/level chance of thorns activation per item. Total damage from
// normal thorns armour (max thorns III) should never exceed 4.0 in
// total.
dmg = math.Min(dmg+float64(1+rand.IntN(4)), 4.0)
}
}
if highest := a.HighestEnchantmentLevel(enchantment.Thorns); highest > 10 {
// When we find an armour piece with thorns XI or above, the logic
// changes: We have to find the armour piece with the highest level
// of thorns and subtract 10 from its level to calculate the final
// damage.
dmg = float64(highest - 10)
}
if dmg > 0 {
// Deal 2 damage to one random thorns item. Bedrock Edition and Java Edition
// both have different behaviour here and neither seem to match the expected
// behaviour. Java Edition deals 2 damage to a random thorns item for every
// thorns armour item worn, while Bedrock Edition deals 1 additional damage
// for every thorns item and another 2 for every thorns item when it
// activates.
slot := rand.IntN(len(slots))
_ = a.Inventory().SetItem(slot, f(slots[slot], 2))
}
return dmg
}
// KnockBackResistance returns the combined knock back resistance of all Armour
// items. A value of 0 means normal knock back force, while a value of 1 means
// all knock back is ignored.
func (a *Armour) KnockBackResistance() float64 {
resistance := 0.0
for _, i := range a.Items() {
if a, ok := i.Item().(item.Armour); ok {
resistance += a.KnockBackResistance()
}
}
return resistance
}
// Slots returns all items (including) air of the armour inventory in the order of helmet, chestplate, leggings,
// boots.
func (a *Armour) Slots() []item.Stack {
return a.inv.Slots()
}
// Items returns a slice of all non-empty armour items equipped.
func (a *Armour) Items() []item.Stack {
return a.inv.Items()
}
// Clear clears the armour inventory, removing all items currently present.
func (a *Armour) Clear() []item.Stack {
return a.inv.Clear()
}
// String converts the armour to a readable string representation.
func (a *Armour) String() string {
return fmt.Sprintf("(helmet: %v, chestplate: %v, leggings: %v, boots: %v)", a.Helmet(), a.Chestplate(), a.Leggings(), a.Boots())
}
// Inventory returns the underlying Inventory instance.
func (a *Armour) Inventory() *Inventory {
return a.inv
}
// Handle assigns a Handler to an Armour inventory so that its methods are called for the respective events. Nil may be
// passed to set the default NopHandler.
// Handle is the equivalent of calling (*Armour).Inventory().H.
func (a *Armour) Handle(h Handler) {
a.inv.Handle(h)
}
// Close closes the armour inventory, removing the slot change function.
func (a *Armour) Close() error {
return a.inv.Close()
}
+33
View File
@@ -0,0 +1,33 @@
package inventory
import (
"github.com/df-mc/dragonfly/server/event"
"github.com/df-mc/dragonfly/server/item"
)
type Holder interface{}
type Context = event.Context[Holder]
// Handler is a type that may be used to handle actions performed on an inventory by a player.
type Handler interface {
// HandleTake handles an item.Stack being taken from a slot in the inventory. This item might be the whole stack or
// part of the stack currently present in that slot.
HandleTake(ctx *Context, slot int, it item.Stack)
// HandlePlace handles an item.Stack being placed in a slot of the inventory. It might either be added to an empty
// slot or a slot that contains an item of the same type.
HandlePlace(ctx *Context, slot int, it item.Stack)
// HandleDrop handles the dropping of an item.Stack in a slot out of the inventory.
HandleDrop(ctx *Context, slot int, it item.Stack)
}
// Check to make sure NopHandler implements Handler.
var _ Handler = NopHandler{}
// NopHandler is an implementation of Handler that does not run any code in any of its methods. It is the default
// Handler of an Inventory.
type NopHandler struct{}
func (NopHandler) HandleTake(*Context, int, item.Stack) {}
func (NopHandler) HandlePlace(*Context, int, item.Stack) {}
func (NopHandler) HandleDrop(*Context, int, item.Stack) {}
+433
View File
@@ -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()")
}
}