diff --git a/server/block/bed.go b/server/block/bed.go
new file mode 100644
index 000000000..3a690bd80
--- /dev/null
+++ b/server/block/bed.go
@@ -0,0 +1,256 @@
+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/player/chat"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/go-gl/mathgl/mgl64"
+)
+
+// 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
+ // Sleeper is the user that is using the bed. It is only set for the Head part of the bed.
+ Sleeper *world.EntityHandle
+}
+
+// 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.Tx) bool {
+ return false
+}
+
+// BreakInfo ...
+func (b Bed) BreakInfo() BreakInfo {
+ return newBreakInfo(0.2, alwaysHarvestable, nothingEffective, oneOf(b)).withBreakHandler(func(pos cube.Pos, tx *world.Tx, _ item.User) {
+ headSide, _, ok := b.head(pos, tx)
+ if !ok {
+ return
+ }
+
+ sleeper := headSide.Sleeper
+ if sleeper != nil {
+
+ ent, ok := sleeper.Entity(tx)
+ if ok {
+ sleeper, ok := ent.(world.Sleeper)
+ if ok {
+ sleeper.Wake()
+ }
+ }
+ }
+ })
+}
+
+// UseOnBlock ...
+func (b Bed) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, tx *world.Tx, user item.User, ctx *item.UseContext) (used bool) {
+ if pos, _, used = firstReplaceable(tx, pos, face, b); !used {
+ return
+ }
+ if !supportedFromBelow(pos, tx) {
+ return
+ }
+
+ b.Facing = user.Rotation().Direction()
+
+ side, sidePos := b, pos.Side(b.Facing.Face())
+ side.Head = true
+
+ if !replaceableWith(tx, sidePos, side) {
+ return
+ }
+
+ if !supportedFromBelow(sidePos, tx) {
+ return
+ }
+
+ ctx.IgnoreBBox = true
+ place(tx, sidePos, side, user, ctx)
+ place(tx, pos, b, user, ctx)
+ return placed(ctx)
+}
+
+// Activate ...
+func (b Bed) Activate(pos cube.Pos, _ cube.Face, tx *world.Tx, u item.User, _ *item.UseContext) bool {
+ s, ok := u.(world.Sleeper)
+ if !ok {
+ return false
+ }
+
+ w := tx.World()
+
+ if w.Dimension() != world.Overworld {
+ tx.SetBlock(pos, nil, nil)
+ ExplosionConfig{
+ Size: 5,
+ SpawnFire: true,
+ }.Explode(tx, pos.Vec3Centre())
+ return true
+ }
+
+ _, sidePos, ok := b.side(pos, tx)
+ if !ok {
+ return false
+ }
+
+ userPos := s.Position()
+ if sidePos.Vec3Middle().Sub(userPos).Len() > 4 && pos.Vec3Middle().Sub(userPos).Len() > 4 {
+ s.Messaget(chat.MessageBedTooFar)
+ return true
+ }
+
+ headSide, headPos, ok := b.head(pos, tx)
+ if !ok {
+ return false
+ }
+ if _, ok = tx.Liquid(headPos); ok {
+ return false
+ }
+
+ previousSpawn := w.PlayerSpawn(s.UUID())
+ if previousSpawn != pos && previousSpawn != sidePos {
+ w.SetPlayerSpawn(s.UUID(), pos)
+ s.Messaget(chat.MessageRespawnPointSet)
+ }
+
+ time := w.Time() % world.TimeFull
+ if (time < world.TimeNight || time >= world.TimeSunrise) && !tx.ThunderingAt(pos) {
+ s.Messaget(chat.MessageNoSleep)
+ return true
+ }
+ if headSide.Sleeper != nil {
+ s.Messaget(chat.MessageBedIsOccupied)
+ return true
+ }
+
+ s.Sleep(headPos)
+ return true
+}
+
+// EntityLand ...
+func (b Bed) EntityLand(_ cube.Pos, _ *world.Tx, e world.Entity, distance *float64) {
+ if _, ok := e.(fallDistanceEntity); ok {
+ *distance *= 0.5
+ }
+ if v, ok := e.(velocityEntity); ok {
+ vel := v.Velocity()
+ vel[1] = vel[1] * -2 / 3
+ v.SetVelocity(vel)
+ }
+}
+
+// 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, tx *world.Tx) {
+ if _, _, ok := b.side(pos, tx); !ok {
+ breakBlockNoDrops(b, pos, tx)
+ }
+}
+
+// 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{}{
+ "direction": int32(horizontalDirection(b.Facing)),
+ "occupied_bit": boolByte(b.Sleeper != 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, tx *world.Tx) (Bed, cube.Pos, bool) {
+ headSide, headPos, ok := b.side(pos, tx)
+ if !ok {
+ return Bed{}, cube.Pos{}, false
+ }
+ if b.Head {
+ return b, pos, true
+ }
+ 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, tx *world.Tx) (Bed, cube.Pos, bool) {
+ face := b.Facing.Face()
+ if b.Head {
+ face = face.Opposite()
+ }
+
+ sidePos := pos.Side(face)
+ o, ok := tx.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
+}
+
+func (Bed) CanRespawnOn() bool {
+ return true
+}
+
+func (Bed) RespawnOn(pos cube.Pos, u item.User, tx *world.Tx) {}
+
+// RespawnBlock represents a block using which player can set his spawn point.
+type RespawnBlock interface {
+ // CanRespawnOn defines if player can use this block to respawn.
+ CanRespawnOn() bool
+ // RespawnOn is called when a player decides to respawn using this block.
+ RespawnOn(pos cube.Pos, u item.User, tx *world.Tx)
+}
+
+// supportedFromBelow ...
+func supportedFromBelow(pos cube.Pos, tx *world.Tx) bool {
+ below := pos.Side(cube.FaceDown)
+ return tx.Block(below).Model().FaceSolid(below, cube.FaceUp, tx)
+}
diff --git a/server/block/hash.go b/server/block/hash.go
index 1e47df7ce..49f84c4b3 100644
--- a/server/block/hash.go
+++ b/server/block/hash.go
@@ -15,6 +15,7 @@ const (
hashBarrier
hashBasalt
hashBeacon
+ hashBed
hashBedrock
hashBeetrootSeeds
hashBlackstone
@@ -149,6 +150,7 @@ const (
hashReinforcedDeepslate
hashResin
hashResinBricks
+ hashRespawnAnchor
hashSand
hashSandstone
hashSeaLantern
@@ -240,6 +242,10 @@ func (Beacon) Hash() (uint64, uint64) {
return hashBeacon, 0
}
+func (b Bed) Hash() (uint64, uint64) {
+ return hashBed, uint64(b.Facing) | uint64(boolByte(b.Head))<<2
+}
+
func (b Bedrock) Hash() (uint64, uint64) {
return hashBedrock, uint64(boolByte(b.InfiniteBurning))
}
@@ -776,6 +782,10 @@ func (r ResinBricks) Hash() (uint64, uint64) {
return hashResinBricks, uint64(boolByte(r.Chiseled))
}
+func (r RespawnAnchor) Hash() (uint64, uint64) {
+ return hashRespawnAnchor, uint64(r.Charge)
+}
+
func (s Sand) Hash() (uint64, uint64) {
return hashSand, uint64(boolByte(s.Red))
}
diff --git a/server/block/model/bed.go b/server/block/model/bed.go
new file mode 100644
index 000000000..e7179a35f
--- /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.BlockSource) []cube.BBox {
+ return []cube.BBox{cube.Box(0, 0, 0, 1, 0.5625, 1)}
+}
+
+// FaceSolid ...
+func (Bed) FaceSolid(cube.Pos, cube.Face, world.BlockSource) bool {
+ return false
+}
diff --git a/server/block/register.go b/server/block/register.go
index 48ac9788a..1192424f1 100644
--- a/server/block/register.go
+++ b/server/block/register.go
@@ -126,6 +126,7 @@ func init() {
registerAll(allBanners())
registerAll(allBarrels())
registerAll(allBasalt())
+ registerAll(allBeds())
registerAll(allBeetroot())
registerAll(allBlackstone())
registerAll(allBlastFurnaces())
@@ -142,8 +143,13 @@ func init() {
registerAll(allComposters())
registerAll(allConcrete())
registerAll(allConcretePowder())
+ registerAll(allCopper())
+ registerAll(allCopperDoors())
+ registerAll(allCopperGrates())
+ registerAll(allCopperTrapdoors())
registerAll(allCoral())
registerAll(allCoralBlocks())
+ registerAll(allDecoratedPots())
registerAll(allDeepslate())
registerAll(allDoors())
registerAll(allDoubleFlowers())
@@ -184,6 +190,7 @@ func init() {
registerAll(allPumpkins())
registerAll(allPurpurs())
registerAll(allQuartz())
+ registerAll(allRespawnAnchors())
registerAll(allSandstones())
registerAll(allSeaPickles())
registerAll(allSigns())
@@ -205,11 +212,6 @@ func init() {
registerAll(allWheat())
registerAll(allWood())
registerAll(allWool())
- registerAll(allDecoratedPots())
- registerAll(allCopper())
- registerAll(allCopperDoors())
- registerAll(allCopperGrates())
- registerAll(allCopperTrapdoors())
}
func init() {
@@ -334,6 +336,7 @@ func init() {
world.RegisterItem(ResinBricks{Chiseled: true})
world.RegisterItem(ResinBricks{})
world.RegisterItem(Resin{})
+ world.RegisterItem(RespawnAnchor{})
world.RegisterItem(Sand{Red: true})
world.RegisterItem(Sand{})
world.RegisterItem(SeaLantern{})
@@ -388,6 +391,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/block/respawn_anchor.go b/server/block/respawn_anchor.go
new file mode 100644
index 000000000..0058fc06d
--- /dev/null
+++ b/server/block/respawn_anchor.go
@@ -0,0 +1,98 @@
+package block
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/item"
+ "github.com/df-mc/dragonfly/server/player/chat"
+ "github.com/df-mc/dragonfly/server/world"
+ "github.com/df-mc/dragonfly/server/world/sound"
+)
+
+// RespawnAnchor is a block that allows the player to set their spawn point in the Nether.
+type RespawnAnchor struct {
+ solid
+ bassDrum
+
+ // Charge is the Glowstone charge of the RespawnAnchor.
+ Charge int
+}
+
+// LightEmissionLevel ...
+func (r RespawnAnchor) LightEmissionLevel() uint8 {
+ return uint8(max(0, 3+4*(r.Charge-1)))
+}
+
+// EncodeItem ...
+func (r RespawnAnchor) EncodeItem() (name string, meta int16) {
+ return "minecraft:respawn_anchor", 0
+}
+
+// EncodeBlock ...
+func (r RespawnAnchor) EncodeBlock() (string, map[string]any) {
+ return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": int32(r.Charge)}
+}
+
+// BreakInfo ...
+func (r RespawnAnchor) BreakInfo() BreakInfo {
+ return newBreakInfo(50, func(t item.Tool) bool {
+ return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierDiamond.HarvestLevel
+ }, pickaxeEffective, oneOf(r)).withBlastResistance(6000)
+}
+
+// Activate ...
+func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, tx *world.Tx, u item.User, ctx *item.UseContext) bool {
+ held, _ := u.HeldItems()
+ _, usingGlowstone := held.Item().(Glowstone)
+
+ sleeper, ok := u.(world.Sleeper)
+ if !ok {
+ return false
+ }
+
+ if r.Charge < 4 && usingGlowstone {
+ r.Charge++
+ tx.SetBlock(pos, r, nil)
+ ctx.SubtractFromCount(1)
+ tx.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{Charge: r.Charge})
+ return true
+ }
+
+ w := tx.World()
+
+ if r.Charge > 0 {
+ if w.Dimension() == world.Nether {
+ previousSpawn := w.PlayerSpawn(sleeper.UUID())
+ if previousSpawn == pos {
+ return false
+ }
+ sleeper.Messaget(chat.MessageRespawnPointSet)
+ w.SetPlayerSpawn(sleeper.UUID(), pos)
+ return true
+ }
+ tx.SetBlock(pos, nil, nil)
+ ExplosionConfig{
+ Size: 5,
+ SpawnFire: true,
+ }.Explode(tx, pos.Vec3Centre())
+ }
+
+ return false
+}
+
+// allRespawnAnchors returns all possible respawn anchors.
+func allRespawnAnchors() []world.Block {
+ all := make([]world.Block, 0, 5)
+ for i := 0; i < 5; i++ {
+ all = append(all, RespawnAnchor{Charge: i})
+ }
+ return all
+}
+
+func (r RespawnAnchor) CanRespawnOn() bool {
+ return r.Charge > 0
+}
+
+func (r RespawnAnchor) RespawnOn(pos cube.Pos, u item.User, w *world.Tx) {
+ w.SetBlock(pos, RespawnAnchor{Charge: r.Charge - 1}, nil)
+ w.PlaySound(pos.Vec3(), sound.RespawnAnchorDeplete{Charge: r.Charge - 1})
+}
diff --git a/server/item/item.go b/server/item/item.go
index 8a2cb4587..2401204f3 100644
--- a/server/item/item.go
+++ b/server/item/item.go
@@ -13,7 +13,7 @@ import (
// MaxCounter represents an item that has a specific max count. By default, each item will be expected to have
// a maximum count of 64. MaxCounter may be implemented to change this behaviour.
type MaxCounter interface {
- // MaxCount returns the maximum number of items that a stack may be composed of. The number returned must
+ // MaxCount returns the maximum number of items a stack may be composed of. The number returned must
// be positive.
MaxCount() int
}
diff --git a/server/player/chat/translate.go b/server/player/chat/translate.go
index 5ecb66ee1..dc240f73e 100644
--- a/server/player/chat/translate.go
+++ b/server/player/chat/translate.go
@@ -12,6 +12,14 @@ var MessageJoin = Translate(str("%multiplayer.player.joined"), 1, `%v joined the
var MessageQuit = Translate(str("%multiplayer.player.left"), 1, `%v left the game`).Enc("%v")
var MessageServerDisconnect = Translate(str("%disconnect.disconnected"), 0, `Disconnected by Server`).Enc("%v")
+var MessageBedTooFar = Translate(str("%tile.bed.tooFar"), 0, `Bed is too far away`).Enc("%v")
+var MessageRespawnPointSet = Translate(str("%tile.bed.respawnSet"), 0, `Respawn point set`).Enc("%v")
+var MessageNoSleep = Translate(str("%tile.bed.noSleep"), 0, `You can only sleep at night and during thunderstorms`).Enc("%v")
+var MessageBedIsOccupied = Translate(str("%tile.bed.occupied"), 0, `This bed is occupied`).Enc("%v")
+var MessageSleeping = Translate(str("%chat.type.sleeping"), 2, `%v is sleeping in a bed. To skip to dawn, %v more users need to sleep in beds at the same time.`)
+var MessageRespawnAnchorNotValid = Translate(str("%tile.respawn_anchor.notValid"), 0, `Your respawn anchor was out of charges, missing or obstructed`).Enc("%v")
+var MessageBedNotValid = Translate(str("%tile.bed.notValid"), 0, `Your home bed was missing or obstructed`)
+
type str string
// Resolve returns the translation identifier as a string.
diff --git a/server/player/handler.go b/server/player/handler.go
index 6631c6cc1..9eaea3eaa 100644
--- a/server/player/handler.go
+++ b/server/player/handler.go
@@ -121,6 +121,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 *Context, pos cube.Pos, frontSide bool, oldText, newText string)
+ // HandleSleep handles the player starting to sleep. ctx.Cancel() may be called to cancel the sleep.
+ HandleSleep(ctx *Context, sendReminder *bool)
// HandleLecternPageTurn handles the player turning a page in a lectern. ctx.Cancel() may be called to cancel the
// page turn. The page number may be changed by assigning to *page.
HandleLecternPageTurn(ctx *Context, pos cube.Pos, oldPage int, newPage *int)
@@ -179,6 +181,7 @@ func (NopHandler) HandleBlockBreak(*Context, cube.Pos, *[]item.Stack, *int)
func (NopHandler) HandleBlockPlace(*Context, cube.Pos, world.Block) {}
func (NopHandler) HandleBlockPick(*Context, cube.Pos, world.Block) {}
func (NopHandler) HandleSignEdit(*Context, cube.Pos, bool, string, string) {}
+func (NopHandler) HandleSleep(*Context, *bool) {}
func (NopHandler) HandleLecternPageTurn(*Context, cube.Pos, int, *int) {}
func (NopHandler) HandleItemPickup(*Context, *item.Stack) {}
func (NopHandler) HandleItemUse(*Context) {}
diff --git a/server/player/player.go b/server/player/player.go
index da23c7c7e..7e9139e61 100644
--- a/server/player/player.go
+++ b/server/player/player.go
@@ -54,8 +54,11 @@ type playerData struct {
sneaking, sprinting, swimming, gliding, crawling, flying,
invisible, immobile, onGround, usingItem bool
- usingSince time.Time
+ sleeping bool
+ sleepPos cube.Pos
+
+ usingSince time.Time
glideTicks int64
fireTicks int64
fallDistance float64
@@ -640,6 +643,8 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) {
p.tx.PlaySound(pos, sound.Drowning{})
}
+ p.Wake()
+
if p.Dead() {
p.kill(src)
}
@@ -881,7 +886,14 @@ func finishDying(_ *world.Tx, e world.Entity) {
// position server side so that in the future, the client won't respawn
// on the death location when disconnecting. The client should not see
// the movement itself yet, though.
- p.data.Pos = p.tx.World().Spawn().Vec3()
+
+ pos, w, spawnObstructed, _ := p.spawnLocation()
+
+ if spawnObstructed {
+ w.SetPlayerSpawn(p.UUID(), pos)
+ }
+
+ p.data.Pos = pos.Vec3()
}
}
@@ -929,13 +941,21 @@ func (p *Player) Respawn() *world.EntityHandle {
}
func (p *Player) respawn(f func(p *Player)) {
+ position, w, spawnObstructed, previousDimension := p.spawnLocation()
+ if spawnObstructed {
+ switch previousDimension {
+ case world.Nether:
+ p.Messaget(chat.MessageRespawnAnchorNotValid)
+ case world.Overworld:
+ p.Messaget(chat.MessageBedNotValid)
+ }
+ }
+
+ pos := position.Vec3Middle()
+
if !p.Dead() || p.session() == session.Nop {
return
}
- // We can use the principle here that returning through a portal of a specific dimension inside that dimension will
- // always bring us back to the overworld.
- w := p.tx.World().PortalDestination(p.tx.World().Dimension())
- pos := w.PlayerSpawn(p.UUID()).Vec3Middle()
p.addHealth(p.MaxHealth())
p.hunger.Reset()
@@ -948,6 +968,9 @@ func (p *Player) respawn(f func(p *Player)) {
handle := p.tx.RemoveEntity(p)
w.Exec(func(tx *world.Tx) {
np := tx.AddEntity(handle).(*Player)
+ if bl, ok := tx.Block(position).(block.RespawnBlock); ok {
+ bl.RespawnOn(position, p, tx)
+ }
np.Teleport(pos)
np.session().SendRespawn(pos, p)
np.SetVisible()
@@ -957,6 +980,23 @@ func (p *Player) respawn(f func(p *Player)) {
})
}
+// spawnLocation returns position and world where player should be spawned.
+func (p *Player) spawnLocation() (playerSpawn cube.Pos, w *world.World, spawnBlockBroken bool, previousDimension world.Dimension) {
+ tx := p.tx
+ w = tx.World()
+ previousDimension = w.Dimension()
+ playerSpawn = w.PlayerSpawn(p.UUID())
+ if b, ok := tx.Block(playerSpawn).(block.RespawnBlock); ok && b.CanRespawnOn() {
+ return playerSpawn, w, false, previousDimension
+ }
+
+ // We can use the principle here that returning through a portal of a specific dimension inside that dimension will
+ // always bring us back to the overworld.
+ w = w.PortalDestination(w.Dimension())
+ worldSpawn := w.Spawn()
+ return worldSpawn, w, playerSpawn != worldSpawn, previousDimension
+}
+
// StartSprinting makes a player start sprinting, increasing the speed of the player by 30% and making
// particles show up under the feet. The player will only start sprinting if its food level is high enough.
// If the player is sneaking when calling StartSprinting, it is stopped from sneaking.
@@ -1167,6 +1207,81 @@ func (p *Player) Jump() {
}
}
+// 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) {
+
+ if p.sleeping {
+ // The player is already sleeping.
+ return
+ }
+
+ tx := p.tx
+ b, ok := tx.Block(pos).(block.Bed)
+
+ if !ok || b.Sleeper != nil {
+ // The player cannot sleep here.
+ return
+ }
+
+ ctx, sendReminder := event.C(p), true
+ if p.Handler().HandleSleep(ctx, &sendReminder); ctx.Cancelled() {
+ return
+ }
+
+ b.Sleeper = p.H()
+ tx.SetBlock(pos, b, nil)
+
+ tx.World().SetRequiredSleepDuration(time.Second * 5)
+
+ p.data.Pos = pos.Vec3Middle().Add(mgl64.Vec3{0, 0.5625})
+ p.sleeping = true
+ p.sleepPos = pos
+
+ if sendReminder {
+ tx.BroadcastSleepingReminder(p)
+ }
+
+ tx.BroadcastSleepingIndicator()
+ p.updateState()
+}
+
+// Wake forces the player out of bed if they are sleeping.
+func (p *Player) Wake() {
+ if !p.sleeping {
+ return
+ }
+ p.sleeping = false
+
+ tx := p.tx
+ tx.BroadcastSleepingIndicator()
+
+ for _, v := range p.viewers() {
+ v.ViewEntityWake(p)
+ }
+ p.updateState()
+
+ pos := p.sleepPos
+ if b, ok := tx.Block(pos).(block.Bed); ok {
+ b.Sleeper = nil
+ tx.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 {
+ return cube.Pos{}, false
+ }
+ return p.sleepPos, 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)
+}
+
// SetInvisible sets the player invisible, so that other players will not be able to see it.
func (p *Player) SetInvisible() {
if p.Invisible() {
@@ -2050,6 +2165,7 @@ func (p *Player) Teleport(pos mgl64.Vec3) {
if p.Handler().HandleTeleport(ctx, pos); ctx.Cancelled() {
return
}
+ p.Wake()
p.teleport(pos)
}
diff --git a/server/player/type.go b/server/player/type.go
index 42d177743..11f0d1e01 100644
--- a/server/player/type.go
+++ b/server/player/type.go
@@ -39,7 +39,10 @@ func (ptype) NetworkOffset() float64 { return 1.621 }
func (ptype) BBox(e world.Entity) cube.BBox {
p := e.(*Player)
s := p.Scale()
+ _, 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 p.Gliding(), p.Swimming(), p.Crawling():
return cube.Box(-0.3*s, 0, -0.3*s, 0.3*s, 0.6*s, 0.3*s)
case p.Sneaking():
diff --git a/server/session/controllable.go b/server/session/controllable.go
index bb155c04b..65e462d03 100644
--- a/server/session/controllable.go
+++ b/server/session/controllable.go
@@ -39,6 +39,9 @@ type Controllable interface {
FlightSpeed() float64
VerticalFlightSpeed() 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 d39981d5e..e7a6fd3dd 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"
"github.com/df-mc/dragonfly/server/entity/effect"
"github.com/df-mc/dragonfly/server/internal/nbtconv"
@@ -114,6 +115,14 @@ func (s *Session) addSpecificMetadata(e any, m 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] = protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
+
+ // For some reason there is no such flag in gophertunnel.
+ m.SetFlag(protocol.EntityDataKeyPlayerFlags, 1)
+ }
+ }
if c, ok := e.(areaEffectCloud); ok {
m[protocol.EntityDataKeyDataRadius] = float32(c.Radius())
@@ -263,6 +272,10 @@ type gameMode interface {
GameMode() world.GameMode
}
+type sleeper interface {
+ Sleeping() (cube.Pos, bool)
+}
+
type tnt interface {
Fuse() time.Duration
}
diff --git a/server/session/handler_player_action.go b/server/session/handler_player_action.go
index 1f6d980de..2a0c25b6c 100644
--- a/server/session/handler_player_action.go
+++ b/server/session/handler_player_action.go
@@ -24,14 +24,10 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR
return errSelfRuntimeID
}
switch action {
- case protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone:
- // Don't do anything for these actions.
+ case protocol.PlayerActionStartSleeping, protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone:
+ // Don't do anything for these actions.
case protocol.PlayerActionStopSleeping:
- if mode := c.GameMode(); !mode.Visible() && !mode.HasCollision() {
- // As of v1.19.50, the client sends this packet when switching to spectator mode... even if it wasn't
- // sleeping in the first place. This accounts for that.
- return nil
- }
+ c.Wake()
case protocol.PlayerActionStartBreak, protocol.PlayerActionContinueDestroyBlock:
s.swingingArm.Store(true)
defer s.swingingArm.Store(false)
diff --git a/server/session/session.go b/server/session/session.go
index 53a5a870e..e87b905bc 100644
--- a/server/session/session.go
+++ b/server/session/session.go
@@ -252,6 +252,8 @@ func (s *Session) close(tx *world.Tx, c Controllable) {
s.chunkLoader.Close(tx)
+ c.Wake()
+
if !s.conf.QuitMessage.Zero() {
chat.Global.Writet(s.conf.QuitMessage, s.conn.IdentityData().DisplayName)
}
diff --git a/server/session/world.go b/server/session/world.go
index 5183ce3dc..b10fffc47 100644
--- a/server/session/world.go
+++ b/server/session/world.go
@@ -324,6 +324,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) {
@@ -810,6 +818,15 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool)
pk.SoundType = packet.SoundEventComposterReady
case sound.LecternBookPlace:
pk.SoundType = packet.SoundEventLecternBookPlace
+ case sound.RespawnAnchorAmbient:
+ pk.SoundType = packet.SoundEventRespawnAnchorAmbient
+ pk.ExtraData = int32(so.Charge)
+ case sound.RespawnAnchorCharge:
+ pk.SoundType = packet.SoundEventRespawnAnchorCharge
+ pk.ExtraData = int32(so.Charge)
+ case sound.RespawnAnchorDeplete:
+ pk.SoundType = packet.SoundEventRespawnAnchorDeplete
+ pk.ExtraData = int32(so.Charge)
case sound.Totem:
s.writePacket(&packet.LevelEvent{
EventType: packet.LevelEventSoundTotemUsed,
@@ -1228,6 +1245,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.CompareAndSwap(99, 1) {
diff --git a/server/world/settings.go b/server/world/settings.go
index bd029c773..c895db623 100644
--- a/server/world/settings.go
+++ b/server/world/settings.go
@@ -30,6 +30,8 @@ type Settings struct {
Thundering bool
// WeatherCycle specifies if weather should be enabled in this world. If set to false, weather will be disabled.
WeatherCycle bool
+ // RequiredSleepTicks is the number of ticks that players must sleep for in order for the time to change to day.
+ RequiredSleepTicks int64
// CurrentTick is the current tick of the world. This is similar to the Time, except that it has no visible effect
// to the client. It can also not be changed through commands and will only ever go up.
CurrentTick int64
diff --git a/server/world/sleep.go b/server/world/sleep.go
new file mode 100644
index 000000000..154228be0
--- /dev/null
+++ b/server/world/sleep.go
@@ -0,0 +1,55 @@
+package world
+
+import (
+ "github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/player/chat"
+ "github.com/google/uuid"
+)
+
+// Sleeper represents an entity that can sleep.
+type Sleeper interface {
+ Entity
+
+ Name() string
+ UUID() uuid.UUID
+
+ Messaget(t chat.Translation, a ...any)
+ 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 (ticker) tryAdvanceDay(tx *Tx, timeCycle bool) {
+ sleepers := tx.Sleepers()
+
+ var thunderAnywhere bool
+ for s := range sleepers {
+ if !thunderAnywhere {
+ thunderAnywhere = tx.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 := tx.w.Time()
+ time := totalTime % TimeFull
+ if (time < TimeNight || time >= TimeSunrise) && !thunderAnywhere {
+ // The conditions for sleeping aren't being met.
+ return
+ }
+
+ if timeCycle {
+ tx.w.SetTime(totalTime + TimeFull - time)
+ }
+ tx.w.StopRaining()
+}
diff --git a/server/world/sound/block.go b/server/world/sound/block.go
index e87c2b7ab..ac3c922c4 100644
--- a/server/world/sound/block.go
+++ b/server/world/sound/block.go
@@ -188,6 +188,24 @@ type PotionBrewed struct{ sound }
// LecternBookPlace is a sound played when a book is placed in a lectern.
type LecternBookPlace struct{ sound }
+// RespawnAnchorCharge is a sound played when a respawn anchor has been charged.
+type RespawnAnchorCharge struct {
+ sound
+ Charge int
+}
+
+// RespawnAnchorDeplete is a sound played when someone respawns using respawn anchor.
+type RespawnAnchorDeplete struct {
+ sound
+ Charge int
+}
+
+// RespawnAnchorAmbient is an ambient sound of respawn anchor.
+type RespawnAnchorAmbient struct {
+ sound
+ Charge int
+}
+
// SignWaxed is a sound played when a sign is waxed.
type SignWaxed struct{ sound }
diff --git a/server/world/tick.go b/server/world/tick.go
index 9c025d807..fc3570a0f 100644
--- a/server/world/tick.go
+++ b/server/world/tick.go
@@ -60,8 +60,21 @@ func (t ticker) tick(tx *Tx) {
}
rain, thunder, tick, tim := w.set.Raining, w.set.Thundering && w.set.Raining, w.set.CurrentTick, int(w.set.Time)
+
+ timeCycle := tx.w.set.TimeCycle
+
+ tryAdvanceDay := false
+ if tx.w.set.RequiredSleepTicks > 0 {
+ tx.w.set.RequiredSleepTicks--
+ tryAdvanceDay = tx.w.set.RequiredSleepTicks <= 0
+ }
+
w.set.Unlock()
+ if tryAdvanceDay {
+ t.tryAdvanceDay(tx, timeCycle)
+ }
+
if tick%20 == 0 {
for _, viewer := range viewers {
if w.Dimension().TimeCycle() {
@@ -72,6 +85,7 @@ func (t ticker) tick(tx *Tx) {
}
}
}
+
if thunder {
w.tickLightning(tx)
}
diff --git a/server/world/tx.go b/server/world/tx.go
index 1422bfa1f..57457480f 100644
--- a/server/world/tx.go
+++ b/server/world/tx.go
@@ -2,6 +2,7 @@ package world
import (
"github.com/df-mc/dragonfly/server/block/cube"
+ "github.com/df-mc/dragonfly/server/player/chat"
"github.com/go-gl/mathgl/mgl64"
"iter"
"sync"
@@ -215,6 +216,51 @@ func (tx *Tx) Viewers(pos mgl64.Vec3) []Viewer {
return tx.World().viewersOf(pos)
}
+// Sleepers returns an iterator that yields all sleeping entities currently added to the World.
+func (tx *Tx) Sleepers() iter.Seq[Sleeper] {
+ ent := tx.Entities()
+ return func(yield func(Sleeper) bool) {
+ for e := range ent {
+ if sleeper, ok := e.(Sleeper); ok {
+ if !yield(sleeper) {
+ return
+ }
+ }
+ }
+ }
+}
+
+// BroadcastSleepingIndicator broadcasts a sleeping indicator to all sleepers in the world.
+func (tx *Tx) BroadcastSleepingIndicator() {
+ sleepers := tx.Sleepers()
+
+ sleeping, allSleepers := 0, 0
+
+ for s := range sleepers {
+ allSleepers++
+ if _, ok := s.Sleeping(); ok {
+ sleeping++
+ }
+ }
+
+ for s := range sleepers {
+ s.SendSleepingIndicator(sleeping, allSleepers)
+ }
+}
+
+// BroadcastSleepingReminder broadcasts a sleeping reminder message to all sleepers in the world, excluding the sleeper
+// passed.
+func (tx *Tx) BroadcastSleepingReminder(sleeper Sleeper) {
+ notSleeping := new(int)
+
+ for s := range tx.Sleepers() {
+ if _, ok := s.Sleeping(); !ok {
+ *notSleeping++
+ defer s.Messaget(chat.MessageSleeping, sleeper.Name(), *notSleeping)
+ }
+ }
+}
+
// World returns the World of the Tx. It panics if the transaction was already
// marked complete.
func (tx *Tx) World() *World {
diff --git a/server/world/viewer.go b/server/world/viewer.go
index 7ffbbbebf..57f05385c 100644
--- a/server/world/viewer.go
+++ b/server/world/viewer.go
@@ -70,6 +70,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 waking 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
@@ -101,5 +103,6 @@ func (NopViewer) ViewSkin(Entity)
func (NopViewer) ViewWorldSpawn(cube.Pos) {}
func (NopViewer) ViewWeather(bool, bool) {}
func (NopViewer) ViewBrewingUpdate(time.Duration, time.Duration, int32, int32, int32, int32) {}
+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 403d3c5ee..a3180033f 100644
--- a/server/world/world.go
+++ b/server/world/world.go
@@ -67,6 +67,16 @@ type World struct {
viewers map[*Loader]Viewer
}
+const (
+ TimeDay = 1000
+ TimeNoon = 6000
+ TimeSunset = 12000
+ TimeNight = 13000
+ TimeMidnight = 18000
+ TimeSunrise = 23000
+ TimeFull = 24000
+)
+
// transaction is a type that may be added to the transaction queue of a World.
// Its Run method is called when the transaction is taken out of the queue.
type transaction interface {
@@ -810,6 +820,17 @@ func (w *World) SetPlayerSpawn(id 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().