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().