Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a fall manager for entities #549

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
142 changes: 142 additions & 0 deletions server/entity/fall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package entity

import (
"github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/entity/damage"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/world"
"github.com/go-gl/mathgl/mgl64"
"math"
"sync"
)

// FallManager handles entities that can fall.
type FallManager struct {
mu sync.Mutex
e fallEntity
fallDistance float64
HashimTheArab marked this conversation as resolved.
Show resolved Hide resolved
onGround bool
}

// fallEntity is an entity that can fall.
type fallEntity interface {
world.Entity
OnGround() bool
}

// entityLander represents a block that reacts to an entity landing on it after falling.
type entityLander interface {
// EntityLand is called when an entity lands on the block.
EntityLand(pos cube.Pos, w *world.World, e world.Entity)
}

// NewFallManager returns a new fall manager.
func NewFallManager(e fallEntity) *FallManager {
return &FallManager{e: e}
}

// SetFallDistance sets the fall distance of the entity.
func (f *FallManager) SetFallDistance(distance float64) {
f.mu.Lock()
defer f.mu.Unlock()
f.fallDistance = distance
}

// FallDistance returns the entity's fall distance.
func (f *FallManager) FallDistance() float64 {
f.mu.Lock()
defer f.mu.Unlock()
return f.fallDistance
}

// ResetFallDistance resets the player's fall distance.
func (f *FallManager) ResetFallDistance() {
f.mu.Lock()
defer f.mu.Unlock()
f.fallDistance = 0
}

// OnGround returns whether the entity is currently considered to be on the ground.
func (f *FallManager) OnGround() bool {
HashimTheArab marked this conversation as resolved.
Show resolved Hide resolved
if false { // todo: if f.e is a player
f.mu.Lock()
defer f.mu.Unlock()
return f.onGround
}
return f.e.OnGround()
}

// UpdateFallState is called to update the entities falling state.
func (f *FallManager) UpdateFallState(distanceThisTick float64) {
f.mu.Lock()
fallDistance := f.fallDistance
f.mu.Unlock()
if f.OnGround() {
if fallDistance > 0 {
f.fall(fallDistance)
f.ResetFallDistance()
}
} else if distanceThisTick < fallDistance {
f.mu.Lock()
f.fallDistance -= distanceThisTick
f.mu.Unlock()
} else {
f.ResetFallDistance()
}
}

// fall is called when a falling entity hits the ground.
func (f *FallManager) fall(distance float64) {
var (
w = f.e.World()
pos = cube.PosFromVec3(f.e.Position())
b = w.Block(pos)
dmg = distance - 3
)
if len(b.Model().BBox(pos, w)) == 0 {
pos = pos.Sub(cube.Pos{0, 1})
b = w.Block(pos)
}
if h, ok := b.(entityLander); ok {
h.EntityLand(pos, w, f.e)
}

if p, ok := f.e.(Living); ok {
if boost, ok := p.Effect(effect.JumpBoost{}); ok {
dmg -= float64(boost.Level())
}
if dmg < 0.5 {
return
}
p.Hurt(math.Ceil(dmg), damage.SourceFall{})
}
}

