diff --git a/server/block/bed.go b/server/block/bed.go
new file mode 100644
index 000000000..ed03f9803
--- /dev/null
+++ b/server/block/bed.go
@@ -0,0 +1,232 @@
+package block
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/block/model"
+ "github.com/df-mc/dragonfly/server/internal/nbtconv"
+ "github.com/df-mc/dragonfly/server/item"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/go-gl/mathgl/mgl64"
+ "github.com/sandertv/gophertunnel/minecraft/text"
+)
+
+// Bed is a block, allowing players to sleep to set their spawns and skip the night.
+type Bed struct {
+ transparent
+ sourceWaterDisplacer
+
+ // Colour is the colour of the bed.
+ Colour item.Colour
+ // Facing is the direction that the bed is facing.
+ Facing cube.Direction
+ // Head is true if the bed is the head side.
+ Head bool
+ // User is the user that is using the bed. It is only set for the Head part of the bed.
+ User item.User
+}
+
+// MaxCount always returns 1.
+func (Bed) MaxCount() int {
+ return 1
+}
+
+// Model ...
+func (Bed) Model() world.BlockModel {
+ return model.Bed{}
+}
+
+// SideClosed ...
+func (Bed) SideClosed(cube.Pos, cube.Pos, *world.World) bool {
+ return false
+}
+
+// BreakInfo ...
+func (b Bed) BreakInfo() BreakInfo {
+ return newBreakInfo(0.2, alwaysHarvestable, nothingEffective, oneOf(b)).withBreakHandler(func(pos cube.Pos, w *world.World, _ item.User) {
+ headSide, _, ok := b.head(pos, w)
+ if !ok {
+ return
+ }
+ if s, ok := headSide.User.(world.Sleeper); ok {
+ s.Wake()
+ }
+ })
+}
+
+// UseOnBlock ...
+func (b Bed) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) {
+ if pos, _, used = firstReplaceable(w, pos, face, b); !used {
+ return
+ }
+ if _, ok := w.Block(pos.Side(cube.FaceDown)).Model().(model.Solid); !ok {
+ return
+ }
+
+ b.Facing = user.Rotation().Direction()
+
+ side, sidePos := b, pos.Side(b.Facing.Face())
+ side.Head = true
+
+ if !replaceableWith(w, sidePos, side) {
+ return
+ }
+ if _, ok := w.Block(sidePos.Side(cube.FaceDown)).Model().(model.Solid); !ok {
+ return
+ }
+
+ ctx.IgnoreBBox = true
+ place(w, sidePos, side, user, ctx)
+ place(w, pos, b, user, ctx)
+ return placed(ctx)
+}
+
+// Activate ...
+func (b Bed) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ *item.UseContext) bool {
+ s, ok := u.(world.Sleeper)
+ if !ok {
+ return false
+ }
+
+ if w.Dimension() != world.Overworld {
+ w.SetBlock(pos, nil, nil)
+ ExplosionConfig{
+ Size: 5,
+ SpawnFire: true,
+ }.Explode(w, pos.Vec3Centre())
+ return true
+ }
+
+ _, sidePos, ok := b.side(pos, w)
+ if !ok {
+ return false
+ }
+
+ userPos := s.Position()
+ if sidePos.Vec3Middle().Sub(userPos).Len() > 4 && pos.Vec3Middle().Sub(userPos).Len() > 4 {
+ s.Messaget(text.Colourf("%%tile.bed.tooFar"))
+ return true
+ }
+
+ headSide, headPos, ok := b.head(pos, w)
+ if !ok {
+ return false
+ }
+ if _, ok = w.Liquid(headPos); ok {
+ return false
+ }
+
+ w.SetPlayerSpawn(s.UUID(), headPos)
+
+ time := w.Time() % world.TimeFull
+ if (time < world.TimeNight || time >= world.TimeSunrise) && !w.ThunderingAt(pos) {
+ s.Messaget(text.Colourf("%%tile.bed.respawnSet"))
+ s.Messaget(text.Colourf("%%tile.bed.noSleep"))
+ return true
+ }
+ if headSide.User != nil {
+ s.Messaget(text.Colourf("%%tile.bed.respawnSet"))
+ s.Messaget(text.Colourf("%%tile.bed.occupied"))
+ return true
+ }
+
+ s.Sleep(headPos)
+ return true
+}
+
+// EntityLand ...
+func (b Bed) EntityLand(_ cube.Pos, _ *world.World, e world.Entity, distance *float64) {
+ if s, ok := e.(sneakingEntity); ok && s.Sneaking() {
+ // If the entity is sneaking, the fall distance and velocity stay the same.
+ return
+ }
+ if _, ok := e.(fallDistanceEntity); ok {
+ *distance *= 0.5
+ }
+ if v, ok := e.(velocityEntity); ok {
+ vel := v.Velocity()
+ vel[1] = vel[1] * -3 / 4
+ v.SetVelocity(vel)
+ }
+}
+
+// sneakingEntity represents an entity that can sneak.
+type sneakingEntity interface {
+ // Sneaking returns true if the entity is currently sneaking.
+ Sneaking() bool
+}
+
+// velocityEntity represents an entity that can maintain a velocity.
+type velocityEntity interface {
+ // Velocity returns the current velocity of the entity.
+ Velocity() mgl64.Vec3
+ // SetVelocity sets the velocity of the entity.
+ SetVelocity(mgl64.Vec3)
+}
+
+// NeighbourUpdateTick ...
+func (b Bed) NeighbourUpdateTick(pos, _ cube.Pos, w *world.World) {
+ if _, _, ok := b.side(pos, w); !ok {
+ w.SetBlock(pos, nil, nil)
+ }
+}
+
+// EncodeItem ...
+func (b Bed) EncodeItem() (name string, meta int16) {
+ return "minecraft:bed", int16(b.Colour.Uint8())
+}
+
+// EncodeBlock ...
+func (b Bed) EncodeBlock() (name string, properties map[string]interface{}) {
+ return "minecraft:bed", map[string]interface{}{
+ "facing_bit": int32(horizontalDirection(b.Facing)),
+ "occupied_bit": boolByte(b.User != nil),
+ "head_bit": boolByte(b.Head),
+ }
+}
+
+// EncodeNBT ...
+func (b Bed) EncodeNBT() map[string]interface{} {
+ return map[string]interface{}{
+ "id": "Bed",
+ "color": b.Colour.Uint8(),
+ }
+}
+
+// DecodeNBT ...
+func (b Bed) DecodeNBT(data map[string]interface{}) interface{} {
+ b.Colour = item.Colours()[nbtconv.Uint8(data, "color")]
+ return b
+}
+
+// head returns the head side of the bed. If neither side is a head side, the third return value is false.
+func (b Bed) head(pos cube.Pos, w *world.World) (Bed, cube.Pos, bool) {
+ headSide, headPos, ok := b.side(pos, w)
+ if !ok {
+ return Bed{}, cube.Pos{}, false
+ }
+ if b.Head {
+ headSide, headPos = b, pos
+ }
+ return headSide, headPos, true
+}
+
+// side returns the other side of the bed. If the other side is not a bed, the third return value is false.
+func (b Bed) side(pos cube.Pos, w *world.World) (Bed, cube.Pos, bool) {
+ face := b.Facing.Face()
+ if b.Head {
+ face = face.Opposite()
+ }
+
+ sidePos := pos.Side(face)
+ o, ok := w.Block(sidePos).(Bed)
+ return o, sidePos, ok
+}
+
+// allBeds returns all possible beds.
+func allBeds() (beds []world.Block) {
+ for _, d := range cube.Directions() {
+ beds = append(beds, Bed{Facing: d})
+ beds = append(beds, Bed{Facing: d, Head: true})
+ }
+ return
+}
diff --git a/server/block/hash.go b/server/block/hash.go
index 0d1943224..aab9c0019 100644
--- a/server/block/hash.go
+++ b/server/block/hash.go
@@ -13,6 +13,7 @@ const (
hashBarrier
hashBasalt
hashBeacon
+ hashBed
hashBedrock
hashBeetrootSeeds
hashBlackstone
@@ -210,6 +211,10 @@ func (Beacon) Hash() uint64 {
return hashBeacon
}
+func (b Bed) Hash() uint64 {
+ return hashBed | uint64(b.Facing)<<8 | uint64(boolByte(b.Head))<<10
+}
+
func (b Bedrock) Hash() uint64 {
return hashBedrock | uint64(boolByte(b.InfiniteBurning))<<8
}
diff --git a/server/block/model/bed.go b/server/block/model/bed.go
new file mode 100644
index 000000000..f641aa7de
--- /dev/null
+++ b/server/block/model/bed.go
@@ -0,0 +1,18 @@
+package model
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/world"
+)
+
+// Bed is a model used for beds. This model works for both parts of the bed.
+type Bed struct{}
+
+func (b Bed) BBox(cube.Pos, *world.World) []cube.BBox {
+ return []cube.BBox{cube.Box(0, 0, 0, 1, 0.5625, 1)}
+}
+
+// FaceSolid ...
+func (Bed) FaceSolid(cube.Pos, cube.Face, *world.World) bool {
+ return false
+}
diff --git a/server/block/register.go b/server/block/register.go
index 98d6e5f0b..47343b8ae 100644
--- a/server/block/register.go
+++ b/server/block/register.go
@@ -116,6 +116,7 @@ func init() {
registerAll(allBanners())
registerAll(allBarrels())
registerAll(allBasalt())
+ registerAll(allBeds())
registerAll(allBeetroot())
registerAll(allBlackstone())
registerAll(allBlastFurnaces())
@@ -349,6 +350,7 @@ func init() {
}
for _, c := range item.Colours() {
world.RegisterItem(Banner{Colour: c})
+ world.RegisterItem(Bed{Colour: c})
world.RegisterItem(Carpet{Colour: c})
world.RegisterItem(ConcretePowder{Colour: c})
world.RegisterItem(Concrete{Colour: c})
diff --git a/server/player/handler.go b/server/player/handler.go
index 541502287..f928ae3a9 100644
--- a/server/player/handler.go
+++ b/server/player/handler.go
@@ -108,6 +108,8 @@ type Handler interface {
// HandleSignEdit handles the player editing a sign. It is called for every keystroke while editing a sign and
// has both the old text passed and the text after the edit. This typically only has a change of one character.
HandleSignEdit(ctx *event.Context, oldText, newText string)
+ // HandleSleep handles the player going to sleep. ctx.Cancel() may be called to cancel the sleep.
+ HandleSleep(ctx *event.Context, sendReminder *bool)
// HandleItemDamage handles the event wherein the item either held by the player or as armour takes
// damage through usage.
// The type of the item may be checked to determine whether it was armour or a tool used. The damage to
@@ -155,6 +157,7 @@ func (NopHandler) HandleBlockBreak(*event.Context, cube.Pos, *[]item.Stack, *int
func (NopHandler) HandleBlockPlace(*event.Context, cube.Pos, world.Block) {}
func (NopHandler) HandleBlockPick(*event.Context, cube.Pos, world.Block) {}
func (NopHandler) HandleSignEdit(*event.Context, string, string) {}
+func (NopHandler) HandleSleep(*event.Context, *bool) {}
func (NopHandler) HandleItemPickup(*event.Context, item.Stack) {}
func (NopHandler) HandleItemUse(*event.Context) {}
func (NopHandler) HandleItemUseOnBlock(*event.Context, cube.Pos, cube.Face, mgl64.Vec3) {}
diff --git a/server/player/player.go b/server/player/player.go
index 6d588f88c..11c463d03 100644
--- a/server/player/player.go
+++ b/server/player/player.go
@@ -67,6 +67,9 @@ type Player struct {
invisible, immobile, onGround, usingItem atomic.Bool
usingSince atomic.Int64
+ sleeping atomic.Bool
+ sleepPos atomic.Value[cube.Pos]
+
glideTicks atomic.Int64
fireTicks atomic.Int64
fallDistance atomic.Float64
@@ -278,6 +281,11 @@ func (p *Player) Messagef(f string, a ...any) {
p.session().SendMessage(fmt.Sprintf(f, a...))
}
+// Messaget sends a message translation to the player. The message is translated client-side using the client's locale.
+func (p *Player) Messaget(key string, a ...string) {
+ p.session().SendTranslation(key, a...)
+}
+
// SendPopup sends a formatted popup to the player. The popup is shown above the hotbar of the player and
// overwrites/is overwritten by the name of the item equipped.
// The popup is formatted following the rules of fmt.Sprintln without a newline at the end.
@@ -611,6 +619,7 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
} else if _, ok := src.(entity.DrowningDamageSource); ok {
w.PlaySound(pos, sound.Drowning{})
}
+ p.Wake()
p.immunity.Store(time.Now().Add(immunity))
if p.Dead() {
@@ -1110,6 +1119,73 @@ func (p *Player) StopFlying() {
p.session().SendGameMode(p.GameMode())
}
+// Sleep makes the player sleep at the given position. If the position does not map to a bed (specifically the head side),
+// the player will not sleep.
+func (p *Player) Sleep(pos cube.Pos) {
+ ctx, sendReminder := event.C(), true
+ if p.Handler().HandleSleep(ctx, &sendReminder); ctx.Cancelled() {
+ return
+ }
+
+ w := p.World()
+ if b, ok := w.Block(pos).(block.Bed); ok {
+ if b.User != nil {
+ // The player cannot sleep here.
+ return
+ }
+ b.User = p
+ w.SetBlock(pos, b, nil)
+ }
+
+ w.SetRequiredSleepDuration(time.Second * 5)
+ if sendReminder {
+ w.BroadcastSleepingReminder(p)
+ }
+
+ p.pos.Store(pos.Vec3Middle().Add(mgl64.Vec3{0, 0.5625}))
+ p.sleeping.Store(true)
+ p.sleepPos.Store(pos)
+
+ w.BroadcastSleepingIndicator()
+ p.updateState()
+}
+
+// Wake forces the player out of bed if they are sleeping.
+func (p *Player) Wake() {
+ if !p.sleeping.CAS(true, false) {
+ return
+ }
+
+ w := p.World()
+ w.SetRequiredSleepDuration(0)
+ w.BroadcastSleepingIndicator()
+
+ for _, v := range p.viewers() {
+ v.ViewEntityWake(p)
+ }
+ p.updateState()
+
+ pos := p.sleepPos.Load()
+ if b, ok := w.Block(pos).(block.Bed); ok {
+ b.User = nil
+ w.SetBlock(pos, b, nil)
+ }
+}
+
+// Sleeping returns true if the player is currently sleeping, along with the position of the bed the player is sleeping
+// on.
+func (p *Player) Sleeping() (cube.Pos, bool) {
+ if !p.sleeping.Load() {
+ return cube.Pos{}, false
+ }
+ return p.sleepPos.Load(), true
+}
+
+// SendSleepingIndicator displays a notification to the player on the amount of sleeping players in the world.
+func (p *Player) SendSleepingIndicator(sleeping, max int) {
+ p.session().ViewSleepingPlayers(sleeping, max)
+}
+
// Jump makes the player jump if they are on ground. It exhausts the player by 0.05 food points, an additional 0.15
// is exhausted if the player is sprint jumping.
func (p *Player) Jump() {
@@ -1842,12 +1918,12 @@ func (p *Player) BreakBlock(pos cube.Pos) {
p.resendBlocks(pos, w)
return
}
+
held, left := p.HeldItems()
p.SwingArm()
w.SetBlock(pos, nil, nil)
w.AddParticle(pos.Vec3Centre(), particle.BlockBreak{Block: b})
-
if breakable, ok := b.(block.Breakable); ok {
info := breakable.BreakInfo()
if info.BreakHandler != nil {
@@ -1960,6 +2036,8 @@ func (p *Player) Teleport(pos mgl64.Vec3) {
if p.Handler().HandleTeleport(ctx, pos); ctx.Cancelled() {
return
}
+ p.Wake()
+ p.ResetFallDistance()
p.teleport(pos)
}
@@ -2069,8 +2147,9 @@ func (p *Player) Velocity() mgl64.Vec3 {
// SetVelocity updates the player's velocity. If there is an attached session, this will just send
// the velocity to the player session for the player to update.
func (p *Player) SetVelocity(velocity mgl64.Vec3) {
+ p.vel.Store(velocity)
if p.session() == session.Nop {
- p.vel.Store(velocity)
+ // We don't have a session, so we don't need to send the velocity here.
return
}
for _, v := range p.viewers() {
diff --git a/server/player/type.go b/server/player/type.go
index d76edb5c5..b548e3ac7 100644
--- a/server/player/type.go
+++ b/server/player/type.go
@@ -13,9 +13,15 @@ func (Type) NetworkOffset() float64 { return 1.62 }
func (Type) BBox(e world.Entity) cube.BBox {
p := e.(*Player)
s := p.Scale()
- switch {
+
// TODO: Shrink BBox for sneaking once implemented in Bedrock Edition. This is already a thing in Java Edition.
- case p.Gliding(), p.Swimming():
+ gliding := p.Gliding()
+ swimming := p.Swimming()
+ _, sleeping := p.Sleeping()
+ switch {
+ case sleeping:
+ return cube.Box(-0.1*s, 0, -0.1*s, 0.1*s, 0.2*s, 0.1*s)
+ case gliding, swimming:
return cube.Box(-0.3*s, 0, -0.3*s, 0.3*s, 0.6*s, 0.3*s)
default:
return cube.Box(-0.3*s, 0, -0.3*s, 0.3*s, 1.8*s, 0.3*s)
diff --git a/server/session/controllable.go b/server/session/controllable.go
index 1e7a3a7cc..4c431485f 100644
--- a/server/session/controllable.go
+++ b/server/session/controllable.go
@@ -33,6 +33,9 @@ type Controllable interface {
Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64)
Speed() float64
+ Sleep(pos cube.Pos)
+ Wake()
+
Chat(msg ...any)
ExecuteCommand(commandLine string)
GameMode() world.GameMode
diff --git a/server/session/entity_metadata.go b/server/session/entity_metadata.go
index 8920f1623..0ea4246c6 100644
--- a/server/session/entity_metadata.go
+++ b/server/session/entity_metadata.go
@@ -1,6 +1,7 @@
package session
import (
+ "github.com/df-mc/dragonfly/server/block/cube"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/internal/nbtconv"
"github.com/df-mc/dragonfly/server/item"
@@ -95,6 +96,12 @@ func (s *Session) parseEntityMetadata(e world.Entity) protocol.EntityMetadata {
if sc, ok := e.(scoreTag); ok {
m[protocol.EntityDataKeyScore] = sc.ScoreTag()
}
+ if sl, ok := e.(sleeper); ok {
+ if pos, ok := sl.Sleeping(); ok {
+ m[protocol.EntityDataKeyBedPosition] = blockPosToProtocol(pos)
+ m.SetFlag(protocol.EntityDataKeyPlayerFlags, protocol.EntityDataFlagSleeping)
+ }
+ }
if c, ok := e.(areaEffectCloud); ok {
m[protocol.EntityDataKeyDataRadius] = float32(c.Radius())
@@ -257,3 +264,7 @@ type tnt interface {
type living interface {
DeathPosition() (mgl64.Vec3, world.Dimension, bool)
}
+
+type sleeper interface {
+ Sleeping() (cube.Pos, bool)
+}
diff --git a/server/session/handler_player_action.go b/server/session/handler_player_action.go
index 0a4334f6d..55988df65 100644
--- a/server/session/handler_player_action.go
+++ b/server/session/handler_player_action.go
@@ -13,7 +13,6 @@ type PlayerActionHandler struct{}
// Handle ...
func (*PlayerActionHandler) Handle(p packet.Packet, s *Session) error {
pk := p.(*packet.PlayerAction)
-
return handlePlayerAction(pk.ActionType, pk.BlockFace, pk.BlockPosition, pk.EntityRuntimeID, s)
}
@@ -23,7 +22,7 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR
return errSelfRuntimeID
}
switch action {
- case protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone:
+ case protocol.PlayerActionRespawn, protocol.PlayerActionStartSleeping, protocol.PlayerActionDimensionChangeDone:
// Don't do anything for these actions.
case protocol.PlayerActionStopSleeping:
if mode := s.c.GameMode(); !mode.Visible() && !mode.HasCollision() {
@@ -31,6 +30,7 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR
// sleeping in the first place. This accounts for that.
return nil
}
+ s.c.Wake()
case protocol.PlayerActionStartBreak, protocol.PlayerActionContinueDestroyBlock:
s.swingingArm.Store(true)
defer s.swingingArm.Store(false)
diff --git a/server/session/handler_player_auth_input.go b/server/session/handler_player_auth_input.go
index 98d26c047..9652530a0 100644
--- a/server/session/handler_player_auth_input.go
+++ b/server/session/handler_player_auth_input.go
@@ -65,7 +65,7 @@ func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Se
if !mgl64.FloatEqual(deltaPos.Len(), 0) {
s.chunkLoader.Move(newPos)
s.writePacket(&packet.NetworkChunkPublisherUpdate{
- Position: protocol.BlockPos{int32(pk.Position[0]), int32(pk.Position[1]), int32(pk.Position[2])},
+ Position: blockPosToProtocol(cube.PosFromVec3(vec32To64(pk.Position))),
Radius: uint32(s.chunkRadius) << 4,
})
}
diff --git a/server/session/session.go b/server/session/session.go
index 412327f22..da5a2512d 100644
--- a/server/session/session.go
+++ b/server/session/session.go
@@ -183,7 +183,7 @@ func (s *Session) Spawn(c Controllable, pos mgl64.Vec3, w *world.World, gm world
s.chunkLoader = world.NewLoader(int(s.chunkRadius), w, s)
s.chunkLoader.Move(pos)
s.writePacket(&packet.NetworkChunkPublisherUpdate{
- Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
+ Position: blockPosToProtocol(cube.PosFromVec3(pos)),
Radius: uint32(s.chunkRadius) << 4,
})
@@ -251,6 +251,8 @@ func (s *Session) close() {
s.closeCurrentContainer()
_ = s.chunkLoader.Close()
+
+ s.c.Wake()
s.c.World().RemoveEntity(s.c)
// This should always be called last due to the timing of the removal of entity runtime IDs.
diff --git a/server/session/text.go b/server/session/text.go
index 935d81e8e..4c885faa7 100644
--- a/server/session/text.go
+++ b/server/session/text.go
@@ -47,6 +47,16 @@ func (s *Session) SendJukeboxPopup(message string) {
})
}
+// SendTranslation ...
+func (s *Session) SendTranslation(key string, a ...string) {
+ s.writePacket(&packet.Text{
+ TextType: packet.TextTypeTranslation,
+ NeedsTranslation: true,
+ Message: key,
+ Parameters: a,
+ })
+}
+
// SendToast ...
func (s *Session) SendToast(title, message string) {
s.writePacket(&packet.ToastRequest{
diff --git a/server/session/world.go b/server/session/world.go
index d1d8795cd..b3e4ca92b 100644
--- a/server/session/world.go
+++ b/server/session/world.go
@@ -329,6 +329,14 @@ func (s *Session) ViewItemCooldown(item world.Item, duration time.Duration) {
})
}
+// ViewSleepingPlayers ...
+func (s *Session) ViewSleepingPlayers(sleeping, max int) {
+ s.writePacket(&packet.LevelEvent{
+ EventType: packet.LevelEventSleepingPlayers,
+ EventData: int32((max << 16) | sleeping),
+ })
+}
+
// ViewParticle ...
func (s *Session) ViewParticle(pos mgl64.Vec3, p world.Particle) {
switch pa := p.(type) {
@@ -353,7 +361,7 @@ func (s *Session) ViewParticle(pos mgl64.Vec3, p world.Particle) {
s.writePacket(&packet.BlockEvent{
EventType: pa.Instrument.Int32(),
EventData: int32(pa.Pitch),
- Position: protocol.BlockPos{int32(pos.X()), int32(pos.Y()), int32(pos.Z())},
+ Position: blockPosToProtocol(cube.PosFromVec3(pos)),
})
case particle.HugeExplosion:
s.writePacket(&packet.LevelEvent{
@@ -791,7 +799,7 @@ func (s *Session) ViewFurnaceUpdate(prevCookTime, cookTime, prevRemainingFuelTim
// ViewBlockUpdate ...
func (s *Session) ViewBlockUpdate(pos cube.Pos, b world.Block, layer int) {
- blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
+ blockPos := blockPosToProtocol(pos)
s.writePacket(&packet.UpdateBlock{
Position: blockPos,
NewBlockRuntimeID: world.BlockRuntimeID(b),
@@ -939,7 +947,7 @@ func (s *Session) OpenBlockContainer(pos cube.Pos) {
s.writePacket(&packet.ContainerOpen{
WindowID: nextID,
ContainerType: containerType,
- ContainerPosition: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
+ ContainerPosition: blockPosToProtocol(pos),
ContainerEntityUniqueID: -1,
})
}
@@ -966,7 +974,7 @@ func (s *Session) openNormalContainer(b block.Container, pos cube.Pos) {
s.writePacket(&packet.ContainerOpen{
WindowID: nextID,
ContainerType: containerType,
- ContainerPosition: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
+ ContainerPosition: blockPosToProtocol(pos),
ContainerEntityUniqueID: -1,
})
s.sendInv(b.Inventory(), uint32(nextID))
@@ -990,7 +998,7 @@ func (s *Session) ViewSlotChange(slot int, newItem item.Stack) {
// ViewBlockAction ...
func (s *Session) ViewBlockAction(pos cube.Pos, a world.BlockAction) {
- blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
+ blockPos := blockPosToProtocol(pos)
switch t := a.(type) {
case block.OpenAction:
s.writePacket(&packet.BlockEvent{
@@ -1046,7 +1054,7 @@ func (s *Session) ViewSkin(e world.Entity) {
// ViewWorldSpawn ...
func (s *Session) ViewWorldSpawn(pos cube.Pos) {
- blockPos := protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
+ blockPos := blockPosToProtocol(pos)
s.writePacket(&packet.SetSpawnPosition{
SpawnType: packet.SpawnTypeWorld,
Position: blockPos,
@@ -1074,6 +1082,14 @@ func (s *Session) ViewWeather(raining, thunder bool) {
s.writePacket(pk)
}
+// ViewEntityWake ...
+func (s *Session) ViewEntityWake(e world.Entity) {
+ s.writePacket(&packet.Animate{
+ EntityRuntimeID: s.entityRuntimeID(e),
+ ActionType: packet.AnimateActionStopSleep,
+ })
+}
+
// nextWindowID produces the next window ID for a new window. It is an int of 1-99.
func (s *Session) nextWindowID() byte {
if s.openedWindowID.CAS(99, 1) {
@@ -1122,6 +1138,11 @@ func vec64To32(vec3 mgl64.Vec3) mgl32.Vec3 {
return mgl32.Vec3{float32(vec3[0]), float32(vec3[1]), float32(vec3[2])}
}
+// blockPosToProtocol converts a cube.Pos to a protocol.BlockPos.
+func blockPosToProtocol(pos cube.Pos) protocol.BlockPos {
+ return protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
+}
+
// boolByte returns 1 if the bool passed is true, or 0 if it is false.
func boolByte(b bool) uint8 {
if b {
diff --git a/server/world/settings.go b/server/world/settings.go
index e8ea8f4d0..3abeab2c4 100644
--- a/server/world/settings.go
+++ b/server/world/settings.go
@@ -26,6 +26,8 @@ type Settings struct {
Raining bool
// ThunderTime is the current thunder time of the World. It advances every tick if WeatherCycle is set to true.
ThunderTime int64
+ // RequiredSleepTicks is the number of ticks that players must sleep for in order for the time to change to day.
+ RequiredSleepTicks int64
// Thunder is the current thunder level of the World.
Thundering bool
// WeatherCycle specifies if weather should be enabled in this world. If set to false, weather will be disabled.
diff --git a/server/world/sleep.go b/server/world/sleep.go
new file mode 100644
index 000000000..3bdaaa601
--- /dev/null
+++ b/server/world/sleep.go
@@ -0,0 +1,57 @@
+package world
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/google/uuid"
+)
+
+// Sleeper represents an entity that can sleep.
+type Sleeper interface {
+ Entity
+
+ UUID() uuid.UUID
+ Name() string
+
+ Message(a ...any)
+ Messaget(key string, a ...string)
+ SendSleepingIndicator(sleeping, max int)
+
+ Sleep(pos cube.Pos)
+ Sleeping() (cube.Pos, bool)
+ Wake()
+}
+
+// tryAdvanceDay attempts to advance the day of the world, by first ensuring that all sleepers are sleeping, and then
+// updating the time of day.
+func (t ticker) tryAdvanceDay() {
+ sleepers := t.w.Sleepers()
+ if len(sleepers) == 0 {
+ // No sleepers in the world.
+ return
+ }
+
+ var thunderAnywhere bool
+ for _, s := range sleepers {
+ if !thunderAnywhere {
+ thunderAnywhere = t.w.ThunderingAt(cube.PosFromVec3(s.Position()))
+ }
+ if _, ok := s.Sleeping(); !ok {
+ // We can't advance the time - not everyone is sleeping.
+ return
+ }
+ }
+
+ for _, s := range sleepers {
+ s.Wake()
+ }
+
+ totalTime := t.w.Time()
+ time := totalTime % TimeFull
+ if (time < TimeNight || time >= TimeSunrise) && !thunderAnywhere {
+ // The conditions for sleeping aren't being met.
+ return
+ }
+
+ t.w.SetTime(totalTime + TimeFull - time)
+ t.w.StopRaining()
+}
diff --git a/server/world/tick.go b/server/world/tick.go
index fa02ee764..ceff23a63 100644
--- a/server/world/tick.go
+++ b/server/world/tick.go
@@ -52,6 +52,13 @@ func (t ticker) tick() {
}
rain, thunder, tick, tim := t.w.set.Raining, t.w.set.Thundering && t.w.set.Raining, t.w.set.CurrentTick, int(t.w.set.Time)
+ sleep := false
+ if t.w.set.RequiredSleepTicks > 0 {
+ t.w.set.RequiredSleepTicks--
+ if t.w.set.RequiredSleepTicks-1 <= 0 {
+ sleep = true
+ }
+ }
t.w.set.Unlock()
if tick%20 == 0 {
@@ -64,6 +71,9 @@ func (t ticker) tick() {
}
}
}
+ if sleep {
+ t.tryAdvanceDay()
+ }
if thunder {
t.w.tickLightning()
}
diff --git a/server/world/viewer.go b/server/world/viewer.go
index 82bb8ff1e..dfcbe1bfb 100644
--- a/server/world/viewer.go
+++ b/server/world/viewer.go
@@ -66,6 +66,8 @@ type Viewer interface {
ViewWorldSpawn(pos cube.Pos)
// ViewWeather views the weather of the world, including rain and thunder.
ViewWeather(raining, thunder bool)
+ // ViewEntityWake views an entity wake up from a bed.
+ ViewEntityWake(e Entity)
}
// NopViewer is a Viewer implementation that does not implement any behaviour. It may be embedded by other structs to
@@ -95,5 +97,6 @@ func (NopViewer) ViewEmote(Entity, uuid.UUID)
func (NopViewer) ViewSkin(Entity) {}
func (NopViewer) ViewWorldSpawn(cube.Pos) {}
func (NopViewer) ViewWeather(bool, bool) {}
+func (NopViewer) ViewEntityWake(Entity) {}
func (NopViewer) ViewFurnaceUpdate(time.Duration, time.Duration, time.Duration, time.Duration, time.Duration, time.Duration) {
}
diff --git a/server/world/world.go b/server/world/world.go
index 7598750be..65c9f7394 100644
--- a/server/world/world.go
+++ b/server/world/world.go
@@ -66,6 +66,16 @@ type World struct {
viewers map[*Loader]Viewer
}
+const (
+ TimeDay = 1000
+ TimeNoon = 6000
+ TimeSunset = 12000
+ TimeNight = 13000
+ TimeMidnight = 18000
+ TimeSunrise = 23000
+ TimeFull = 24000
+)
+
// New creates a new initialised world. The world may be used right away, but it will not be saved or loaded
// from files until it has been given a different provider than the default. (NopProvider)
// By default, the name of the world will be 'World'.
@@ -733,6 +743,8 @@ func (w *World) RemoveEntity(e Entity) {
viewers := slices.Clone(c.v)
c.Unlock()
+ w.tryAdvanceDay()
+
w.entityMu.Lock()
delete(w.entities, e)
w.entityMu.Unlock()
@@ -791,6 +803,41 @@ func (w *World) Entities() []Entity {
return m
}
+// Sleepers returns a list of all sleeping entities currently added to the World.
+func (w *World) Sleepers() []Sleeper {
+ ent := w.Entities()
+ sleepers := make([]Sleeper, 0, len(ent)/40)
+ for _, e := range ent {
+ if s, ok := e.(Sleeper); ok {
+ sleepers = append(sleepers, s)
+ }
+ }
+ return sleepers
+}
+
+// BroadcastSleepingIndicator broadcasts a sleeping indicator to all sleepers in the world.
+func (w *World) BroadcastSleepingIndicator() {
+ sleepers := w.Sleepers()
+ sleeping := len(sliceutil.Filter(sleepers, func(s Sleeper) bool {
+ _, ok := s.Sleeping()
+ return ok
+ }))
+ for _, s := range sleepers {
+ s.SendSleepingIndicator(sleeping, len(sleepers))
+ }
+}
+
+// BroadcastSleepingReminder broadcasts a sleeping reminder message to all sleepers in the world, excluding the sleeper
+// passed.
+func (w *World) BroadcastSleepingReminder(sleeper Sleeper) {
+ for _, s := range w.Sleepers() {
+ if s == sleeper {
+ continue
+ }
+ s.Messaget("chat.type.sleeping", sleeper.Name())
+ }
+}
+
// OfEntity attempts to return a world that an entity is currently in. If the entity was not currently added
// to a world, the world returned is nil and the bool returned is false.
func OfEntity(e Entity) (*World, bool) {
@@ -858,6 +905,17 @@ func (w *World) SetPlayerSpawn(uuid uuid.UUID, pos cube.Pos) {
}
}
+// SetRequiredSleepDuration sets the duration of time players in the world must sleep for, in order to advance to the
+// next day.
+func (w *World) SetRequiredSleepDuration(duration time.Duration) {
+ if w == nil {
+ return
+ }
+ w.set.Lock()
+ defer w.set.Unlock()
+ w.set.RequiredSleepTicks = duration.Milliseconds() / 50
+}
+
// DefaultGameMode returns the default game mode of the world. When players join, they are given this game
// mode.
// The default game mode may be changed using SetDefaultGameMode().