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
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:
@@ -0,0 +1,464 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user