From 88f1a8fe9acb23231040e442ed9c7ad31eefc024 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 22:02:09 +0100 Subject: [PATCH 01/11] implement beds , respawn anchors --- server/block/bed.go | 247 ++++++++++++++++++++++++ server/block/hash.go | 2 + server/block/model/bed.go | 18 ++ server/block/register.go | 6 + server/block/respawn_anchor.go | 104 ++++++++++ server/item/item.go | 5 + server/player/handler.go | 3 + server/player/player.go | 165 ++++++++++++++-- server/player/type.go | 3 + server/server.go | 2 +- server/session/controllable.go | 3 + server/session/entity_metadata.go | 13 ++ server/session/handler_player_action.go | 5 +- server/session/session.go | 2 + server/session/text.go | 10 + server/session/world.go | 25 +++ server/world/settings.go | 2 + server/world/sleep.go | 57 ++++++ server/world/sound/block.go | 18 ++ server/world/tick.go | 10 + server/world/viewer.go | 3 + server/world/world.go | 67 +++++++ 22 files changed, 746 insertions(+), 24 deletions(-) create mode 100644 server/block/bed.go create mode 100644 server/block/model/bed.go create mode 100644 server/block/respawn_anchor.go create mode 100644 server/world/sleep.go diff --git a/server/block/bed.go b/server/block/bed.go new file mode 100644 index 000000000..108b8f485 --- /dev/null +++ b/server/block/bed.go @@ -0,0 +1,247 @@ +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 +} + +// Hash ... +func (b Bed) Hash() (uint64, uint64) { + return hashBed, uint64(b.Facing) | uint64(boolByte(b.Head))<<2 +} + +// 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 + } + + u.SetSpawnPos(pos, w) + s.Messaget(text.Colourf("%%tile.bed.respawnSet")) + time := w.Time() % world.TimeFull + if (time < world.TimeNight || time >= world.TimeSunrise) && !w.ThunderingAt(pos) { + s.Messaget(text.Colourf("%%tile.bed.noSleep")) + return true + } + if headSide.User != nil { + 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 +} + +func (Bed) SpawnBlock() bool { + return true +} + +func (Bed) Update(pos cube.Pos, u item.User, w *world.World) {} + +// SpawnBlock represents a block using which player can set his spawn point. +type SpawnBlock interface { + SpawnBlock() bool + Update(pos cube.Pos, u item.User, w *world.World) +} diff --git a/server/block/hash.go b/server/block/hash.go index 098786c88..690a2feff 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -15,6 +15,7 @@ const ( hashBarrier hashBasalt hashBeacon + hashBed hashBedrock hashBeetrootSeeds hashBlackstone @@ -144,6 +145,7 @@ const ( hashRawGold hashRawIron hashReinforcedDeepslate + hashRespawnAnchor hashSand hashSandstone hashSeaLantern 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 1ce675a6c..f9d629925 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -122,6 +122,7 @@ func init() { registerAll(allBanners()) registerAll(allBarrels()) registerAll(allBasalt()) + registerAll(allBeds()) registerAll(allBeetroot()) registerAll(allBlackstone()) registerAll(allBlastFurnaces()) @@ -203,6 +204,7 @@ func init() { registerAll(allCopperDoors()) registerAll(allCopperGrates()) registerAll(allCopperTrapdoors()) + registerAll(allRespawnAnchors()) } func init() { @@ -374,6 +376,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}) @@ -461,6 +464,9 @@ func init() { world.RegisterItem(Copper{Type: c, Oxidation: o, Waxed: true}) } } + for _, t := range allRespawnAnchorsItems() { + world.RegisterItem(t) + } } func registerAll(blocks []world.Block) { diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go new file mode 100644 index 000000000..28263a97e --- /dev/null +++ b/server/block/respawn_anchor.go @@ -0,0 +1,104 @@ +package block + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/item" + "github.com/df-mc/dragonfly/server/world" + "github.com/df-mc/dragonfly/server/world/sound" + "github.com/sandertv/gophertunnel/minecraft/text" +) + +// RespawnAnchor is a block , allows players to set there respawn point in the nether. +type RespawnAnchor struct { + solid + bassDrum + charge int32 +} + +// Hash ... +func (r RespawnAnchor) Hash() (uint64, uint64) { + return hashRespawnAnchor, uint64(r.charge) +} + +// LightEmissionLevel ... +func (r RespawnAnchor) LightEmissionLevel() uint8 { + return (uint8(r.charge) + 1) * 3 +} + +// EncodeItem ... +func (r RespawnAnchor) EncodeItem() (name string, meta int16) { + return "minecraft:respawn_anchor", int16(r.charge) +} + +// EncodeBlock ... +func (r RespawnAnchor) EncodeBlock() (string, map[string]any) { + return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": r.charge} +} + +// BreakInfo ... +func (r RespawnAnchor) BreakInfo() BreakInfo { + return newBreakInfo(35, 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, w *world.World, u item.User, ctx *item.UseContext) bool { + + var nether bool = w.Dimension().WaterEvaporates() + + held, _ := u.HeldItems() + _, usingGlowStone := held.Item().(Glowstone) + + if r.charge < 4 && usingGlowStone { + r.charge++ + w.SetBlock(pos, r, nil) + ctx.SubtractFromCount(1) + w.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{Charge: r.charge}) + return true + } + if nether { + if r.charge > 0 { + u.Messaget(text.Colourf("%%tile.bed.respawnSet")) + u.SetSpawnPos(pos, w) + } + return false + } + + if r.charge > 0 { + w.SetBlock(pos, nil, nil) + ExplosionConfig{ + Size: 5, + SpawnFire: true, + }.Explode(w, pos.Vec3Centre()) + } + + return false +} + +// allRespawnAnchors returns all possible respawn anchors. +func allRespawnAnchors() []world.Block { + all := make([]world.Block, 0, 5) + for i := int32(0); i < 5; i++ { + all = append(all, RespawnAnchor{charge: i}) + } + return all +} + +// allRespawnAnchorsItems returns all possible respawn anchors as items. +func allRespawnAnchorsItems() []world.Item { + all := make([]world.Item, 0, 5) + for i := int32(0); i < 5; i++ { + all = append(all, RespawnAnchor{charge: i}) + } + return all +} + +func (r RespawnAnchor) SpawnBlock() bool { + return r.charge > 0 +} + +func (r RespawnAnchor) Update(pos cube.Pos, u item.User, w *world.World) { + 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 6370b8483..d6041d137 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -166,6 +166,11 @@ type User interface { UsingItem() bool ReleaseItem() UseItem() + + SetSpawnPos(pos cube.Pos, w *world.World) + + Messaget(key string, a ...string) + Sneaking() bool } // Carrier represents an entity that is able to carry an item. diff --git a/server/player/handler.go b/server/player/handler.go index 981bfc9f6..0b7e08408 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -112,6 +112,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, pos cube.Pos, frontSide bool, 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) // 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 *event.Context, pos cube.Pos, oldPage int, newPage *int) @@ -167,6 +169,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, cube.Pos, bool, string, string) {} +func (NopHandler) HandleSleep(*event.Context, *bool) {} func (NopHandler) HandleLecternPageTurn(*event.Context, cube.Pos, int, *int) {} func (NopHandler) HandleItemPickup(*event.Context, *item.Stack) {} func (NopHandler) HandleItemUse(*event.Context) {} diff --git a/server/player/player.go b/server/player/player.go index 69d0ec591..d9ca46fa3 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.Pointer[cube.Pos] + glideTicks atomic.Int64 fireTicks atomic.Int64 fallDistance atomic.Uint64 @@ -107,12 +110,14 @@ type Player struct { breakParticleCounter atomic.Uint32 hunger *hungerManager + + dimensionProvider DimensionProvider } // New returns a new initialised player. A random UUID is generated for the player, so that it may be // identified over network. You can either pass on player data you want to load or // you can leave the data as nil to use default data. -func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { +func New(name string, skin skin.Skin, pos mgl64.Vec3, dp DimensionProvider) *Player { p := &Player{} *p = Player{ inv: inventory.New(36, func(slot int, before, after item.Stack) { @@ -120,20 +125,21 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { p.broadcastItems(slot, before, after) } }), - enderChest: inventory.New(27, nil), - uuid: uuid.New(), - offHand: inventory.New(1, p.broadcastItems), - armour: inventory.NewArmour(p.broadcastArmour), - hunger: newHungerManager(), - health: entity.NewHealthManager(20, 20), - experience: entity.NewExperienceManager(), - effects: entity.NewEffectManager(), - name: name, - locale: language.BritishEnglish, - breathing: true, - cooldowns: make(map[string]time.Time), - mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, - heldSlot: &atomic.Uint32{}, + enderChest: inventory.New(27, nil), + uuid: uuid.New(), + offHand: inventory.New(1, p.broadcastItems), + armour: inventory.NewArmour(p.broadcastArmour), + hunger: newHungerManager(), + health: entity.NewHealthManager(20, 20), + experience: entity.NewExperienceManager(), + effects: entity.NewEffectManager(), + name: name, + locale: language.BritishEnglish, + breathing: true, + cooldowns: make(map[string]time.Time), + mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, + heldSlot: &atomic.Uint32{}, + dimensionProvider: dp, } var scoreTag string var heldSlot uint32 @@ -161,8 +167,8 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { // A set of additional fields must be provided to initialise the player with the client's data, such as the // name and the skin of the player. You can either pass on player data you want to load or // you can leave the data as nil to use default data. -func NewWithSession(name, xuid string, uuid uuid.UUID, skin skin.Skin, s *session.Session, pos mgl64.Vec3, data *Data) *Player { - p := New(name, skin, pos) +func NewWithSession(name, xuid string, uuid uuid.UUID, skin skin.Skin, s *session.Session, pos mgl64.Vec3, data *Data, dp DimensionProvider) *Player { + p := New(name, skin, pos, dp) p.s.Store(s) p.skin.Store(&skin) p.uuid, p.xuid = uuid, xuid @@ -289,6 +295,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. @@ -665,6 +676,8 @@ func (p *Player) Hurt(dmg float64, src world.DamageSource) (float64, bool) { w.PlaySound(pos, sound.Drowning{}) } + p.Wake() + p.SetAttackImmunity(immunity) if p.Dead() { p.kill(src) @@ -910,8 +923,14 @@ func (p *Player) kill(src world.DamageSource) { // We have an actual client connected to this player: We change its 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. - newPos := w.Spawn().Vec3() - p.pos.Store(&newPos) + pos, w, blockHasBeenBroken := p.realSpawnPos() + if blockHasBeenBroken { + //bl := w.Block(pos).(block.SpawnBlock) + //bl.Update(pos, p, w) + p.SetSpawnPos(w.Spawn(), w) + } + vec := pos.Vec3() + p.pos.Store(&vec) } }) } @@ -940,7 +959,14 @@ func (p *Player) dropContents() { // Respawn spawns the player after it dies, so that its health is replenished and it is spawned in the world // again. Nothing will happen if the player does not have a session connected to it. func (p *Player) Respawn() { - w := p.World() + position, w, ok := p.realSpawnPos() + if bl, ok := w.Block(position).(block.SpawnBlock); ok { + bl.Update(position, p, w) + } + pos := position.Vec3Middle() + if ok { + p.Messaget("%tile.bed.notValid") + } if !p.Dead() || w == nil || p.session() == session.Nop { return } @@ -953,7 +979,6 @@ func (p *Player) Respawn() { // 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()) - pos := w.PlayerSpawn(p.UUID()).Vec3Middle() p.Handler().HandleRespawn(&pos, &w) @@ -964,6 +989,26 @@ func (p *Player) Respawn() { p.SetVisible() } +// SpawnPos returns spawn position in the current world. +func (p *Player) SpawnPos() cube.Pos { + return p.World().PlayerSpawn(p.uuid) +} + +// SetSpawnPos sets spawn position for the player in the provided world. +func (p *Player) SetSpawnPos(pos cube.Pos, w *world.World) { + w.SetPlayerSpawn(p.UUID(), pos) +} + +// realSpawnPos returns position and world where player should be spawned. +func (p *Player) realSpawnPos() (cube.Pos, *world.World, bool) { + pos := p.SpawnPos() + w := p.World() + if b, ok := w.Block(pos).(block.SpawnBlock); ok && b.SpawnBlock() { + return pos, w, false + } + return p.dimensionProvider.World().Spawn(), p.dimensionProvider.World(), true +} + // 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. @@ -1160,6 +1205,75 @@ 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) { + 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) + } + + vec := pos.Vec3Middle().Add(mgl64.Vec3{0, 0.5625}) + + p.pos.Store(&vec) + 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.CompareAndSwap(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) +} + // SetInvisible sets the player invisible, so that other players will not be able to see it. func (p *Player) SetInvisible() { if !p.invisible.CompareAndSwap(false, true) { @@ -1990,6 +2104,7 @@ func (p *Player) Teleport(pos mgl64.Vec3) { if p.Handler().HandleTeleport(ctx, pos); ctx.Cancelled() { return } + p.Wake() p.teleport(pos) } @@ -3109,3 +3224,11 @@ func (p *Player) resendBlock(pos cube.Pos, w *world.World) { func format(a []any) string { return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n") } + +// DimensionProvider provides access to all 3 dimensions. +type DimensionProvider interface { + World() *world.World + Nether() *world.World + End() *world.World + Players() []*Player +} diff --git a/server/player/type.go b/server/player/type.go index b3174384d..30426346a 100644 --- a/server/player/type.go +++ b/server/player/type.go @@ -13,7 +13,10 @@ func (Type) NetworkOffset() float64 { return 1.621 } func (Type) 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/server.go b/server/server.go index 1801e27ad..71622b8d0 100644 --- a/server/server.go +++ b/server/server.go @@ -455,7 +455,7 @@ func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, data *player.Da w, gm, pos = data.World, data.GameMode, data.Position } s := session.New(conn, srv.conf.MaxChunkRadius, srv.conf.Log, srv.conf.JoinMessage, srv.conf.QuitMessage) - p := player.NewWithSession(conn.IdentityData().DisplayName, conn.IdentityData().XUID, id, srv.parseSkin(conn.ClientData()), s, pos, data) + p := player.NewWithSession(conn.IdentityData().DisplayName, conn.IdentityData().XUID, id, srv.parseSkin(conn.ClientData()), s, pos, data, srv) s.Spawn(p, pos, w, gm, srv.handleSessionClose) srv.pwg.Add(1) diff --git a/server/session/controllable.go b/server/session/controllable.go index 7919218d3..bb8caf019 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -36,6 +36,9 @@ type Controllable interface { Speed() float64 FlightSpeed() 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 4aee51c59..57ac7acab 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" @@ -113,6 +114,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()) @@ -261,6 +270,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 5dfc30bc7..8bcff3fa3 100644 --- a/server/session/handler_player_action.go +++ b/server/session/handler_player_action.go @@ -23,14 +23,15 @@ 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.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone, protocol.PlayerActionStartSleeping: + // Don't do anything for these actions. case protocol.PlayerActionStopSleeping: if mode := s.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 } + s.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 8937804fb..c91d9c4f9 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -254,6 +254,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 240957f9b..66f43f648 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 cc6188569..bd3cc6b77 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -334,6 +334,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) { @@ -793,6 +801,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 = so.Charge + case sound.RespawnAnchorCharge: + pk.SoundType = packet.SoundEventRespawnAnchorCharge + pk.ExtraData = so.Charge + case sound.RespawnAnchorDeplete: + pk.SoundType = packet.SoundEventRespawnAnchorDeplete + pk.ExtraData = so.Charge case sound.Totem: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundTotemUsed, @@ -1186,6 +1203,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..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/sound/block.go b/server/world/sound/block.go index d9a35c73d..70ae7f9a0 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 { + Charge int32 + sound +} + +// RespawnAnchorDeplete is a sound played when someone respawns using respawn anchor. +type RespawnAnchorDeplete struct { + Charge int32 + sound +} + +// RespawnAnchorAmbient is an ambient sound of respawn anchor. +type RespawnAnchorAmbient struct { + Charge int32 + sound +} + // 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 b2b163aae..ce2d930fc 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 eee356db7..85c07bc24 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 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 @@ -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 fce62e3a7..3541c677c 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -69,6 +69,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'. @@ -806,6 +816,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) { @@ -873,6 +918,28 @@ 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 +} + +// SetSleepRequirement sets the duration of time players in the world must sleep for, in order for the time to change to +// day. +func (w *World) SetSleepRequirement(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(). From 41e283149098a66cffbeed3bca78ee2768961ce2 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 22:14:48 +0100 Subject: [PATCH 02/11] run go generate --- server/block/hash.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/block/hash.go b/server/block/hash.go index 690a2feff..065cf8917 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -236,6 +236,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)) } @@ -752,6 +756,10 @@ func (ReinforcedDeepslate) Hash() (uint64, uint64) { return hashReinforcedDeepslate, 0 } +func (RespawnAnchor) Hash() (uint64, uint64) { + return hashRespawnAnchor, 0 +} + func (s Sand) Hash() (uint64, uint64) { return hashSand, uint64(boolByte(s.Red)) } From dea2dfd3f5e194fbee056df551550d4f97e264f4 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 22:17:24 +0100 Subject: [PATCH 03/11] remove hash methods in the files --- server/block/bed.go | 5 ----- server/block/respawn_anchor.go | 5 ----- 2 files changed, 10 deletions(-) diff --git a/server/block/bed.go b/server/block/bed.go index 108b8f485..59a9454b6 100644 --- a/server/block/bed.go +++ b/server/block/bed.go @@ -25,11 +25,6 @@ type Bed struct { User item.User } -// Hash ... -func (b Bed) Hash() (uint64, uint64) { - return hashBed, uint64(b.Facing) | uint64(boolByte(b.Head))<<2 -} - // MaxCount always returns 1. func (Bed) MaxCount() int { return 1 diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go index 28263a97e..36c42b424 100644 --- a/server/block/respawn_anchor.go +++ b/server/block/respawn_anchor.go @@ -15,11 +15,6 @@ type RespawnAnchor struct { charge int32 } -// Hash ... -func (r RespawnAnchor) Hash() (uint64, uint64) { - return hashRespawnAnchor, uint64(r.charge) -} - // LightEmissionLevel ... func (r RespawnAnchor) LightEmissionLevel() uint8 { return (uint8(r.charge) + 1) * 3 From 198ddfc9868ddd7f2a651eec7841b3197f751bf1 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 22:27:33 +0100 Subject: [PATCH 04/11] fix github Staticcheck --- server/block/respawn_anchor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go index 36c42b424..3f06ce507 100644 --- a/server/block/respawn_anchor.go +++ b/server/block/respawn_anchor.go @@ -40,7 +40,7 @@ func (r RespawnAnchor) BreakInfo() BreakInfo { // Activate ... func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.World, u item.User, ctx *item.UseContext) bool { - var nether bool = w.Dimension().WaterEvaporates() + var nether = w.Dimension().WaterEvaporates() held, _ := u.HeldItems() _, usingGlowStone := held.Item().(Glowstone) From 0f9d7a28c70ff1c60dc53e40f9d71212a5a69472 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 22:32:20 +0100 Subject: [PATCH 05/11] remove useless comments --- server/player/player.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/player/player.go b/server/player/player.go index d9ca46fa3..ee1045d9f 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -920,13 +920,12 @@ func (p *Player) kill(src world.DamageSource) { } if p.Dead() { p.SetInvisible() + // We have an actual client connected to this player: We change its 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. pos, w, blockHasBeenBroken := p.realSpawnPos() if blockHasBeenBroken { - //bl := w.Block(pos).(block.SpawnBlock) - //bl.Update(pos, p, w) p.SetSpawnPos(w.Spawn(), w) } vec := pos.Vec3() From 20f6e1c211718b94290e70732ab82dbaee871be9 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 23:29:59 +0100 Subject: [PATCH 06/11] some fixes --- server/block/respawn_anchor.go | 36 +++++++++++++++++----------------- server/player/player.go | 7 +------ server/world/sound/block.go | 6 +++--- server/world/tick.go | 2 +- 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go index 3f06ce507..9ad7e1cca 100644 --- a/server/block/respawn_anchor.go +++ b/server/block/respawn_anchor.go @@ -12,27 +12,27 @@ import ( type RespawnAnchor struct { solid bassDrum - charge int32 + Charge int } // LightEmissionLevel ... func (r RespawnAnchor) LightEmissionLevel() uint8 { - return (uint8(r.charge) + 1) * 3 + return uint8(max(0, 3+4*(r.Charge-1))) } // EncodeItem ... func (r RespawnAnchor) EncodeItem() (name string, meta int16) { - return "minecraft:respawn_anchor", int16(r.charge) + return "minecraft:respawn_anchor", int16(r.Charge) } // EncodeBlock ... func (r RespawnAnchor) EncodeBlock() (string, map[string]any) { - return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": r.charge} + return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": r.Charge} } // BreakInfo ... func (r RespawnAnchor) BreakInfo() BreakInfo { - return newBreakInfo(35, func(t item.Tool) bool { + return newBreakInfo(50, func(t item.Tool) bool { return t.ToolType() == item.TypePickaxe && t.HarvestLevel() >= item.ToolTierDiamond.HarvestLevel }, pickaxeEffective, oneOf(r)).withBlastResistance(6000) } @@ -43,24 +43,24 @@ func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.Wo var nether = w.Dimension().WaterEvaporates() held, _ := u.HeldItems() - _, usingGlowStone := held.Item().(Glowstone) + _, usingGlowstone := held.Item().(Glowstone) - if r.charge < 4 && usingGlowStone { - r.charge++ + if r.Charge < 4 && usingGlowstone { + r.Charge++ w.SetBlock(pos, r, nil) ctx.SubtractFromCount(1) - w.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{Charge: r.charge}) + w.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{Charge: r.Charge}) return true } if nether { - if r.charge > 0 { + if r.Charge > 0 { u.Messaget(text.Colourf("%%tile.bed.respawnSet")) u.SetSpawnPos(pos, w) } return false } - if r.charge > 0 { + if r.Charge > 0 { w.SetBlock(pos, nil, nil) ExplosionConfig{ Size: 5, @@ -74,8 +74,8 @@ func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.Wo // allRespawnAnchors returns all possible respawn anchors. func allRespawnAnchors() []world.Block { all := make([]world.Block, 0, 5) - for i := int32(0); i < 5; i++ { - all = append(all, RespawnAnchor{charge: i}) + for i := 0; i < 5; i++ { + all = append(all, RespawnAnchor{Charge: i}) } return all } @@ -83,17 +83,17 @@ func allRespawnAnchors() []world.Block { // allRespawnAnchorsItems returns all possible respawn anchors as items. func allRespawnAnchorsItems() []world.Item { all := make([]world.Item, 0, 5) - for i := int32(0); i < 5; i++ { - all = append(all, RespawnAnchor{charge: i}) + for i := 0; i < 5; i++ { + all = append(all, RespawnAnchor{Charge: i}) } return all } func (r RespawnAnchor) SpawnBlock() bool { - return r.charge > 0 + return r.Charge > 0 } func (r RespawnAnchor) Update(pos cube.Pos, u item.User, w *world.World) { - w.SetBlock(pos, RespawnAnchor{charge: r.charge - 1}, nil) - w.PlaySound(pos.Vec3(), sound.RespawnAnchorDeplete{Charge: r.charge - 1}) + w.SetBlock(pos, RespawnAnchor{Charge: r.Charge - 1}, nil) + w.PlaySound(pos.Vec3(), sound.RespawnAnchorDeplete{Charge: r.Charge - 1}) } diff --git a/server/player/player.go b/server/player/player.go index ee1045d9f..99f463e38 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -988,11 +988,6 @@ func (p *Player) Respawn() { p.SetVisible() } -// SpawnPos returns spawn position in the current world. -func (p *Player) SpawnPos() cube.Pos { - return p.World().PlayerSpawn(p.uuid) -} - // SetSpawnPos sets spawn position for the player in the provided world. func (p *Player) SetSpawnPos(pos cube.Pos, w *world.World) { w.SetPlayerSpawn(p.UUID(), pos) @@ -1000,7 +995,7 @@ func (p *Player) SetSpawnPos(pos cube.Pos, w *world.World) { // realSpawnPos returns position and world where player should be spawned. func (p *Player) realSpawnPos() (cube.Pos, *world.World, bool) { - pos := p.SpawnPos() + pos := p.World().PlayerSpawn(p.uuid) w := p.World() if b, ok := w.Block(pos).(block.SpawnBlock); ok && b.SpawnBlock() { return pos, w, false diff --git a/server/world/sound/block.go b/server/world/sound/block.go index 70ae7f9a0..10856f997 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -190,20 +190,20 @@ type LecternBookPlace struct{ sound } // RespawnAnchorCharge is a sound played when a respawn anchor has been charged. type RespawnAnchorCharge struct { - Charge int32 sound + Charge int } // RespawnAnchorDeplete is a sound played when someone respawns using respawn anchor. type RespawnAnchorDeplete struct { - Charge int32 sound + Charge int } // RespawnAnchorAmbient is an ambient sound of respawn anchor. type RespawnAnchorAmbient struct { - Charge int32 sound + Charge int } // SignWaxed is a sound played when a sign is waxed. diff --git a/server/world/tick.go b/server/world/tick.go index ce2d930fc..684733971 100644 --- a/server/world/tick.go +++ b/server/world/tick.go @@ -55,7 +55,7 @@ func (t ticker) tick() { sleep := false if t.w.set.RequiredSleepTicks > 0 { t.w.set.RequiredSleepTicks-- - if t.w.set.RequiredSleepTicks-1 <= 0 { + if t.w.set.RequiredSleepTicks <= 0 { sleep = true } } From 2c62bdb087cac93119c9b4c5052e3fa9c58ae773 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 23:31:49 +0100 Subject: [PATCH 07/11] fix int to int32 casting --- server/session/world.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/session/world.go b/server/session/world.go index bd3cc6b77..b373ffb64 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -803,13 +803,13 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventLecternBookPlace case sound.RespawnAnchorAmbient: pk.SoundType = packet.SoundEventRespawnAnchorAmbient - pk.ExtraData = so.Charge + pk.ExtraData = int32(so.Charge) case sound.RespawnAnchorCharge: pk.SoundType = packet.SoundEventRespawnAnchorCharge - pk.ExtraData = so.Charge + pk.ExtraData = int32(so.Charge) case sound.RespawnAnchorDeplete: pk.SoundType = packet.SoundEventRespawnAnchorDeplete - pk.ExtraData = so.Charge + pk.ExtraData = int32(so.Charge) case sound.Totem: s.writePacket(&packet.LevelEvent{ EventType: packet.LevelEventSoundTotemUsed, From ffc6f69ea8e679764e33051147d7491160920257 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Sun, 17 Nov 2024 23:41:43 +0100 Subject: [PATCH 08/11] requested changes --- server/block/bed.go | 2 +- server/block/respawn_anchor.go | 2 +- server/item/item.go | 3 ++- server/player/player.go | 12 +++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/server/block/bed.go b/server/block/bed.go index 59a9454b6..d589a532a 100644 --- a/server/block/bed.go +++ b/server/block/bed.go @@ -115,7 +115,7 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ return false } - u.SetSpawnPos(pos, w) + w.SetPlayerSpawn(u.UUID(), pos) s.Messaget(text.Colourf("%%tile.bed.respawnSet")) time := w.Time() % world.TimeFull if (time < world.TimeNight || time >= world.TimeSunrise) && !w.ThunderingAt(pos) { diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go index 9ad7e1cca..d10b4ee86 100644 --- a/server/block/respawn_anchor.go +++ b/server/block/respawn_anchor.go @@ -55,7 +55,7 @@ func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.Wo if nether { if r.Charge > 0 { u.Messaget(text.Colourf("%%tile.bed.respawnSet")) - u.SetSpawnPos(pos, w) + w.SetPlayerSpawn(u.UUID(), pos) } return false } diff --git a/server/item/item.go b/server/item/item.go index d6041d137..0c0e87ef1 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -7,6 +7,7 @@ import ( "github.com/df-mc/dragonfly/server/internal/lang" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" + "github.com/google/uuid" "golang.org/x/text/language" "image/color" "time" @@ -167,7 +168,7 @@ type User interface { ReleaseItem() UseItem() - SetSpawnPos(pos cube.Pos, w *world.World) + UUID() uuid.UUID Messaget(key string, a ...string) Sneaking() bool diff --git a/server/player/player.go b/server/player/player.go index 99f463e38..84784cbae 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -925,10 +925,13 @@ func (p *Player) kill(src world.DamageSource) { // the future, the client won't respawn on the death location when disconnecting. The client should // not see the movement itself yet, though. pos, w, blockHasBeenBroken := p.realSpawnPos() + vec := pos.Vec3() + if blockHasBeenBroken { - p.SetSpawnPos(w.Spawn(), w) + p.Handler().HandleRespawn(&vec, &w) + w.SetPlayerSpawn(p.UUID(), w.Spawn()) } - vec := pos.Vec3() + p.pos.Store(&vec) } }) @@ -988,11 +991,6 @@ func (p *Player) Respawn() { p.SetVisible() } -// SetSpawnPos sets spawn position for the player in the provided world. -func (p *Player) SetSpawnPos(pos cube.Pos, w *world.World) { - w.SetPlayerSpawn(p.UUID(), pos) -} - // realSpawnPos returns position and world where player should be spawned. func (p *Player) realSpawnPos() (cube.Pos, *world.World, bool) { pos := p.World().PlayerSpawn(p.uuid) From 2ba7934b50a1cd82141f2990e23b4fd6911f38a6 Mon Sep 17 00:00:00 2001 From: FDUTCH Date: Mon, 18 Nov 2024 17:33:37 +0100 Subject: [PATCH 09/11] requested changes --- server/block/bed.go | 22 +++++---- server/block/hash.go | 4 +- server/block/respawn_anchor.go | 17 +++---- server/player/player.go | 87 ++++++++++++++++------------------ server/server.go | 2 +- server/world/sleep.go | 3 -- server/world/viewer.go | 2 +- 7 files changed, 68 insertions(+), 69 deletions(-) diff --git a/server/block/bed.go b/server/block/bed.go index d589a532a..5f57a05d3 100644 --- a/server/block/bed.go +++ b/server/block/bed.go @@ -115,8 +115,12 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ return false } - w.SetPlayerSpawn(u.UUID(), pos) - s.Messaget(text.Colourf("%%tile.bed.respawnSet")) + previousSpawn := w.PlayerSpawn(u.UUID()) + if previousSpawn != pos { + w.SetPlayerSpawn(u.UUID(), pos) + s.Messaget(text.Colourf("%%tile.bed.respawnSet")) + } + time := w.Time() % world.TimeFull if (time < world.TimeNight || time >= world.TimeSunrise) && !w.ThunderingAt(pos) { s.Messaget(text.Colourf("%%tile.bed.noSleep")) @@ -229,14 +233,16 @@ func allBeds() (beds []world.Block) { return } -func (Bed) SpawnBlock() bool { +func (Bed) CanSpawn() bool { return true } -func (Bed) Update(pos cube.Pos, u item.User, w *world.World) {} +func (Bed) SpawnOn(pos cube.Pos, u item.User, w *world.World) {} -// SpawnBlock represents a block using which player can set his spawn point. -type SpawnBlock interface { - SpawnBlock() bool - Update(pos cube.Pos, u item.User, w *world.World) +// RespawnBlock represents a block using which player can set his spawn point. +type RespawnBlock interface { + // CanSpawn defines if player can use this block to respawn. + CanSpawn() bool + // SpawnOn is called when a player decides to respawn using this block. + SpawnOn(pos cube.Pos, u item.User, w *world.World) } diff --git a/server/block/hash.go b/server/block/hash.go index 065cf8917..b2ba7704e 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -756,8 +756,8 @@ func (ReinforcedDeepslate) Hash() (uint64, uint64) { return hashReinforcedDeepslate, 0 } -func (RespawnAnchor) Hash() (uint64, uint64) { - return hashRespawnAnchor, 0 +func (r RespawnAnchor) Hash() (uint64, uint64) { + return hashRespawnAnchor, uint64(r.Charge) } func (s Sand) Hash() (uint64, uint64) { diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go index d10b4ee86..9a5463144 100644 --- a/server/block/respawn_anchor.go +++ b/server/block/respawn_anchor.go @@ -8,7 +8,7 @@ import ( "github.com/sandertv/gophertunnel/minecraft/text" ) -// RespawnAnchor is a block , allows players to set there respawn point in the nether. +// RespawnAnchor is a block that allows the player to set their spawn point in the Nether. type RespawnAnchor struct { solid bassDrum @@ -27,7 +27,7 @@ func (r RespawnAnchor) EncodeItem() (name string, meta int16) { // EncodeBlock ... func (r RespawnAnchor) EncodeBlock() (string, map[string]any) { - return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": r.Charge} + return "minecraft:respawn_anchor", map[string]any{"respawn_anchor_charge": int32(r.Charge)} } // BreakInfo ... @@ -39,9 +39,6 @@ func (r RespawnAnchor) BreakInfo() BreakInfo { // Activate ... func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.World, u item.User, ctx *item.UseContext) bool { - - var nether = w.Dimension().WaterEvaporates() - held, _ := u.HeldItems() _, usingGlowstone := held.Item().(Glowstone) @@ -52,8 +49,12 @@ func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.Wo w.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{Charge: r.Charge}) return true } - if nether { + if w.Dimension() == world.Nether { if r.Charge > 0 { + previousSpawn := w.PlayerSpawn(u.UUID()) + if previousSpawn == pos { + return false + } u.Messaget(text.Colourf("%%tile.bed.respawnSet")) w.SetPlayerSpawn(u.UUID(), pos) } @@ -89,11 +90,11 @@ func allRespawnAnchorsItems() []world.Item { return all } -func (r RespawnAnchor) SpawnBlock() bool { +func (r RespawnAnchor) CanSpawn() bool { return r.Charge > 0 } -func (r RespawnAnchor) Update(pos cube.Pos, u item.User, w *world.World) { +func (r RespawnAnchor) SpawnOn(pos cube.Pos, u item.User, w *world.World) { w.SetBlock(pos, RespawnAnchor{Charge: r.Charge - 1}, nil) w.PlaySound(pos.Vec3(), sound.RespawnAnchorDeplete{Charge: r.Charge - 1}) } diff --git a/server/player/player.go b/server/player/player.go index 84784cbae..813825274 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -110,14 +110,12 @@ type Player struct { breakParticleCounter atomic.Uint32 hunger *hungerManager - - dimensionProvider DimensionProvider } // New returns a new initialised player. A random UUID is generated for the player, so that it may be // identified over network. You can either pass on player data you want to load or // you can leave the data as nil to use default data. -func New(name string, skin skin.Skin, pos mgl64.Vec3, dp DimensionProvider) *Player { +func New(name string, skin skin.Skin, pos mgl64.Vec3) *Player { p := &Player{} *p = Player{ inv: inventory.New(36, func(slot int, before, after item.Stack) { @@ -125,21 +123,20 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3, dp DimensionProvider) *Pla p.broadcastItems(slot, before, after) } }), - enderChest: inventory.New(27, nil), - uuid: uuid.New(), - offHand: inventory.New(1, p.broadcastItems), - armour: inventory.NewArmour(p.broadcastArmour), - hunger: newHungerManager(), - health: entity.NewHealthManager(20, 20), - experience: entity.NewExperienceManager(), - effects: entity.NewEffectManager(), - name: name, - locale: language.BritishEnglish, - breathing: true, - cooldowns: make(map[string]time.Time), - mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, - heldSlot: &atomic.Uint32{}, - dimensionProvider: dp, + enderChest: inventory.New(27, nil), + uuid: uuid.New(), + offHand: inventory.New(1, p.broadcastItems), + armour: inventory.NewArmour(p.broadcastArmour), + hunger: newHungerManager(), + health: entity.NewHealthManager(20, 20), + experience: entity.NewExperienceManager(), + effects: entity.NewEffectManager(), + name: name, + locale: language.BritishEnglish, + breathing: true, + cooldowns: make(map[string]time.Time), + mc: &entity.MovementComputer{Gravity: 0.08, Drag: 0.02, DragBeforeGravity: true}, + heldSlot: &atomic.Uint32{}, } var scoreTag string var heldSlot uint32 @@ -167,8 +164,8 @@ func New(name string, skin skin.Skin, pos mgl64.Vec3, dp DimensionProvider) *Pla // A set of additional fields must be provided to initialise the player with the client's data, such as the // name and the skin of the player. You can either pass on player data you want to load or // you can leave the data as nil to use default data. -func NewWithSession(name, xuid string, uuid uuid.UUID, skin skin.Skin, s *session.Session, pos mgl64.Vec3, data *Data, dp DimensionProvider) *Player { - p := New(name, skin, pos, dp) +func NewWithSession(name, xuid string, uuid uuid.UUID, skin skin.Skin, s *session.Session, pos mgl64.Vec3, data *Data) *Player { + p := New(name, skin, pos) p.s.Store(s) p.skin.Store(&skin) p.uuid, p.xuid = uuid, xuid @@ -924,7 +921,7 @@ func (p *Player) kill(src world.DamageSource) { // We have an actual client connected to this player: We change its 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. - pos, w, blockHasBeenBroken := p.realSpawnPos() + pos, w, blockHasBeenBroken, _ := p.realSpawnPos() vec := pos.Vec3() if blockHasBeenBroken { @@ -961,14 +958,19 @@ func (p *Player) dropContents() { // Respawn spawns the player after it dies, so that its health is replenished and it is spawned in the world // again. Nothing will happen if the player does not have a session connected to it. func (p *Player) Respawn() { - position, w, ok := p.realSpawnPos() - if bl, ok := w.Block(position).(block.SpawnBlock); ok { - bl.Update(position, p, w) - } - pos := position.Vec3Middle() + position, w, ok, previousDimension := p.realSpawnPos() if ok { - p.Messaget("%tile.bed.notValid") + switch previousDimension { + case world.Nether: + p.Messaget("%tile.respawn_anchor.notValid") + case world.Overworld: + p.Messaget("%tile.bed.notValid") + } + } + if bl, ok := w.Block(position).(block.RespawnBlock); ok { + bl.SpawnOn(position, p, w) } + pos := position.Vec3Middle() if !p.Dead() || w == nil || p.session() == session.Nop { return } @@ -978,10 +980,6 @@ func (p *Player) Respawn() { p.Extinguish() p.ResetFallDistance() - // 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()) - p.Handler().HandleRespawn(&pos, &w) w.AddEntity(p) @@ -992,13 +990,19 @@ func (p *Player) Respawn() { } // realSpawnPos returns position and world where player should be spawned. -func (p *Player) realSpawnPos() (cube.Pos, *world.World, bool) { - pos := p.World().PlayerSpawn(p.uuid) - w := p.World() - if b, ok := w.Block(pos).(block.SpawnBlock); ok && b.SpawnBlock() { - return pos, w, false +func (p *Player) realSpawnPos() (pos cube.Pos, w *world.World, spawnBlockBroken bool, previousDimension world.Dimension) { + w = p.World() + + previousDimension = w.Dimension() + pos = w.PlayerSpawn(p.UUID()) + if b, ok := w.Block(pos).(block.RespawnBlock); ok && b.CanSpawn() { + return pos, w, false, previousDimension } - return p.dimensionProvider.World().Spawn(), p.dimensionProvider.World(), true + + // 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()) + return w.Spawn(), w, true, previousDimension } // StartSprinting makes a player start sprinting, increasing the speed of the player by 30% and making @@ -1237,7 +1241,6 @@ func (p *Player) Wake() { } w := p.World() - w.SetRequiredSleepDuration(0) w.BroadcastSleepingIndicator() for _, v := range p.viewers() { @@ -3216,11 +3219,3 @@ func (p *Player) resendBlock(pos cube.Pos, w *world.World) { func format(a []any) string { return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n") } - -// DimensionProvider provides access to all 3 dimensions. -type DimensionProvider interface { - World() *world.World - Nether() *world.World - End() *world.World - Players() []*Player -} diff --git a/server/server.go b/server/server.go index 71622b8d0..1801e27ad 100644 --- a/server/server.go +++ b/server/server.go @@ -455,7 +455,7 @@ func (srv *Server) createPlayer(id uuid.UUID, conn session.Conn, data *player.Da w, gm, pos = data.World, data.GameMode, data.Position } s := session.New(conn, srv.conf.MaxChunkRadius, srv.conf.Log, srv.conf.JoinMessage, srv.conf.QuitMessage) - p := player.NewWithSession(conn.IdentityData().DisplayName, conn.IdentityData().XUID, id, srv.parseSkin(conn.ClientData()), s, pos, data, srv) + p := player.NewWithSession(conn.IdentityData().DisplayName, conn.IdentityData().XUID, id, srv.parseSkin(conn.ClientData()), s, pos, data) s.Spawn(p, pos, w, gm, srv.handleSessionClose) srv.pwg.Add(1) diff --git a/server/world/sleep.go b/server/world/sleep.go index 3bdaaa601..d94052f48 100644 --- a/server/world/sleep.go +++ b/server/world/sleep.go @@ -2,17 +2,14 @@ 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) diff --git a/server/world/viewer.go b/server/world/viewer.go index 85c07bc24..e32f0aaa5 100644 --- a/server/world/viewer.go +++ b/server/world/viewer.go @@ -70,7 +70,7 @@ 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 views an entity waking up from a bed. ViewEntityWake(e Entity) } From 5342bc81501056dd363de0056186f4577266b7ab Mon Sep 17 00:00:00 2001 From: fdutch Date: Sun, 24 Nov 2024 21:34:17 +0100 Subject: [PATCH 10/11] requested changes --- server/block/bed.go | 40 ++++++++++--------------- server/block/register.go | 4 +-- server/block/respawn_anchor.go | 30 ++++++++----------- server/item/item.go | 8 +---- server/player/player.go | 21 ++++++------- server/session/handler_player_action.go | 2 +- server/world/sleep.go | 2 ++ server/world/world.go | 11 ------- 8 files changed, 44 insertions(+), 74 deletions(-) diff --git a/server/block/bed.go b/server/block/bed.go index 5f57a05d3..a303a4a1d 100644 --- a/server/block/bed.go +++ b/server/block/bed.go @@ -21,8 +21,8 @@ type Bed struct { 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 + // Sleeper is the user that is using the bed. It is only set for the Head part of the bed. + Sleeper world.Sleeper } // MaxCount always returns 1. @@ -47,9 +47,7 @@ func (b Bed) BreakInfo() BreakInfo { if !ok { return } - if s, ok := headSide.User.(world.Sleeper); ok { - s.Wake() - } + headSide.Sleeper.Wake() }) } @@ -115,9 +113,9 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ return false } - previousSpawn := w.PlayerSpawn(u.UUID()) - if previousSpawn != pos { - w.SetPlayerSpawn(u.UUID(), pos) + previousSpawn := w.PlayerSpawn(s.UUID()) + if previousSpawn != pos && previousSpawn != sidePos { + w.SetPlayerSpawn(s.UUID(), pos) s.Messaget(text.Colourf("%%tile.bed.respawnSet")) } @@ -126,7 +124,7 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ s.Messaget(text.Colourf("%%tile.bed.noSleep")) return true } - if headSide.User != nil { + if headSide.Sleeper != nil { s.Messaget(text.Colourf("%%tile.bed.occupied")) return true } @@ -137,26 +135,16 @@ func (b Bed) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, _ // 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 + vel[1] = vel[1] * -2 / 3 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. @@ -180,8 +168,8 @@ func (b Bed) EncodeItem() (name string, meta int16) { // 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), + "direction": int32(horizontalDirection(b.Facing)), + "occupied_bit": boolByte(b.Sleeper != nil), "head_bit": boolByte(b.Head), } } @@ -226,9 +214,11 @@ func (b Bed) side(pos cube.Pos, w *world.World) (Bed, cube.Pos, bool) { // 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}) + for _, c := range item.Colours() { + for _, d := range cube.Directions() { + beds = append(beds, Bed{Facing: d, Colour: c}) + beds = append(beds, Bed{Facing: d, Colour: c, Head: true}) + } } return } diff --git a/server/block/register.go b/server/block/register.go index f9d629925..f13af45b3 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -464,8 +464,8 @@ func init() { world.RegisterItem(Copper{Type: c, Oxidation: o, Waxed: true}) } } - for _, t := range allRespawnAnchorsItems() { - world.RegisterItem(t) + for c := range 5 { + world.RegisterItem(RespawnAnchor{Charge: c}) } } diff --git a/server/block/respawn_anchor.go b/server/block/respawn_anchor.go index 9a5463144..197404c99 100644 --- a/server/block/respawn_anchor.go +++ b/server/block/respawn_anchor.go @@ -42,6 +42,11 @@ func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.Wo held, _ := u.HeldItems() _, usingGlowstone := held.Item().(Glowstone) + sleeper, ok := u.(world.Sleeper) + if !ok { + return false + } + if r.Charge < 4 && usingGlowstone { r.Charge++ w.SetBlock(pos, r, nil) @@ -49,19 +54,17 @@ func (r RespawnAnchor) Activate(pos cube.Pos, clickedFace cube.Face, w *world.Wo w.PlaySound(pos.Vec3Centre(), sound.RespawnAnchorCharge{Charge: r.Charge}) return true } - if w.Dimension() == world.Nether { - if r.Charge > 0 { - previousSpawn := w.PlayerSpawn(u.UUID()) + + if r.Charge > 0 { + if w.Dimension() == world.Nether { + previousSpawn := w.PlayerSpawn(sleeper.UUID()) if previousSpawn == pos { return false } - u.Messaget(text.Colourf("%%tile.bed.respawnSet")) - w.SetPlayerSpawn(u.UUID(), pos) + sleeper.Messaget(text.Colourf("%%tile.bed.respawnSet")) + w.SetPlayerSpawn(sleeper.UUID(), pos) + return false } - return false - } - - if r.Charge > 0 { w.SetBlock(pos, nil, nil) ExplosionConfig{ Size: 5, @@ -81,15 +84,6 @@ func allRespawnAnchors() []world.Block { return all } -// allRespawnAnchorsItems returns all possible respawn anchors as items. -func allRespawnAnchorsItems() []world.Item { - all := make([]world.Item, 0, 5) - for i := 0; i < 5; i++ { - all = append(all, RespawnAnchor{Charge: i}) - } - return all -} - func (r RespawnAnchor) CanSpawn() bool { return r.Charge > 0 } diff --git a/server/item/item.go b/server/item/item.go index 0c0e87ef1..cb405a551 100644 --- a/server/item/item.go +++ b/server/item/item.go @@ -7,7 +7,6 @@ import ( "github.com/df-mc/dragonfly/server/internal/lang" "github.com/df-mc/dragonfly/server/world" "github.com/go-gl/mathgl/mgl64" - "github.com/google/uuid" "golang.org/x/text/language" "image/color" "time" @@ -16,7 +15,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 } @@ -167,11 +166,6 @@ type User interface { UsingItem() bool ReleaseItem() UseItem() - - UUID() uuid.UUID - - Messaget(key string, a ...string) - Sneaking() bool } // Carrier represents an entity that is able to carry an item. diff --git a/server/player/player.go b/server/player/player.go index 813825274..b24cc92aa 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -921,11 +921,12 @@ func (p *Player) kill(src world.DamageSource) { // We have an actual client connected to this player: We change its 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. - pos, w, blockHasBeenBroken, _ := p.realSpawnPos() + pos, w, spawnObstructed, _ := p.spawnLocation() vec := pos.Vec3() - if blockHasBeenBroken { - p.Handler().HandleRespawn(&vec, &w) + p.Handler().HandleRespawn(&vec, &w) + + if spawnObstructed { w.SetPlayerSpawn(p.UUID(), w.Spawn()) } @@ -958,8 +959,8 @@ func (p *Player) dropContents() { // Respawn spawns the player after it dies, so that its health is replenished and it is spawned in the world // again. Nothing will happen if the player does not have a session connected to it. func (p *Player) Respawn() { - position, w, ok, previousDimension := p.realSpawnPos() - if ok { + position, w, spawnObstructed, previousDimension := p.spawnLocation() + if spawnObstructed { switch previousDimension { case world.Nether: p.Messaget("%tile.respawn_anchor.notValid") @@ -989,8 +990,8 @@ func (p *Player) Respawn() { p.SetVisible() } -// realSpawnPos returns position and world where player should be spawned. -func (p *Player) realSpawnPos() (pos cube.Pos, w *world.World, spawnBlockBroken bool, previousDimension world.Dimension) { +// spawnLocation returns position and world where player should be spawned. +func (p *Player) spawnLocation() (pos cube.Pos, w *world.World, spawnBlockBroken bool, previousDimension world.Dimension) { w = p.World() previousDimension = w.Dimension() @@ -1211,11 +1212,11 @@ func (p *Player) Sleep(pos cube.Pos) { w := p.World() if b, ok := w.Block(pos).(block.Bed); ok { - if b.User != nil { + if b.Sleeper != nil { // The player cannot sleep here. return } - b.User = p + b.Sleeper = p w.SetBlock(pos, b, nil) } @@ -1250,7 +1251,7 @@ func (p *Player) Wake() { pos := *p.sleepPos.Load() if b, ok := w.Block(pos).(block.Bed); ok { - b.User = nil + b.Sleeper = nil w.SetBlock(pos, b, nil) } } diff --git a/server/session/handler_player_action.go b/server/session/handler_player_action.go index 8bcff3fa3..542f79b06 100644 --- a/server/session/handler_player_action.go +++ b/server/session/handler_player_action.go @@ -23,7 +23,7 @@ func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityR return errSelfRuntimeID } switch action { - case protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone, protocol.PlayerActionStartSleeping: + case protocol.PlayerActionStartSleeping, protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone: // Don't do anything for these actions. case protocol.PlayerActionStopSleeping: if mode := s.c.GameMode(); !mode.Visible() && !mode.HasCollision() { diff --git a/server/world/sleep.go b/server/world/sleep.go index d94052f48..6aceb4140 100644 --- a/server/world/sleep.go +++ b/server/world/sleep.go @@ -2,6 +2,7 @@ package world import ( "github.com/df-mc/dragonfly/server/block/cube" + "github.com/google/uuid" ) // Sleeper represents an entity that can sleep. @@ -9,6 +10,7 @@ type Sleeper interface { Entity Name() string + UUID() uuid.UUID Messaget(key string, a ...string) SendSleepingIndicator(sleeping, max int) diff --git a/server/world/world.go b/server/world/world.go index 3541c677c..2d7e170fe 100644 --- a/server/world/world.go +++ b/server/world/world.go @@ -929,17 +929,6 @@ func (w *World) SetRequiredSleepDuration(duration time.Duration) { w.set.RequiredSleepTicks = duration.Milliseconds() / 50 } -// SetSleepRequirement sets the duration of time players in the world must sleep for, in order for the time to change to -// day. -func (w *World) SetSleepRequirement(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(). From c3ca7d03d6750d86de6c6511b94a30af0cfff0bc Mon Sep 17 00:00:00 2001 From: fdutch Date: Mon, 25 Nov 2024 16:48:56 +0100 Subject: [PATCH 11/11] requested changes --- server/block/bed.go | 8 +++----- server/player/player.go | 4 +--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/server/block/bed.go b/server/block/bed.go index a303a4a1d..67eb83559 100644 --- a/server/block/bed.go +++ b/server/block/bed.go @@ -214,11 +214,9 @@ func (b Bed) side(pos cube.Pos, w *world.World) (Bed, cube.Pos, bool) { // allBeds returns all possible beds. func allBeds() (beds []world.Block) { - for _, c := range item.Colours() { - for _, d := range cube.Directions() { - beds = append(beds, Bed{Facing: d, Colour: c}) - beds = append(beds, Bed{Facing: d, Colour: c, Head: true}) - } + for _, d := range cube.Directions() { + beds = append(beds, Bed{Facing: d}) + beds = append(beds, Bed{Facing: d, Head: true}) } return } diff --git a/server/player/player.go b/server/player/player.go index b24cc92aa..b9ca100b2 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -924,10 +924,8 @@ func (p *Player) kill(src world.DamageSource) { pos, w, spawnObstructed, _ := p.spawnLocation() vec := pos.Vec3() - p.Handler().HandleRespawn(&vec, &w) - if spawnObstructed { - w.SetPlayerSpawn(p.UUID(), w.Spawn()) + w.SetPlayerSpawn(p.UUID(), pos) } p.pos.Store(&vec)