diff --git a/server/block/composter.go b/server/block/composter.go index a309a38d0..e605b93f6 100644 --- a/server/block/composter.go +++ b/server/block/composter.go @@ -67,7 +67,7 @@ func (c Composter) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.Us if !ok { return false } - ctx.CountSub = 1 + ctx.SubtractFromCount(1) w.AddParticle(pos.Vec3(), particle.BoneMeal{}) if rand.Float64() > compostable.CompostChance() { w.PlaySound(pos.Vec3(), sound.ComposterFill{}) diff --git a/server/block/hash.go b/server/block/hash.go index baf995445..744da0ba0 100644 --- a/server/block/hash.go +++ b/server/block/hash.go @@ -93,6 +93,7 @@ const ( hashLapisOre hashLava hashLeaves + hashLectern hashLight hashLitPumpkin hashLog @@ -531,6 +532,10 @@ func (l Leaves) Hash() uint64 { return hashLeaves | uint64(l.Wood.Uint8())<<8 | uint64(boolByte(l.Persistent))<<12 | uint64(boolByte(l.ShouldUpdate))<<13 } +func (l Lectern) Hash() uint64 { + return hashLectern | uint64(l.Facing)<<8 +} + func (l Light) Hash() uint64 { return hashLight | uint64(l.Level)<<8 } diff --git a/server/block/jukebox.go b/server/block/jukebox.go index 36bb2057f..2cdd43b8c 100644 --- a/server/block/jukebox.go +++ b/server/block/jukebox.go @@ -32,7 +32,7 @@ func (j Jukebox) BreakInfo() BreakInfo { } return newBreakInfo(0.8, alwaysHarvestable, axeEffective, simpleDrops(d...)).withBreakHandler(func(pos cube.Pos, w *world.World, u item.User) { if _, hasDisc := j.Disc(); hasDisc { - w.PlaySound(pos.Vec3(), sound.MusicDiscEnd{}) + w.PlaySound(pos.Vec3Centre(), sound.MusicDiscEnd{}) } }) } @@ -51,22 +51,21 @@ func (j Jukebox) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User j.Item = item.Stack{} w.SetBlock(pos, j, nil) - w.PlaySound(pos.Vec3(), sound.MusicDiscEnd{}) + w.PlaySound(pos.Vec3Centre(), sound.MusicDiscEnd{}) } else if held, _ := u.HeldItems(); !held.Empty() { if m, ok := held.Item().(item.MusicDisc); ok { j.Item = held w.SetBlock(pos, j, nil) - w.PlaySound(pos.Vec3(), sound.MusicDiscEnd{}) - ctx.CountSub = 1 + w.PlaySound(pos.Vec3Centre(), sound.MusicDiscEnd{}) + ctx.SubtractFromCount(1) - w.PlaySound(pos.Vec3(), sound.MusicDiscPlay{DiscType: m.DiscType}) + w.PlaySound(pos.Vec3Centre(), sound.MusicDiscPlay{DiscType: m.DiscType}) if u, ok := u.(jukeboxUser); ok { u.SendJukeboxPopup(fmt.Sprintf("Now playing: %v - %v", m.DiscType.Author(), m.DiscType.DisplayName())) } } } - return true } @@ -77,7 +76,6 @@ func (j Jukebox) Disc() (sound.DiscType, bool) { return m.DiscType, true } } - return sound.DiscType{}, false } @@ -93,11 +91,9 @@ func (j Jukebox) EncodeNBT() map[string]any { // DecodeNBT ... func (j Jukebox) DecodeNBT(data map[string]any) any { s := nbtconv.MapItem(data, "RecordItem") - if _, ok := s.Item().(item.MusicDisc); ok { j.Item = s } - return j } diff --git a/server/block/lectern.go b/server/block/lectern.go new file mode 100644 index 000000000..9a8775a23 --- /dev/null +++ b/server/block/lectern.go @@ -0,0 +1,166 @@ +package block + +import ( + "fmt" + "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/df-mc/dragonfly/server/world/sound" + "github.com/go-gl/mathgl/mgl64" + "time" +) + +// Lectern is a librarian's job site block found in villages. It is used to hold books for multiple players to read in +// multiplayer. +// TODO: Redstone functionality. +type Lectern struct { + bass + sourceWaterDisplacer + + // Facing represents the direction the Lectern is facing. + Facing cube.Direction + // Book is the book currently held by the Lectern. + Book item.Stack + // Page is the page the Lectern is currently on in the book. + Page int +} + +// Model ... +func (Lectern) Model() world.BlockModel { + return model.Lectern{} +} + +// FuelInfo ... +func (Lectern) FuelInfo() item.FuelInfo { + return newFuelInfo(time.Second * 15) +} + +// SideClosed ... +func (Lectern) SideClosed(cube.Pos, cube.Pos, *world.World) bool { + return false +} + +// BreakInfo ... +func (l Lectern) BreakInfo() BreakInfo { + d := []item.Stack{item.NewStack(Lectern{}, 1)} + if !l.Book.Empty() { + d = append(d, l.Book) + } + return newBreakInfo(2, alwaysHarvestable, axeEffective, simpleDrops(d...)) +} + +// UseOnBlock ... +func (l Lectern) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *world.World, user item.User, ctx *item.UseContext) (used bool) { + pos, _, used = firstReplaceable(w, pos, face, l) + if !used { + return false + } + l.Facing = user.Rotation().Direction().Opposite() + place(w, pos, l, user, ctx) + return placed(ctx) +} + +// readableBook represents a book that can be read through a lectern. +type readableBook interface { + // TotalPages returns the total number of pages in the book. + TotalPages() int + // Page returns a specific page from the book and true when the page exists. It will otherwise return an empty string + // and false. + Page(page int) (string, bool) +} + +// Activate ... +func (l Lectern) Activate(pos cube.Pos, _ cube.Face, w *world.World, u item.User, ctx *item.UseContext) bool { + if !l.Book.Empty() { + // We can't put a book on the lectern if it's full. + return false + } + + held, _ := u.HeldItems() + if _, ok := held.Item().(readableBook); !ok { + // We can't put a non-book item on the lectern. + return false + } + + l.Book, l.Page = held, 0 + w.SetBlock(pos, l, nil) + + w.PlaySound(pos.Vec3Centre(), sound.LecternBookPlace{}) + ctx.SubtractFromCount(1) + return true +} + +// Punch ... +func (l Lectern) Punch(pos cube.Pos, _ cube.Face, w *world.World, _ item.User) { + if l.Book.Empty() { + // We can't remove a book from the lectern if there isn't one. + return + } + + dropItem(w, l.Book, pos.Side(cube.FaceUp).Vec3Middle()) + + l.Book = item.Stack{} + w.SetBlock(pos, l, nil) + w.PlaySound(pos.Vec3Centre(), sound.Attack{}) +} + +// TurnPage updates the page the lectern is currently on to the page given. +func (l Lectern) TurnPage(pos cube.Pos, w *world.World, page int) error { + if page == l.Page { + // We're already on the correct page, so we don't need to do anything. + return nil + } + if l.Book.Empty() { + return fmt.Errorf("lectern at %v is empty", pos) + } + if r, ok := l.Book.Item().(readableBook); ok && (page >= r.TotalPages() || page < 0) { + return fmt.Errorf("page number %d is out of bounds", page) + } + l.Page = page + w.SetBlock(pos, l, nil) + return nil +} + +// EncodeNBT ... +func (l Lectern) EncodeNBT() map[string]any { + m := map[string]any{ + "hasBook": boolByte(!l.Book.Empty()), + "page": int32(l.Page), + "id": "Lectern", + } + if r, ok := l.Book.Item().(readableBook); ok { + m["book"] = nbtconv.WriteItem(l.Book, true) + m["totalPages"] = int32(r.TotalPages()) + } + return m +} + +// DecodeNBT ... +func (l Lectern) DecodeNBT(m map[string]any) any { + l.Page = int(nbtconv.Int32(m, "page")) + l.Book = nbtconv.MapItem(m, "book") + return l +} + +// EncodeItem ... +func (Lectern) EncodeItem() (name string, meta int16) { + return "minecraft:lectern", 0 +} + +// EncodeBlock ... +func (l Lectern) EncodeBlock() (string, map[string]any) { + return "minecraft:lectern", map[string]any{ + "direction": int32(horizontalDirection(l.Facing)), + "powered_bit": uint8(0), // We don't support redstone, anyway. + } +} + +// allLecterns ... +func allLecterns() (lecterns []world.Block) { + for _, d := range cube.Directions() { + lecterns = append(lecterns, Lectern{Facing: d}) + } + return +} diff --git a/server/block/model/lectern.go b/server/block/model/lectern.go new file mode 100644 index 000000000..99c2f3afa --- /dev/null +++ b/server/block/model/lectern.go @@ -0,0 +1,19 @@ +package model + +import ( + "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/world" +) + +// Lectern is a model used by lecterns. +type Lectern struct{} + +// BBox ... +func (Lectern) BBox(cube.Pos, *world.World) []cube.BBox { + return []cube.BBox{cube.Box(0, 0, 0, 1, 0.9, 1)} +} + +// FaceSolid ... +func (Lectern) FaceSolid(cube.Pos, cube.Face, *world.World) bool { + return false +} diff --git a/server/block/register.go b/server/block/register.go index 9a2dddc67..36b89fa5d 100644 --- a/server/block/register.go +++ b/server/block/register.go @@ -153,6 +153,7 @@ func init() { registerAll(allLanterns()) registerAll(allLava()) registerAll(allLeaves()) + registerAll(allLecterns()) registerAll(allLight()) registerAll(allLitPumpkins()) registerAll(allLogs()) @@ -265,6 +266,7 @@ func init() { world.RegisterItem(Kelp{}) world.RegisterItem(Ladder{}) world.RegisterItem(Lapis{}) + world.RegisterItem(Lectern{}) world.RegisterItem(LitPumpkin{}) world.RegisterItem(Loom{}) world.RegisterItem(MelonSeeds{}) diff --git a/server/block/wood_door.go b/server/block/wood_door.go index affa2fd99..0a595b760 100644 --- a/server/block/wood_door.go +++ b/server/block/wood_door.go @@ -100,7 +100,7 @@ func (d WoodDoor) UseOnBlock(pos cube.Pos, face cube.Face, _ mgl64.Vec3, w *worl ctx.IgnoreBBox = true place(w, pos, d, user, ctx) place(w, pos.Side(cube.FaceUp), WoodDoor{Wood: d.Wood, Facing: d.Facing, Top: true, Right: d.Right}, user, ctx) - ctx.CountSub = 1 + ctx.SubtractFromCount(1) return placed(ctx) } diff --git a/server/item/bone_meal.go b/server/item/bone_meal.go index 81d9cca8e..30a002d9a 100644 --- a/server/item/bone_meal.go +++ b/server/item/bone_meal.go @@ -19,7 +19,7 @@ type BoneMealAffected interface { // UseOnBlock ... func (b BoneMeal) UseOnBlock(pos cube.Pos, _ cube.Face, _ mgl64.Vec3, w *world.World, _ User, ctx *UseContext) bool { if bm, ok := w.Block(pos).(BoneMealAffected); ok && bm.BoneMeal(pos, w) { - ctx.CountSub = 1 + ctx.SubtractFromCount(1) w.AddParticle(pos.Vec3(), particle.BoneMeal{}) return true } diff --git a/server/item/book_and_quill.go b/server/item/book_and_quill.go index 9ec1cc5ca..a9437d5fa 100644 --- a/server/item/book_and_quill.go +++ b/server/item/book_and_quill.go @@ -9,10 +9,15 @@ type BookAndQuill struct { } // MaxCount always returns 1. -func (b BookAndQuill) MaxCount() int { +func (BookAndQuill) MaxCount() int { return 1 } +// TotalPages returns the total number of pages in the book. +func (b BookAndQuill) TotalPages() int { + return len(b.Pages) +} + // Page returns a specific page from the book and true when the page exists. It will otherwise return an empty string // and false. func (b BookAndQuill) Page(page int) (string, bool) { @@ -105,7 +110,7 @@ func (b BookAndQuill) EncodeNBT() map[string]any { } // EncodeItem ... -func (b BookAndQuill) EncodeItem() (name string, meta int16) { +func (BookAndQuill) EncodeItem() (name string, meta int16) { return "minecraft:writable_book", 0 } diff --git a/server/item/glass_bottle.go b/server/item/glass_bottle.go index c77ba4172..7aea9ed98 100644 --- a/server/item/glass_bottle.go +++ b/server/item/glass_bottle.go @@ -24,7 +24,7 @@ func (g GlassBottle) UseOnBlock(pos cube.Pos, _ cube.Face, _ mgl64.Vec3, w *worl if b, ok := bl.(bottleFiller); ok { var res world.Block if res, ctx.NewItem, ok = b.FillBottle(); ok { - ctx.CountSub = 1 + ctx.SubtractFromCount(1) if res != bl { // Some blocks (think a cauldron) change when using a bottle on it. w.SetBlock(pos, res, nil) diff --git a/server/item/written_book.go b/server/item/written_book.go index 783120b91..762e7990b 100644 --- a/server/item/written_book.go +++ b/server/item/written_book.go @@ -15,10 +15,15 @@ type WrittenBook struct { } // MaxCount always returns 1. -func (w WrittenBook) MaxCount() int { +func (WrittenBook) MaxCount() int { return 1 } +// TotalPages returns the total number of pages in the book. +func (w WrittenBook) TotalPages() int { + return len(w.Pages) +} + // Page returns a specific page from the book and true when the page exists. It will otherwise return an empty string // and false. func (w WrittenBook) Page(page int) (string, bool) { @@ -66,6 +71,6 @@ func (w WrittenBook) EncodeNBT() map[string]any { } // EncodeItem ... -func (w WrittenBook) EncodeItem() (name string, meta int16) { +func (WrittenBook) EncodeItem() (name string, meta int16) { return "minecraft:written_book", 0 } diff --git a/server/player/handler.go b/server/player/handler.go index 3f0f71d62..5f13e4ba5 100644 --- a/server/player/handler.go +++ b/server/player/handler.go @@ -107,6 +107,9 @@ 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, frontSide bool, oldText, newText string) + // 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) // 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 @@ -154,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, bool, string, string) {} +func (NopHandler) HandleLecternPageTurn(*event.Context, cube.Pos, int, *int) {} 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 72ee1a7df..dcda971d3 100644 --- a/server/player/player.go +++ b/server/player/player.go @@ -2586,7 +2586,7 @@ func (p *Player) OpenSign(pos cube.Pos, frontSide bool) { } // EditSign edits the sign at the cube.Pos passed and writes the text passed to a sign at that position. If no sign is -// present or if the Player cannot edit it, an error is returned +// present, an error is returned. func (p *Player) EditSign(pos cube.Pos, frontText, backText string) error { w := p.World() sign, ok := w.Block(pos).(block.Sign) @@ -2618,6 +2618,25 @@ func (p *Player) EditSign(pos cube.Pos, frontText, backText string) error { return nil } +// TurnLecternPage edits the lectern at the cube.Pos passed by turning the page to the page passed. If no lectern is +// present, an error is returned. +func (p *Player) TurnLecternPage(pos cube.Pos, page int) error { + w := p.World() + lectern, ok := w.Block(pos).(block.Lectern) + if !ok { + return fmt.Errorf("edit lectern: no lectern at position %v", pos) + } + + ctx := event.C() + if p.Handler().HandleLecternPageTurn(ctx, pos, lectern.Page, &page); ctx.Cancelled() { + return nil + } + + lectern.Page = page + w.SetBlock(pos, lectern, nil) + return nil +} + // updateState updates the state of the player to all viewers of the player. func (p *Player) updateState() { for _, v := range p.viewers() { diff --git a/server/session/controllable.go b/server/session/controllable.go index 6efca8cfd..f14344e6c 100644 --- a/server/session/controllable.go +++ b/server/session/controllable.go @@ -85,6 +85,7 @@ type Controllable interface { OpenSign(pos cube.Pos, frontSide bool) EditSign(pos cube.Pos, frontText, backText string) error + TurnLecternPage(pos cube.Pos, page int) error EnderChestInventory() *inventory.Inventory diff --git a/server/session/handler_block_actor_data.go b/server/session/handler_block_actor_data.go index 3aeff2552..e69e798e1 100644 --- a/server/session/handler_block_actor_data.go +++ b/server/session/handler_block_actor_data.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/df-mc/dragonfly/server/block" "github.com/df-mc/dragonfly/server/block/cube" + "github.com/df-mc/dragonfly/server/entity" + "github.com/go-gl/mathgl/mgl64" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" "strings" "unicode/utf8" @@ -17,7 +19,10 @@ type BlockActorDataHandler struct{} func (b BlockActorDataHandler) Handle(p packet.Packet, s *Session) error { pk := p.(*packet.BlockActorData) if id, ok := pk.NBTData["id"]; ok { - pos := cube.Pos{int(pk.Position.X()), int(pk.Position.Y()), int(pk.Position.Z())} + pos := blockPosFromProtocol(pk.Position) + if !canReach(s.c, pos.Vec3Middle()) { + return fmt.Errorf("block at %v is not within reach", pos) + } switch id { case "Sign": return b.handleSign(pk, pos, s) @@ -86,3 +91,21 @@ func (b BlockActorDataHandler) textFromNBTData(data map[string]any, frontSide bo } return text, nil } + +// canReach checks if a player can reach a position with its current range. The range depends on if the player +// is either survival or creative mode. +func canReach(c Controllable, pos mgl64.Vec3) bool { + const ( + creativeRange = 14.0 + survivalRange = 8.0 + ) + if !c.GameMode().AllowsInteraction() { + return false + } + + eyes := entity.EyePosition(c) + if c.GameMode().CreativeInventory() { + return eyes.Sub(pos).Len() <= creativeRange && !c.Dead() + } + return eyes.Sub(pos).Len() <= survivalRange && !c.Dead() +} diff --git a/server/session/handler_lectern_update.go b/server/session/handler_lectern_update.go new file mode 100644 index 000000000..4a41054d6 --- /dev/null +++ b/server/session/handler_lectern_update.go @@ -0,0 +1,28 @@ +package session + +import ( + "fmt" + "github.com/df-mc/dragonfly/server/block" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// LecternUpdateHandler handles the LecternUpdate packet, sent when a player interacts with a lectern. +type LecternUpdateHandler struct{} + +// Handle ... +func (LecternUpdateHandler) Handle(p packet.Packet, s *Session) error { + pk := p.(*packet.LecternUpdate) + if pk.DropBook { + // This is completely redundant, so ignore this packet. + return nil + } + + pos := blockPosFromProtocol(pk.Position) + if !canReach(s.c, pos.Vec3Middle()) { + return fmt.Errorf("block at %v is not within reach", pos) + } + if _, ok := s.c.World().Block(pos).(block.Lectern); !ok { + return fmt.Errorf("block at %v is not a lectern", pos) + } + return s.c.TurnLecternPage(pos, int(pk.Page)) +} diff --git a/server/session/session.go b/server/session/session.go index e83bb2ea7..c8d6b2226 100644 --- a/server/session/session.go +++ b/server/session/session.go @@ -457,6 +457,7 @@ func (s *Session) registerHandlers() { packet.IDInventoryTransaction: &InventoryTransactionHandler{}, packet.IDItemFrameDropItem: nil, packet.IDItemStackRequest: &ItemStackRequestHandler{changes: map[byte]map[byte]changeInfo{}, responseChanges: map[int32]map[*inventory.Inventory]map[byte]responseChange{}}, + packet.IDLecternUpdate: &LecternUpdateHandler{}, packet.IDLevelSoundEvent: &LevelSoundEventHandler{}, packet.IDMobEquipment: &MobEquipmentHandler{}, packet.IDModalFormResponse: &ModalFormResponseHandler{forms: make(map[uint32]form.Form)}, diff --git a/server/session/world.go b/server/session/world.go index 26dcc3b51..4624fb85d 100644 --- a/server/session/world.go +++ b/server/session/world.go @@ -758,6 +758,8 @@ func (s *Session) playSound(pos mgl64.Vec3, t world.Sound, disableRelative bool) pk.SoundType = packet.SoundEventComposterFillLayer case sound.ComposterReady: pk.SoundType = packet.SoundEventComposterReady + case sound.LecternBookPlace: + pk.SoundType = packet.SoundEventLecternBookPlace } s.writePacket(pk) } @@ -1155,6 +1157,11 @@ func vec64To32(vec3 mgl64.Vec3) mgl32.Vec3 { return mgl32.Vec3{float32(vec3[0]), float32(vec3[1]), float32(vec3[2])} } +// blockPosFromProtocol ... +func blockPosFromProtocol(pos protocol.BlockPos) cube.Pos { + return cube.Pos{int(pos.X()), int(pos.Y()), int(pos.Z())} +} + // 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/sound/block.go b/server/world/sound/block.go index 9fc06dde1..e735e3ec8 100644 --- a/server/world/sound/block.go +++ b/server/world/sound/block.go @@ -173,6 +173,9 @@ type ComposterFillLayer struct{ sound } // ComposterReady is a sound played when a composter has produced bone meal and is ready to be collected. type ComposterReady struct{ sound } +// LecternBookPlace is a sound played when a book is placed in a lectern. +type LecternBookPlace struct{ sound } + // sound implements the world.Sound interface. type sound struct{}