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