26ed99fda6
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
465 lines
17 KiB
Go
465 lines
17 KiB
Go
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
|
|
}
|