From 0ab8d02ecee3a8132e3d91c4d99ca3eb0e781916 Mon Sep 17 00:00:00 2001 From: AkmalFairuz Date: Fri, 9 Jan 2026 18:19:56 +0700 Subject: [PATCH 1/5] Implement Armour Stand --- server/entity/armour_stand.go | 80 ++++++++ server/entity/armour_stand_behaviour.go | 252 ++++++++++++++++++++++++ server/entity/ent.go | 38 ++++ server/entity/register.go | 2 + server/item/armour_stand.go | 28 +++ server/item/register.go | 1 + server/player/player.go | 23 ++- server/session/entity_metadata.go | 14 ++ server/session/world.go | 53 ++++- server/world/entity.go | 1 + server/world/sound/armour_stand.go | 13 ++ 11 files changed, 492 insertions(+), 13 deletions(-) create mode 100644 server/entity/armour_stand.go create mode 100644 server/entity/armour_stand_behaviour.go create mode 100644 server/item/armour_stand.go create mode 100644 server/world/sound/armour_stand.go diff --git a/server/entity/armour_stand.go b/server/entity/armour_stand.go new file mode 100644 index 000000000..b2a5b69a2 --- /dev/null +++ b/server/entity/armour_stand.go @@ -0,0 +1,80 @@ +package entity + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/internal/nbtconv" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/inventory" + "github.com/df-mc/dragonfly/server/world" +) + +// NewArmourStand creates a new armour stand entity in the world with the given spawn options. +func NewArmourStand(opts world.EntitySpawnOpts) *world.EntityHandle { + conf := armourStandConf + conf.Armour = inventory.NewArmour(nil) + return opts.New(ArmourStandType, conf) +} + +var armourStandConf = ArmourStandBehaviourConfig{} + +var ArmourStandType armourStandType + +// armourStandType is a world.EntityType implementation for armour stands. +type armourStandType struct{} + +func (armourStandType) Open(tx *world.Tx, handle *world.EntityHandle, data *world.EntityData) world.Entity { + return &Ent{tx: tx, handle: handle, data: data} +} + +func (armourStandType) EncodeEntity() string { return "minecraft:armor_stand" } + +func (armourStandType) BBox(world.Entity) cube.BBox { + return cube.Box(-0.25, 0, -0.25, 0.25, 1.975, 0.25) +} + +func (armourStandType) DecodeNBT(m map[string]any, data *world.EntityData) { + c := ArmourStandBehaviourConfig{ + Armour: inventory.NewArmour(nil), + PoseIndex: int(nbtconv.Int32(m, "PoseIndex")) % 13, + MainHand: nbtconv.MapItem(m, "MainHand"), + OffHand: nbtconv.MapItem(m, "Offhand"), + } + armours := nbtconv.Slice(m, "Armor") + for i := 0; i < 4; i++ { + itemMap, ok := armours[i].(map[string]any) + if !ok { + continue + } + var it item.Stack + switch i { + case 0: + nbtconv.Item(itemMap, &it) + c.Armour.SetHelmet(it) + case 1: + nbtconv.Item(itemMap, &it) + c.Armour.SetChestplate(it) + case 2: + nbtconv.Item(itemMap, &it) + c.Armour.SetLeggings(it) + case 3: + nbtconv.Item(itemMap, &it) + c.Armour.SetBoots(it) + } + } + data.Data = c.New() +} + +func (armourStandType) EncodeNBT(data *world.EntityData) map[string]any { + a := data.Data.(*ArmourStandBehaviour) + return map[string]any{ + "MainHand": nbtconv.WriteItem(a.conf.MainHand, true), + "Offhand": nbtconv.WriteItem(a.conf.OffHand, true), + "Armor": []map[string]any{ + nbtconv.WriteItem(a.Armour().Helmet(), true), + nbtconv.WriteItem(a.Armour().Chestplate(), true), + nbtconv.WriteItem(a.Armour().Leggings(), true), + nbtconv.WriteItem(a.Armour().Boots(), true), + }, + "PoseIndex": int32(a.conf.PoseIndex), + } +} diff --git a/server/entity/armour_stand_behaviour.go b/server/entity/armour_stand_behaviour.go new file mode 100644 index 000000000..e1bae0355 --- /dev/null +++ b/server/entity/armour_stand_behaviour.go @@ -0,0 +1,252 @@ +package entity + +import ( + "github.com/df-mc/dragonfly/server/block" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/item/inventory" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "time" +) + +// ArmourStandBehaviourConfig holds optional parameters for +// ArmourStandBehaviour. +type ArmourStandBehaviourConfig struct { + Armour *inventory.Armour + // MainHand is the item equipped in the main hand slot of the armour stand. + MainHand item.Stack + // OffHand is the item equipped in the offhand slot of the armour stand. + OffHand item.Stack + // PoseIndex is the pose index of the armor stand. Possible values range + // from 0 to 12 (inclusive). + PoseIndex int +} + +// Apply ... +func (conf ArmourStandBehaviourConfig) Apply(data *world.EntityData) { + data.Data = conf.New() +} + +// New creates an ArmourStandBehaviour using the optional parameters in conf. +func (conf ArmourStandBehaviourConfig) New() *ArmourStandBehaviour { + a := &ArmourStandBehaviour{ + conf: conf, + armours: conf.Armour, + lastOnGround: true, + } + a.passive = PassiveBehaviourConfig{ + Gravity: 0.04, + Drag: 0.02, + Tick: a.tick, + }.New() + return a +} + +// ArmourStandBehaviour implements the behaviour for armour stand entities. +type ArmourStandBehaviour struct { + conf ArmourStandBehaviourConfig + + passive *PassiveBehaviour + invulnerable time.Duration + armours *inventory.Armour + + lastOnGround bool +} + +// armourStandDropOffset returns the position offset at which an item should be +// dropped from an armour stand, based on the type of item. +func armourStandDropOffset(stack item.Stack) mgl64.Vec3 { + var offset mgl64.Vec3 + switch stack.Item().(type) { + case item.Helmet: + offset[1] = 1.8 + case item.Chestplate: + offset[1] = 1.4 + case item.Leggings: + offset[1] = 0.6 + case item.Boots: + offset[1] = 0.2 + default: + offset[1] = 1.4 + } + return offset +} + +// Tick ... +func (a *ArmourStandBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { + return a.passive.Tick(e, tx) +} + +// tick ... +func (a *ArmourStandBehaviour) tick(e *Ent, tx *world.Tx) { + if a.invulnerable > 0 { + a.invulnerable -= time.Millisecond * 50 + if a.invulnerable < 0 { + a.invulnerable = 0 + } + a.updateState(e) + } + + if a.lastOnGround != a.passive.mc.OnGround() { + if !a.lastOnGround { + tx.PlaySound(e.Position(), sound.ArmourStandLand{}) + } + a.lastOnGround = a.passive.mc.OnGround() + } +} + +// Explode ... +func (a *ArmourStandBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, config block.ExplosionConfig) { + a.passive.Explode(e, src, impact, config) +} + +// AcceptItem ... +func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, tx *world.Tx, ctx *item.UseContext) bool { + if sneaker, ok := from.(interface { + Sneaking() bool + }); ok && sneaker.Sneaking() { + a.SetPoseIndex(e, (a.PoseIndex()+1)%13) + return false + } + + heldItems, ok := from.(interface { + HeldItems() (mainHand, offHand item.Stack) + }) + if !ok { + return false + } + mainHand, _ := heldItems.HeldItems() + if mainHand.Empty() { + return false + } + var ( + dropItem item.Stack + dropOffset mgl64.Vec3 + add = mainHand.Grow(-mainHand.Count() + 1) + ) + i := add.Item() + inv := a.armours + if _, isArmour := i.(item.Armour); isArmour { + switch i.(type) { + case item.Helmet: + dropItem = inv.Helmet() + inv.SetHelmet(add) + case item.Chestplate: + dropItem = inv.Chestplate() + inv.SetChestplate(add) + case item.Leggings: + dropItem = inv.Leggings() + inv.SetLeggings(add) + case item.Boots: + dropItem = inv.Boots() + inv.SetBoots(add) + } + a.updateArmours(e) + } else { + it, left := a.HeldItems() + dropItem = it + a.SetHeldItems(e, add, left) + } + + dropOffset = armourStandDropOffset(dropItem) + ctx.SubtractFromCount(1) + if !dropItem.Empty() { + tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: e.Position().Add(dropOffset)}, dropItem)) + } + return true +} + +// Attack ... +func (a *ArmourStandBehaviour) Attack(e *Ent, _ world.Entity, tx *world.Tx) { + if a.invulnerable > 0 { + a.destroy(e, tx) + return + } + tx.PlaySound(e.Position(), sound.ArmourStandPlace{}) + a.invulnerable = time.Second / 2 + a.updateState(e) +} + +// destroy destroys the armour stand, dropping all equipped items. +func (a *ArmourStandBehaviour) destroy(e *Ent, tx *world.Tx) { + tx.PlaySound(e.Position(), sound.ArmourStandBreak{}) + a.dropAll(e, tx) + _ = e.Close() +} + +// InvulnerableDuration returns the duration for which the armour stand is invulnerable. +func (a *ArmourStandBehaviour) InvulnerableDuration() time.Duration { + return a.invulnerable +} + +// dropAll drops all equipped items of the armour stand. +func (a *ArmourStandBehaviour) dropAll(e *Ent, tx *world.Tx) { + dropPos := e.Position() + drop := func(stack item.Stack) { + if stack.Empty() { + return + } + tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: dropPos.Add(armourStandDropOffset(stack))}, stack)) + } + for _, i := range a.armours.Items() { + drop(i) + } + mainHand, offHand := a.HeldItems() + drop(mainHand) + drop(offHand) +} + +// Armour returns the armour equipped on the armour stand. +func (a *ArmourStandBehaviour) Armour() *inventory.Armour { + return a.armours +} + +// HeldItems returns the items equipped in the main hand and offhand slots of the armour stand. +func (a *ArmourStandBehaviour) HeldItems() (mainHand, offHand item.Stack) { + return a.conf.MainHand, a.conf.OffHand +} + +// PoseIndex returns the pose index of the armour stand. Possible values range +// from 0 to 12 (inclusive). +func (a *ArmourStandBehaviour) PoseIndex() int { + return a.conf.PoseIndex +} + +// SetHeldItems sets the items equipped in the main hand and offhand slots of the armour stand. +func (a *ArmourStandBehaviour) SetHeldItems(e *Ent, mainHand, offHand item.Stack) { + a.conf.MainHand = mainHand + a.conf.OffHand = offHand + a.updateHeldItems(e) +} + +// SetPoseIndex sets the pose index of the armour stand. Possible values range +// from 0 to 12 (inclusive). +func (a *ArmourStandBehaviour) SetPoseIndex(e *Ent, poseIndex int) { + if poseIndex < 0 || poseIndex > 12 { + panic("pose index must be between 0 and 12") + } + a.conf.PoseIndex = poseIndex + a.updateState(e) +} + +// updateArmours updates the armour stand's equipped items for all viewers. +func (a *ArmourStandBehaviour) updateArmours(e *Ent) { + for _, v := range e.tx.Viewers(e.Position()) { + v.ViewEntityArmour(e) + } +} + +// updateHeldItems updates the armour stand's held items for all viewers. +func (a *ArmourStandBehaviour) updateHeldItems(e *Ent) { + for _, v := range e.tx.Viewers(e.Position()) { + v.ViewEntityItems(e) + } +} + +// updateState updates the armour stand's state for all viewers. +func (a *ArmourStandBehaviour) updateState(e *Ent) { + for _, v := range e.tx.Viewers(e.data.Pos) { + v.ViewEntityState(e) + } +} diff --git a/server/entity/ent.go b/server/entity/ent.go index e3bb889fb..450426ea5 100644 --- a/server/entity/ent.go +++ b/server/entity/ent.go @@ -3,6 +3,7 @@ package entity import ( "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" "sync" @@ -17,6 +18,23 @@ type Behaviour interface { Tick(e *Ent, tx *world.Tx) *Movement } +// ItemAcceptor is an interface that Behaviours or world.Entity may implement +// to accept items from players. +type ItemAcceptor interface { + // AcceptItem returns whether the entity accepts the item stack passed. This + // may be called when a player tries to interact with the entity. + AcceptItem(from world.Entity, tx *world.Tx, ctx *item.UseContext) bool +} + +// Attackable is an interface that Behaviours or world.Entity may implement +// to allow entities to be attacked by other entities. +type Attackable interface { + // Attack is called when the entity is attacked by an attacker. Unlike world.Living + // entities, no damage value is passed. Used for entities that do not have health but + // can still be interacted with through attacks like armour stands. + Attack(attacker world.Entity, tx *world.Tx) +} + // Ent is a world.Entity implementation that allows entity implementations to // share a lot of code. It is currently under development and is prone to // (breaking) changes. @@ -115,6 +133,26 @@ func (e *Ent) SetNameTag(s string) { } } +// AcceptItem returns whether the entity accepts the item stack passed. This may be +// called when a player tries to interact with the entity. +func (e *Ent) AcceptItem(from world.Entity, tx *world.Tx, ctx *item.UseContext) bool { + if acceptor, ok := e.Behaviour().(interface { + AcceptItem(e *Ent, from world.Entity, tx *world.Tx, ctx *item.UseContext) bool + }); ok { + return acceptor.AcceptItem(e, from, tx, ctx) + } + return false +} + +// Attack is called when the entity is attacked by an attacker. +func (e *Ent) Attack(attacker world.Entity, tx *world.Tx) { + if attackable, ok := e.Behaviour().(interface { + Attack(e *Ent, attacker world.Entity, tx *world.Tx) + }); ok { + attackable.Attack(e, attacker, tx) + } +} + // Tick ticks Ent, progressing its lifetime and closing the entity if it is // in the void. func (e *Ent) Tick(tx *world.Tx, current int64) { diff --git a/server/entity/register.go b/server/entity/register.go index e9c54922e..edb695944 100644 --- a/server/entity/register.go +++ b/server/entity/register.go @@ -11,6 +11,7 @@ import ( // implemented by Dragonfly. var DefaultRegistry = conf.New([]world.EntityType{ AreaEffectCloudType, + ArmourStandType, ArrowType, BottleOfEnchantingType, EggType, @@ -35,6 +36,7 @@ var conf = world.EntityRegistryConfig{ EnderPearl: NewEnderPearl, FallingBlock: NewFallingBlock, Lightning: NewLightning, + ArmourStand: NewArmourStand, Firework: func(opts world.EntitySpawnOpts, firework world.Item, owner world.Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *world.EntityHandle { return newFirework(opts, firework.(item.Firework), owner, sidewaysVelocityMultiplier, upwardsAcceleration, attached) }, diff --git a/server/item/armour_stand.go b/server/item/armour_stand.go new file mode 100644 index 000000000..16b629b9e --- /dev/null +++ b/server/item/armour_stand.go @@ -0,0 +1,28 @@ +package item + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" +) + +// ArmourStand is an armour stand item. It can be placed to create an armour stand +// entity that can hold and display armour and other items. +type ArmourStand struct{} + +// UseOnBlock ... +func (ArmourStand) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user User, ctx *UseContext) bool { + spawnPos := pos.Side(face).Vec3Middle() + opts := world.EntitySpawnOpts{Position: spawnPos, Rotation: user.Rotation().Neg()} + create := tx.World().EntityRegistry().Config().ArmourStand + tx.AddEntity(create(opts)) + ctx.SubtractFromCount(1) + tx.PlaySound(spawnPos, sound.ArmourStandPlace{}) + return true +} + +// EncodeItem ... +func (ArmourStand) EncodeItem() (name string, meta int16) { + return "minecraft:armor_stand", 0 +} diff --git a/server/item/register.go b/server/item/register.go index 7e18533c0..af890821b 100644 --- a/server/item/register.go +++ b/server/item/register.go @@ -10,6 +10,7 @@ import ( func init() { world.RegisterItem(AmethystShard{}) world.RegisterItem(Apple{}) + world.RegisterItem(ArmourStand{}) world.RegisterItem(Arrow{}) world.RegisterItem(BakedPotato{}) world.RegisterItem(Beef{Cooked: true}) diff --git a/server/player/player.go b/server/player/player.go index 04ae1826b..5bdc5f9ff 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1733,13 +1733,20 @@ func (p *Player) UseItemOnEntity(e world.Entity) bool { return false } i, left := p.HeldItems() - usable, ok := i.Item().(item.UsableOnEntity) - if !ok { - return true - } useCtx := p.useContext() - if !usable.UseOnEntity(e, p.tx, p, useCtx) { - return true + used := false + if usable, ok := i.Item().(item.UsableOnEntity); ok { + if usable.UseOnEntity(e, p.tx, p, useCtx) { + used = true + } + } + if acceptor, ok := e.(entity.ItemAcceptor); ok { + if acceptor.AcceptItem(p, p.tx, useCtx) { + used = true + } + } + if !used { + return false } p.SwingArm() p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left) @@ -1782,6 +1789,10 @@ func (p *Player) AttackEntity(e world.Entity) bool { } p.SwingArm() + if attackable, ok := e.(entity.Attackable); ok { + attackable.Attack(p, p.tx) + } + if !isLiving { return false } diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go index f4908d907..3a25eddd6 100644 --- a/server/session/entity_metadata.go +++ b/server/session/entity_metadata.go @@ -176,6 +176,12 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { if mv, ok := e.(markVariable); ok { m[protocol.EntityDataKeyMarkVariant] = mv.MarkVariant() } + if iv, ok := e.(invulnerableDuration); ok { + m[protocol.EntityDataKeyInvulnerableTicks] = uint8(iv.InvulnerableDuration().Milliseconds() / 50) + } + if pi, ok := e.(armourStand); ok { + m[protocol.EntityDataKeyPoseIndex] = int32(pi.PoseIndex()) + } } type sneaker interface { @@ -290,3 +296,11 @@ type variable interface { type markVariable interface { MarkVariant() int32 } + +type armourStand interface { + PoseIndex() int +} + +type invulnerableDuration interface { + InvulnerableDuration() time.Duration +} diff --git a/server/session/world.go b/server/session/world.go index b05ad0bc5..02acd6425 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -41,6 +41,16 @@ type OffsetEntity interface { NetworkOffset() float64 } +// ItemHelder is an entity that can hold an item. +type ItemHelder interface { + HeldItems() (mainHand, offHand item.Stack) +} + +// ArmourWearer is an entity that can wear armour. +type ArmourWearer interface { + Armour() *inventory.Armour +} + // entityHidden checks if a world.Entity is being explicitly hidden from the Session. func (s *Session) entityHidden(e world.Entity) bool { s.entityMutex.RLock() @@ -277,9 +287,13 @@ func (s *Session) ViewEntityItems(e world.Entity) { // Don't view the items of the entity if the entity is the Controllable entity of the session. return } - c, ok := e.(item.Carrier) + c, ok := e.(interface{ ItemHelder }) if !ok { - return + if b, ok2 := e.(*entity.Ent); ok2 { + if c, ok = b.Behaviour().(ItemHelder); !ok { + return + } + } } mainHand, offHand := c.HeldItems() @@ -304,13 +318,14 @@ func (s *Session) ViewEntityArmour(e world.Entity) { // Don't view the items of the entity if the entity is the Controllable entity of the session. return } - armoured, ok := e.(interface { - Armour() *inventory.Armour - }) + armoured, ok := e.(ArmourWearer) if !ok { - return + if b, ok2 := e.(*entity.Ent); ok2 { + if armoured, ok = b.Behaviour().(ArmourWearer); !ok { + return + } + } } - inv := armoured.Armour() // Show the entity's armour @@ -872,6 +887,30 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) Volume: 1, Pitch: 1.0, }) + case sound.ArmourStandHit: + s.writePacket(&packet.LevelEvent{ + EventType: packet.LevelEventSoundArmorStandHit, + Position: vec64To32(pos), + }) + return + case sound.ArmourStandBreak: + s.writePacket(&packet.LevelEvent{ + EventType: packet.LevelEventSoundArmorStandBreak, + Position: vec64To32(pos), + }) + return + case sound.ArmourStandPlace: + s.writePacket(&packet.LevelEvent{ + EventType: packet.LevelEventSoundArmorStandPlace, + Position: vec64To32(pos), + }) + return + case sound.ArmourStandLand: + s.writePacket(&packet.LevelEvent{ + EventType: packet.LevelEventSoundArmorStandLand, + Position: vec64To32(pos), + }) + return } s.writePacket(pk) } diff --git a/server/world/entity.go b/server/world/entity.go index 69a0b83cb..1c19ad28b 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -373,6 +373,7 @@ type EntityRegistryConfig struct { Snowball func(opts EntitySpawnOpts, owner Entity) *EntityHandle SplashPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle Lightning func(opts EntitySpawnOpts) *EntityHandle + ArmourStand func(opts EntitySpawnOpts) *EntityHandle } // New creates an EntityRegistry using conf and the EntityTypes passed. diff --git a/server/world/sound/armour_stand.go b/server/world/sound/armour_stand.go new file mode 100644 index 000000000..97679801e --- /dev/null +++ b/server/world/sound/armour_stand.go @@ -0,0 +1,13 @@ +package sound + +// ArmourStandBreak is the sound played when an armour stand is broken. +type ArmourStandBreak struct{ sound } + +// ArmourStandHit is the sound played when an armour stand is hit. +type ArmourStandHit struct{ sound } + +// ArmourStandPlace is the sound played when an armour stand is placed. +type ArmourStandPlace struct{ sound } + +// ArmourStandLand is the sound played when an armour stand lands. +type ArmourStandLand struct{ sound } From 93082a4bbb819832606f26be5fb35dd97ae6b0fe Mon Sep 17 00:00:00 2001 From: AkmalFairuz Date: Fri, 9 Jan 2026 18:28:25 +0700 Subject: [PATCH 2/5] Store the drop item to held items first --- server/entity/armour_stand_behaviour.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/entity/armour_stand_behaviour.go b/server/entity/armour_stand_behaviour.go index e1bae0355..1017a9a44 100644 --- a/server/entity/armour_stand_behaviour.go +++ b/server/entity/armour_stand_behaviour.go @@ -152,6 +152,15 @@ func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, tx *world.T dropOffset = armourStandDropOffset(dropItem) ctx.SubtractFromCount(1) if !dropItem.Empty() { + if p, ok := from.(interface { + HeldItems() (mainHand, offHand item.Stack) + SetHeldItems(mainHand, offHand item.Stack) + }); ok { + i, left := p.HeldItems() + s1, s2 := i.AddStack(dropItem) + p.SetHeldItems(s1, left) + dropItem = s2 + } tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: e.Position().Add(dropOffset)}, dropItem)) } return true From 82e76c627eae9592183e8129abf7a09146d264b4 Mon Sep 17 00:00:00 2001 From: AkmalFairuz Date: Fri, 9 Jan 2026 18:37:56 +0700 Subject: [PATCH 3/5] Fix AcceptItem --- server/entity/armour_stand_behaviour.go | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/server/entity/armour_stand_behaviour.go b/server/entity/armour_stand_behaviour.go index 1017a9a44..8b94eca02 100644 --- a/server/entity/armour_stand_behaviour.go +++ b/server/entity/armour_stand_behaviour.go @@ -102,7 +102,7 @@ func (a *ArmourStandBehaviour) Explode(e *Ent, src mgl64.Vec3, impact float64, c } // AcceptItem ... -func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, tx *world.Tx, ctx *item.UseContext) bool { +func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, _ *world.Tx, ctx *item.UseContext) bool { if sneaker, ok := from.(interface { Sneaking() bool }); ok && sneaker.Sneaking() { @@ -121,9 +121,8 @@ func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, tx *world.T return false } var ( - dropItem item.Stack - dropOffset mgl64.Vec3 - add = mainHand.Grow(-mainHand.Count() + 1) + dropItem item.Stack + add = mainHand.Grow(-mainHand.Count() + 1) ) i := add.Item() inv := a.armours @@ -149,20 +148,8 @@ func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, tx *world.T a.SetHeldItems(e, add, left) } - dropOffset = armourStandDropOffset(dropItem) ctx.SubtractFromCount(1) - if !dropItem.Empty() { - if p, ok := from.(interface { - HeldItems() (mainHand, offHand item.Stack) - SetHeldItems(mainHand, offHand item.Stack) - }); ok { - i, left := p.HeldItems() - s1, s2 := i.AddStack(dropItem) - p.SetHeldItems(s1, left) - dropItem = s2 - } - tx.AddEntity(NewItem(world.EntitySpawnOpts{Position: e.Position().Add(dropOffset)}, dropItem)) - } + ctx.NewItem, ctx.NewItemSurvivalOnly = dropItem, true return true } From 765d49795a4da4c917657760bea825584c609251 Mon Sep 17 00:00:00 2001 From: AkmalFairuz Date: Fri, 9 Jan 2026 23:29:45 +0700 Subject: [PATCH 4/5] return true even not used --- server/player/player.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/player/player.go b/server/player/player.go index 5bdc5f9ff..c45f588ab 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1746,7 +1746,7 @@ func (p *Player) UseItemOnEntity(e world.Entity) bool { } } if !used { - return false + return true } p.SwingArm() p.SetHeldItems(p.subtractItem(p.damageItem(i, useCtx.Damage), useCtx.CountSub), left) From 50101dc8d7698c24728c6296331738883893aa4a Mon Sep 17 00:00:00 2001 From: AkmalFairuz Date: Sat, 10 Jan 2026 17:35:13 +0700 Subject: [PATCH 5/5] Refactor ArmourStandBehaviour: rename invulnerable to hurt and update related methods --- server/entity/armour_stand_behaviour.go | 26 ++++++++++++------------- server/session/entity_metadata.go | 8 ++++---- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/server/entity/armour_stand_behaviour.go b/server/entity/armour_stand_behaviour.go index 8b94eca02..a11a4303d 100644 --- a/server/entity/armour_stand_behaviour.go +++ b/server/entity/armour_stand_behaviour.go @@ -18,7 +18,7 @@ type ArmourStandBehaviourConfig struct { MainHand item.Stack // OffHand is the item equipped in the offhand slot of the armour stand. OffHand item.Stack - // PoseIndex is the pose index of the armor stand. Possible values range + // PoseIndex is the pose index of the armour stand. Possible values range // from 0 to 12 (inclusive). PoseIndex int } @@ -47,9 +47,9 @@ func (conf ArmourStandBehaviourConfig) New() *ArmourStandBehaviour { type ArmourStandBehaviour struct { conf ArmourStandBehaviourConfig - passive *PassiveBehaviour - invulnerable time.Duration - armours *inventory.Armour + passive *PassiveBehaviour + hurt time.Duration + armours *inventory.Armour lastOnGround bool } @@ -80,10 +80,10 @@ func (a *ArmourStandBehaviour) Tick(e *Ent, tx *world.Tx) *Movement { // tick ... func (a *ArmourStandBehaviour) tick(e *Ent, tx *world.Tx) { - if a.invulnerable > 0 { - a.invulnerable -= time.Millisecond * 50 - if a.invulnerable < 0 { - a.invulnerable = 0 + if a.hurt > 0 { + a.hurt -= time.Millisecond * 50 + if a.hurt < 0 { + a.hurt = 0 } a.updateState(e) } @@ -155,12 +155,12 @@ func (a *ArmourStandBehaviour) AcceptItem(e *Ent, from world.Entity, _ *world.Tx // Attack ... func (a *ArmourStandBehaviour) Attack(e *Ent, _ world.Entity, tx *world.Tx) { - if a.invulnerable > 0 { + if a.hurt > 0 { a.destroy(e, tx) return } tx.PlaySound(e.Position(), sound.ArmourStandPlace{}) - a.invulnerable = time.Second / 2 + a.hurt = time.Millisecond * 300 a.updateState(e) } @@ -171,9 +171,9 @@ func (a *ArmourStandBehaviour) destroy(e *Ent, tx *world.Tx) { _ = e.Close() } -// InvulnerableDuration returns the duration for which the armour stand is invulnerable. -func (a *ArmourStandBehaviour) InvulnerableDuration() time.Duration { - return a.invulnerable +// HurtDuration returns the remaining hurt duration of the armour stand. +func (a *ArmourStandBehaviour) HurtDuration() time.Duration { + return a.hurt } // dropAll drops all equipped items of the armour stand. diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go index 3a25eddd6..0a16c63f1 100644 --- a/server/session/entity_metadata.go +++ b/server/session/entity_metadata.go @@ -176,8 +176,8 @@ func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) { if mv, ok := e.(markVariable); ok { m[protocol.EntityDataKeyMarkVariant] = mv.MarkVariant() } - if iv, ok := e.(invulnerableDuration); ok { - m[protocol.EntityDataKeyInvulnerableTicks] = uint8(iv.InvulnerableDuration().Milliseconds() / 50) + if iv, ok := e.(hurtDuration); ok { + m[protocol.EntityDataKeyHurt] = uint8(iv.HurtDuration().Milliseconds() / 50) } if pi, ok := e.(armourStand); ok { m[protocol.EntityDataKeyPoseIndex] = int32(pi.PoseIndex()) @@ -301,6 +301,6 @@ type armourStand interface { PoseIndex() int } -type invulnerableDuration interface { - InvulnerableDuration() time.Duration +type hurtDuration interface { + HurtDuration() time.Duration }