// CheckOnGround checks if the entity is currently considered to be on the ground.
func (f *FallManager) CheckOnGround(w *world.World) bool {
HashimTheArab marked this conversation as resolved.
Show resolved Hide resolved
box := f.e.BBox().Translate(f.e.Position())

b := box.Grow(1)

min, max := cube.PosFromVec3(b.Min()), cube.PosFromVec3(b.Max())
for x := min[0]; x <= max[0]; x++ {
for z := min[2]; z <= max[2]; z++ {
for y := min[1]; y < max[1]; y++ {
pos := cube.Pos{x, y, z}
boxList := w.Block(pos).Model().BBox(pos, w)
for _, bb := range boxList {
if bb.GrowVec3(mgl64.Vec3{0, 0.05}).Translate(pos.Vec3()).IntersectsWith(box) {
f.mu.Lock()
f.onGround = true
f.mu.Unlock()
return true
}
}
}
}
}
f.mu.Lock()
f.onGround = false
f.mu.Unlock()
return false
}
3 changes: 3 additions & 0 deletions server/entity/living.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ type Living interface {
AddEffect(e effect.Effect)
// RemoveEffect removes any effect that might currently be active on the entity.
RemoveEffect(e effect.Type)
// Effect returns the effect instance and true if the Player has the effect. If not found, it will return an empty
// effect instance and false.
Effect(e effect.Type) (effect.Effect, bool)
// Effects returns any effect currently applied to the entity. The returned effects are guaranteed not to have
// expired when returned.
Effects() []effect.Effect
Expand Down
84 changes: 11 additions & 73 deletions server/player/player.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ type Player struct {
invisible, immobile, onGround, usingItem atomic.Bool
usingSince atomic.Int64

fireTicks atomic.Int64
fallDistance atomic.Float64
fireTicks atomic.Int64

cooldownMu sync.Mutex
cooldowns map[itemHash]time.Time
Expand All @@ -78,6 +77,7 @@ type Player struct {
health *entity.HealthManager
experience *entity.ExperienceManager
effects *entity.EffectManager
fall *entity.FallManager

lastXPPickup atomic.Value[time.Time]
immunity atomic.Value[time.Time]
Expand Down Expand Up @@ -111,6 +111,7 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player {
health: entity.NewHealthManager(),
experience: entity.NewExperienceManager(),
effects: entity.NewEffectManager(),
fall: entity.NewFallManager(p),
gameMode: *atomic.NewValue[world.GameMode](world.GameModeSurvival),
h: *atomic.NewValue[Handler](NopHandler{}),
name: name,
Expand Down Expand Up @@ -254,12 +255,12 @@ func (p *Player) SendToast(title, message string) {

// ResetFallDistance resets the player's fall distance.
func (p *Player) ResetFallDistance() {
p.fallDistance.Store(0)
p.fall.ResetFallDistance()
}

// FallDistance returns the player's fall distance.
func (p *Player) FallDistance() float64 {
return p.fallDistance.Load()
return p.fall.FallDistance()
}

// SendTitle sends a title to the player. The title may be configured to change the duration it is displayed
Expand Down Expand Up @@ -465,46 +466,6 @@ func (p *Player) Heal(health float64, source healing.Source) {
p.addHealth(health)
}

// updateFallState is called to update the entities falling state.
func (p *Player) updateFallState(distanceThisTick float64) {
fallDistance := p.fallDistance.Load()
if p.OnGround() {
if fallDistance > 0 {
p.fall(fallDistance)
p.ResetFallDistance()
}
} else if distanceThisTick < fallDistance {
p.fallDistance.Sub(distanceThisTick)
} else {
p.ResetFallDistance()
}
}

// fall is called when a falling entity hits the ground.
func (p *Player) fall(distance float64) {
var (
w = p.World()
pos = cube.PosFromVec3(p.Position())
b = w.Block(pos)
dmg = distance - 3
)
if len(b.Model().BBox(pos, w)) == 0 {
pos = pos.Sub(cube.Pos{0, 1})
b = w.Block(pos)
}
if h, ok := b.(block.EntityLander); ok {
h.EntityLand(pos, w, p)
}

if boost, ok := p.Effect(effect.JumpBoost{}); ok {
dmg -= float64(boost.Level())
}
if dmg < 0.5 {
return
}
p.Hurt(math.Ceil(dmg), damage.SourceFall{})
}

// Hurt hurts the player for a given amount of damage. The source passed represents the cause of the damage,
// for example damage.SourceEntityAttack if the player is attacked by another entity.
// If the final damage exceeds the health that the player currently has, the player is killed and will have to
Expand Down Expand Up @@ -1842,9 +1803,9 @@ func (p *Player) Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64) {
}

p.checkBlockCollisions(w)
p.onGround.Store(p.checkOnGround(w))
p.fall.CheckOnGround(w)

p.updateFallState(deltaPos[1])
p.fall.UpdateFallState(deltaPos[1])

// The vertical axis isn't relevant for calculation of exhaustion points.
deltaPos[1] = 0
Expand Down Expand Up @@ -2047,7 +2008,7 @@ func (p *Player) Tick(w *world.World, current int64) {
}

p.checkBlockCollisions(w)
p.onGround.Store(p.checkOnGround(w))
p.fall.CheckOnGround(w)

p.tickFood(w)
p.effects.Tick(p)
Expand Down Expand Up @@ -2160,29 +2121,6 @@ func (p *Player) checkBlockCollisions(w *world.World) {
}
}

// checkOnGround checks if the player is currently considered to be on the ground.
func (p *Player) checkOnGround(w *world.World) bool {
box := p.BBox().Translate(p.Position())

b := box.Grow(1)

min, max := cube.PosFromVec3(b.Min()), cube.PosFromVec3(b.Max())
for x := min[0]; x <= max[0]; x++ {
for z := min[2]; z <= max[2]; z++ {
for y := min[1]; y < max[1]; y++ {
pos := cube.Pos{x, y, z}
boxList := w.Block(pos).Model().BBox(pos, w)
for _, bb := range boxList {
if bb.GrowVec3(mgl64.Vec3{0, 0.05}).Translate(pos.Vec3()).IntersectsWith(box) {
return true
}
}
}
}
}
return false
}

// BBox returns the axis aligned bounding box of the player.
func (p *Player) BBox() cube.BBox {
s := p.Scale()
Expand Down Expand Up @@ -2213,7 +2151,7 @@ func (p *Player) OnGround() bool {
if p.session() == session.Nop {
return p.mc.OnGround()
}
return p.onGround.Load()
return p.fall.OnGround()
}

// EyeHeight returns the eye height of the player: 1.62, or 0.52 if the player is swimming.
Expand Down Expand Up @@ -2431,7 +2369,7 @@ func (p *Player) load(data Data) {
p.AddEffect(potion)
}
p.fireTicks.Store(data.FireTicks)
p.fallDistance.Store(data.FallDistance)
p.fall.SetFallDistance(data.FallDistance)

p.loadInventory(data.Inventory)
}
Expand Down Expand Up @@ -2480,7 +2418,7 @@ func (p *Player) Data() Data {
},
Effects: p.Effects(),
FireTicks: p.fireTicks.Load(),
FallDistance: p.fallDistance.Load(),
FallDistance: p.fall.FallDistance(),
Dimension: p.World().Dimension().EncodeDimension(),
}
}
Expand Down