Files
mc/server/world/entity.go
T

465 lines
17 KiB
Go
Raw Normal View History

2026-07-09 08:33:57 +08:00
package world
import (
"encoding/binary"
"io"
"maps"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/go-gl/mathgl/mgl64"
"github.com/google/uuid"
)
// EntityType is the type of Entity. It specifies the name, encoded Entity
// ID and bounding box of an Entity.
type EntityType interface {
// Open returns an Entity implementation in the context of a transaction.
Open(tx *Tx, handle *EntityHandle, data *EntityData) Entity
// EncodeEntity converts the Entity to its encoded representation: It
// returns the type of the Minecraft Entity, for example
// 'minecraft:falling_block'.
EncodeEntity() string
// BBox returns the bounding box of an Entity with this EntityType.
BBox(e Entity) cube.BBox
// DecodeNBT reads the fields from the NBT data map passed and converts it
// to an Entity of the same EntityType.
DecodeNBT(m map[string]any, data *EntityData)
// EncodeNBT encodes the Entity of the same EntityType passed to a map of
// properties that can be encoded to NBT.
EncodeNBT(data *EntityData) map[string]any
}
// EntityConfig is used to configure the initial settings of an Entity upon
// creation using NewEntity.
type EntityConfig interface {
Apply(data *EntityData)
}
// EntityHandle is a persistent identifier of an entity. It holds data of the
// entity that can be transformed into an Entity implementation in the context
// of a transaction.
type EntityHandle struct {
id uuid.UUID
t EntityType
cond *sync.Cond
worldless *atomic.Bool
weakTxActive bool
w *World
data EntityData
// TODO Handler? Handle world change here?
}
// EntitySpawnOpts holds spawning related options for entities created.
type EntitySpawnOpts struct {
// Position is the position that an Entity should be spawned at.
Position mgl64.Vec3
// Rotation is the rotation that an Entity should be spawned with.
Rotation cube.Rotation
// Velocity specifies the initial velocity of the Entity.
Velocity mgl64.Vec3
// ID specifies the UUID of an entity. This field should usually be left
// empty, as a valid UUID is generated when not set. Non-player entities
// only have the last 8 bytes of the UUID set.
ID uuid.UUID
// NameTag is the name tag that the entity is spawned with.
NameTag string
}
// New creates an EntityHandle using an EntityType and EntityConfig passed. The
// EntityHandle may be added to a world by calling Tx.AddEntity().
// The spawn conditions depend on the options set in opts.
func (opts EntitySpawnOpts) New(t EntityType, conf EntityConfig) *EntityHandle {
if opts.ID == uuid.Nil {
// Generate a new UUID with only the upper 8 bytes filled. This UUID
// needs to be translatable to an int64.
opts.ID = uuid.New()
clear(opts.ID[:8])
}
handle := &EntityHandle{id: opts.ID, t: t, cond: sync.NewCond(&sync.Mutex{}), worldless: &atomic.Bool{}}
handle.worldless.Store(true)
handle.data.Pos, handle.data.Rot, handle.data.Vel = opts.Position, opts.Rotation, opts.Velocity
handle.data.Name = opts.NameTag
conf.Apply(&handle.data)
return handle
}
// NewEntity creates an EntityHandle using an EntityType and EntityConfig
// passed. The EntityHandle may be added to a world by calling Tx.AddEntity().
// NewEntity uses the zero value for EntitySpawnOpts.
func NewEntity(t EntityType, conf EntityConfig) *EntityHandle {
var opts EntitySpawnOpts
return opts.New(t, conf)
}
// entityFromData reads an entity from the decoded NBT data passed and returns
// an EntityHandle.
func entityFromData(t EntityType, id int64, data map[string]any) *EntityHandle {
handle := &EntityHandle{t: t, cond: sync.NewCond(&sync.Mutex{}), worldless: &atomic.Bool{}}
binary.LittleEndian.PutUint64(handle.id[8:], uint64(id))
handle.decodeNBT(data)
t.DecodeNBT(data, &handle.data)
return handle
}
// Type returns the EntityType of the EntityHandle.
func (e *EntityHandle) Type() EntityType {
return e.t
}
// Entity attempts to convert an EntityHandle to an Entity using the Tx passed.
// A non-nil Entity is returned only if the entity's world matches the world of
// the Tx. If they do not match, false is returned.
func (e *EntityHandle) Entity(tx *Tx) (Entity, bool) {
if e == nil || e.w != tx.World() {
return nil, false
}
return e.t.Open(tx, e, &e.data), true
}
// mustEntity calls Entity but panics if the worlds do not match.
func (e *EntityHandle) mustEntity(tx *Tx) Entity {
if ent, ok := e.Entity(tx); ok {
return ent
}
panic("can't load entity with Tx of different world")
}
// UUID returns the identifier of the EntityHandle.
func (e *EntityHandle) UUID() uuid.UUID {
return e.id
}
// Close closes the EntityHandle. Any subsequent call to ExecWorld will return
// immediately without the transaction function being called. Close always
// returns nil.
func (e *EntityHandle) Close() error {
e.setAndUnlockWorld(closeWorld)
return nil
}
// ExecWorld obtains the EntityHandle's World in a thread-safe way and opens a
// transaction in it when it does. If the EntityHandle has not been added to a
// world, ExecWorld will block until the EntityHandle is added to a World and
// run the transaction function once it is. If the Entity is closed before
// ExecWorld is called, ExecWorld will return false immediately without running
// the transaction function.
func (e *EntityHandle) ExecWorld(f func(tx *Tx, e Entity)) bool {
return e.execWorld(f, false)
}
// execWorld uses a sync.Cond to synchronise access to the handler's world. We
// are dealing with a rather complicated synchronisation pattern here. The goal
// for ExecWorld is to block until e.w becomes accessible. Meanwhile, World.Exec
// may also affect e.w, which execWorld needs to deal with.
func (e *EntityHandle) execWorld(f func(tx *Tx, e Entity), weak bool) bool {
e.cond.L.Lock()
for e.w == nil || (!weak && e.weakTxActive) {
// Wait suspends the current goroutine and unlocks e.cond.L, until
// e.cond.Broadcast() is called. After this, one of the goroutines
// waiting will acquire a lock of e.cond.L again. This means that only
// one goroutine will run the code after this simultaneously.
e.cond.Wait()
}
// If a goroutine manages to exit the for loop, it will have acquired a lock
// on e.cond.L. This also means that e.w can be assumed to not be nil here.
// Because of the lock on e.cond.L, no other transaction will be able to
// change e.w until we finish. e.worldless is set to true in
// e.unsetAndLockWorld(), where the entity's world is removed.
e.worldless.Store(false)
if e.w == closeWorld {
// EntityHandle was closed. No need to continue.
e.cond.L.Unlock()
return false
}
// We now arrive at the more complicated part. When we call e.w.Exec(), our
// transaction must await earlier transactions in the world. If one of those
// earlier transactions tries to change e.w (through e.unsetAndLockWorld()
// or e.setAndUnlockWorld()), it must lock e.cond.L. This would lead to a
// deadlock, because we already have e.cond.L locked here.
// We work around this with so-called "weak transactions". This is a
// transaction that may be invalidated before it is executed. In this case,
// this invalidation happens by setting e.worldless to true. If the
// transaction turns out to be invalidated (ret == false), we simply try
// again, this time with e.execWorld(f, true) to make this goroutine bypass
// any goroutines still awaiting e.cond.
ret := e.weakExec(func(tx *Tx) { f(tx, e.mustEntity(tx)) })
e.cond.L.Unlock()
if !ret {
// Our weak transaction was suspended. We try again, this time with
// e.execWorld(f, true) to make this goroutine bypass any goroutines
// still awaiting e.cond.
return e.execWorld(f, true)
}
return true
}
// weakExec performs a "weak transaction". It adds a transaction to the world
// that is invalidated when e.worldless is set to true. In this case, weakExec
// returns false. If the weak transaction is successfully executed, it returns
// true, and any calls to ExecWorld waiting on e.cond are awakened. The goal of
// weakExec is to suspend the current goroutine and unlock e.cond.L while
// waiting for previous transactions to finish.
func (e *EntityHandle) weakExec(f ExecFunc) bool {
e.weakTxActive = true
// We create a weak transaction and start a for loop to listen for the
// length of the channel. This might look weird, but the crucial part here
// is the call to e.cond.Wait(), which unlocks e.cond.L. This is required
// to prevent a deadlock if an earlier transaction tries to change e.w.
c := e.w.weakExec(e.worldless, e.cond, f)
for len(c) == 0 && e.w != closeWorld {
// Calling e.cond.Wait() here will free the lock on e.cond.L until our
// transaction finishes. e.w.weakExec() ensures that e.cond.Broadcast()
// is called once the transaction finished/is suspended, so we can
// continue after that.
e.cond.Wait()
}
// If the EntityHandle was closed (e.w == closeWorld), we treat the
// transaction as successful, because all transactions must be cancelled.
if e.w != closeWorld && !<-c {
// Weak transaction was suspended. Return false and try again.
return false
}
// After setting e.weakTxActive back to false, we must Broadcast to make
// sure any goroutines waiting in e.execWorld as a result of the
// e.weakTxActive condition can continue.
e.weakTxActive = false
e.cond.Broadcast()
return true
}
var closeWorld = &World{}
// unsetAndLockWorld sets e.w to nil, causing any subsequent calls to ExecWorld
// to block until e.w is set to a non-nil value.
func (e *EntityHandle) unsetAndLockWorld() {
e.cond.L.Lock()
defer e.cond.L.Unlock()
e.worldless.Store(true)
e.w = nil
}
// setAndUnlockWorld sets e.w to a World passed and broadcasts e.cond, so that
// any goroutines waiting for a non-nil world are awoken.
func (e *EntityHandle) setAndUnlockWorld(w *World) {
e.cond.L.Lock()
defer e.cond.L.Unlock()
if e.w != nil {
panic("cannot add entity to new world before removing from old world")
}
e.w = w
e.cond.Broadcast()
}
// decodeNBT decodes the position, velocity, rotation, age, on-fire duration and
// name tag of an entity.
func (e *EntityHandle) decodeNBT(m map[string]any) {
e.data.Pos = readVec3(m, "Pos")
e.data.Vel = readVec3(m, "Motion")
e.data.Rot = readRotation(m)
e.data.Age = time.Duration(readInt16(m, "Age")) * (time.Second / 20)
e.data.FireDuration = time.Duration(readInt16(m, "Fire")) * time.Second / 20
e.data.Name, _ = m["NameTag"].(string)
}
// encodeNBT encodes the position, velocity, rotation, age, on-fire duration and
// name tag of an entity.
func (e *EntityHandle) encodeNBT() map[string]any {
return map[string]any{
"Pos": []float32{float32(e.data.Pos[0]), float32(e.data.Pos[1]), float32(e.data.Pos[2])},
"Motion": []float32{float32(e.data.Vel[0]), float32(e.data.Vel[1]), float32(e.data.Vel[2])},
"Yaw": float32(e.data.Rot[0]),
"Pitch": float32(e.data.Rot[1]),
"Fire": int16(e.data.FireDuration.Seconds() * 20),
"Age": int16(e.data.Age / (time.Second * 20)),
"NameTag": e.data.Name,
}
}
// EntityData holds data shared by every entity. It is kept in an EntityHandle.
type EntityData struct {
Pos, Vel mgl64.Vec3
Rot cube.Rotation
Name string
FireDuration time.Duration
Age time.Duration
Data any
}
// Entity represents an Entity in the world, typically an object that may be moved around and can be
// interacted with by other entities.
// Viewers of a world may view an Entity when near it.
type Entity interface {
io.Closer
// H returns the EntityHandle that points to the entity.
H() *EntityHandle
// Position returns the current position of the Entity in the world.
Position() mgl64.Vec3
// Rotation returns the yaw (horizontal rotation) and pitch (vertical
// rotation) of the entity in degrees.
Rotation() cube.Rotation
}
// TickerEntity represents an Entity that has a Tick method which should be called every time the Entity is
// ticked every 20th of a second.
type TickerEntity interface {
Entity
// Tick ticks the Entity with the current World and tick passed.
Tick(tx *Tx, current int64)
}
// EntityAction represents an action that may be performed by an Entity. Typically, these actions are sent to
// viewers in a world so that they can see these actions.
type EntityAction interface {
EntityAction()
}
// DamageSource represents the source of the damage dealt to an Entity. This
// source may be passed to the Hurt() method of an Entity in order to deal
// damage to an Entity with a specific source.
type DamageSource interface {
// ReducedByArmour checks if the source of damage may be reduced if the
// receiver of the damage is wearing armour.
ReducedByArmour() bool
// ReducedByResistance specifies if the Source is affected by the resistance
// effect. If false, damage dealt to an Entity with this source will not be
// lowered if the Entity has the resistance effect.
ReducedByResistance() bool
// Fire specifies if the Source is fire related and should be ignored when
// an Entity has the fire resistance effect.
Fire() bool
// IgnoreTotem specifies whether the totem will be ignored if the damage is lethal.
IgnoreTotem() bool
}
// HealingSource represents a source of healing for an Entity. This source may
// be passed to the Heal() method of a living Entity.
type HealingSource interface {
HealingSource()
}
// EntityRegistry is a mapping that EntityTypes may be registered to. It is used
// for loading entities from disk in a World's Provider.
type EntityRegistry struct {
conf EntityRegistryConfig
ent map[string]EntityType
}
// EntityRegistryConfig holds functions used by the block and item packages to
// create entities as a result of their behaviour. ALL functions of
// EntityRegistryConfig must be filled out for the behaviour of these blocks and
// items not to fail.
type EntityRegistryConfig struct {
Item func(opts EntitySpawnOpts, it any) *EntityHandle
FallingBlock func(opts EntitySpawnOpts, bl Block) *EntityHandle
TNT func(opts EntitySpawnOpts, fuse time.Duration) *EntityHandle
BottleOfEnchanting func(opts EntitySpawnOpts, owner Entity) *EntityHandle
Arrow func(opts EntitySpawnOpts, conf ArrowSpawnConfig) *EntityHandle
Egg func(opts EntitySpawnOpts, owner Entity) *EntityHandle
EnderPearl func(opts EntitySpawnOpts, owner Entity) *EntityHandle
Firework func(opts EntitySpawnOpts, firework Item, owner Entity, sidewaysVelocityMultiplier, upwardsAcceleration float64, attached bool) *EntityHandle
LingeringPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle
Snowball func(opts EntitySpawnOpts, owner Entity) *EntityHandle
SplashPotion func(opts EntitySpawnOpts, t any, owner Entity) *EntityHandle
Lightning func(opts EntitySpawnOpts) *EntityHandle
}
// ArrowSpawnConfig holds the options used to spawn an arrow entity.
type ArrowSpawnConfig struct {
// Damage specifies the base damage dealt by the arrow.
Damage float64
// Owner is the entity that fired the arrow.
Owner Entity
// Critical specifies if the arrow should deal critical damage.
Critical bool
// DisablePickup specifies if picking up the arrow should be disabled.
DisablePickup bool
// ObtainArrowOnPickup specifies if the arrow should be returned as an item when picked up.
ObtainArrowOnPickup bool
// PunchLevel specifies the level of punch knockback applied to the arrow.
PunchLevel int
// PiercingLevel is the crossbow Piercing enchantment level. The arrow passes
// through PiercingLevel entities and damages PiercingLevel+1 in total. A
// value of 0 means no piercing.
PiercingLevel int
// Tip specifies the potion tip carried by the arrow.
Tip any
}
// New creates an EntityRegistry using conf and the EntityTypes passed.
func (conf EntityRegistryConfig) New(ent []EntityType) EntityRegistry {
m := make(map[string]EntityType, len(ent))
for _, e := range ent {
name := e.EncodeEntity()
if _, ok := m[name]; ok {
panic("cannot register the same entity (" + name + ") twice")
}
m[name] = e
}
return EntityRegistry{conf: conf, ent: m}
}
// Config returns the EntityRegistryConfig that was used to create the
// EntityRegistry.
func (reg EntityRegistry) Config() EntityRegistryConfig {
return reg.conf
}
// Lookup looks up an EntityType by its name. If found, the EntityType is
// returned and the bool is true. The bool is false otherwise.
func (reg EntityRegistry) Lookup(name string) (EntityType, bool) {
t, ok := reg.ent[name]
return t, ok
}
// Types returns all EntityTypes passed upon construction of the EntityRegistry.
func (reg EntityRegistry) Types() []EntityType {
return slices.Collect(maps.Values(reg.ent))
}
func readVec3(x map[string]any, k string) mgl64.Vec3 {
if i, ok := x[k].([]any); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
var v mgl64.Vec3
for index, f := range i {
f32, _ := f.(float32)
v[index] = float64(f32)
}
return v
} else if i, ok := x[k].([]float32); ok {
if len(i) != 3 {
return mgl64.Vec3{}
}
return mgl64.Vec3{float64(i[0]), float64(i[1]), float64(i[2])}
}
return mgl64.Vec3{}
}
func readFloat32(m map[string]any, k string) float32 {
v, _ := m[k].(float32)
return v
}
func readRotation(m map[string]any) cube.Rotation {
return cube.Rotation{float64(readFloat32(m, "Yaw")), float64(readFloat32(m, "Pitch"))}
}
func readInt16(m map[string]any, k string) int16 {
v, _ := m[k].(int16)
return v
}