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..a11a4303d --- /dev/null +++ b/server/entity/armour_stand_behaviour.go @@ -0,0 +1,248 @@ +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 armour 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 + hurt 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.hurt > 0 { + a.hurt -= time.Millisecond * 50 + if a.hurt < 0 { + a.hurt = 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, _ *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 + 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) + } + + ctx.SubtractFromCount(1) + ctx.NewItem, ctx.NewItemSurvivalOnly = dropItem, true + return true +} + +// Attack ... +func (a *ArmourStandBehaviour) Attack(e *Ent, _ world.Entity, tx *world.Tx) { + if a.hurt > 0 { + a.destroy(e, tx) + return + } + tx.PlaySound(e.Position(), sound.ArmourStandPlace{}) + a.hurt = time.Millisecond * 300 + 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() +} + +// 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. +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 786af8575..7f2f6f6d2 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 11dd5c13c..73d42dbb4 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -1731,12 +1731,19 @@ 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) { + 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 true } p.SwingArm() @@ -1780,6 +1787,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 427266aac..82431885e 100644 --- a/server/session/entity_metadata.go +++ b/server/session/entity_metadata.go @@ -186,6 +186,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.(hurtDuration); ok { + m[protocol.EntityDataKeyHurt] = uint8(iv.HurtDuration().Milliseconds() / 50) + } + if pi, ok := e.(armourStand); ok { + m[protocol.EntityDataKeyPoseIndex] = int32(pi.PoseIndex()) + } } type sneaker interface { @@ -304,3 +310,11 @@ type variable interface { type markVariable interface { MarkVariant() int32 } + +type armourStand interface { + PoseIndex() int +} + +type hurtDuration interface { + HurtDuration() time.Duration +} diff --git a/server/session/world.go b/server/session/world.go index 569f390f6..8c1339400 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 dcc415ce9..e8437e095 100644 --- a/server/world/entity.go +++ b/server/world/entity.go @@ -374,6 +374,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 }