diff --git a/bot/ingame.go b/bot/ingame.go index 675144a..7529201 100644 --- a/bot/ingame.go +++ b/bot/ingame.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/Tnze/go-mc/bot/world" + "github.com/Tnze/go-mc/bot/world/entity" "github.com/Tnze/go-mc/bot/world/entity/player" "github.com/Tnze/go-mc/chat" "github.com/Tnze/go-mc/data" @@ -186,6 +187,8 @@ func (c *Client) handlePacket(p pk.Packet) (disconnect bool, err error) { err = handleMultiBlockChangePacket(c, p) case data.UnloadChunk: err = handleUnloadChunkPacket(c, p) + case data.TileEntityData: + err = handleTileEntityDataPacket(c, p) case data.PositionClientbound: err = handlePlayerPositionAndLookPacket(c, p) @@ -623,11 +626,23 @@ func handleChunkDataPacket(c *Client, p pk.Packet) error { if err != nil { return fmt.Errorf("decode chunk column: %w", err) } + chunk.TileEntities = make(map[world.TilePosition]entity.BlockEntity, 64) + for _, e := range pkt.BlockEntities { + chunk.TileEntities[world.ToTilePos(e.X, e.Y, e.Z)] = e + } c.Wd.LoadChunk(int(pkt.X), int(pkt.Z), chunk) return nil } +func handleTileEntityDataPacket(c *Client, p pk.Packet) error { + var pkt ptypes.TileEntityData + if err := pkt.Decode(p); err != nil { + return err + } + return c.Wd.TileEntityUpdate(pkt) +} + func handlePlayerPositionAndLookPacket(c *Client, p pk.Packet) error { var pkt ptypes.PositionAndLookClientbound if err := pkt.Decode(p); err != nil { diff --git a/bot/world/entity/entity.go b/bot/world/entity/entity.go index 1744c1f..2ccc6c8 100644 --- a/bot/world/entity/entity.go +++ b/bot/world/entity/entity.go @@ -8,6 +8,23 @@ import ( "github.com/google/uuid" ) +// BlockEntity describes the representation of a tile entity at a position. +type BlockEntity struct { + ID string `nbt:"id"` + + // global co-ordinates + X int `nbt:"x"` + Y int `nbt:"y"` + Z int `nbt:"z"` + + // sign-specific. + Color string `nbt:"color"` + Text1 string `nbt:"Text1"` + Text2 string `nbt:"Text2"` + Text3 string `nbt:"Text3"` + Text4 string `nbt:"Text4"` +} + //Entity represents an instance of an entity. type Entity struct { ID int32 diff --git a/bot/world/tile.go b/bot/world/tile.go new file mode 100644 index 0000000..b862047 --- /dev/null +++ b/bot/world/tile.go @@ -0,0 +1,21 @@ +package world + +import ( + "fmt" +) + +// TilePosition describes the location of a tile/block entity within a chunk. +type TilePosition uint32 + +func (p TilePosition) Pos() (x, y, z int) { + return int((p>>8) & 0xff), int((p>>16) & 0xff), int(p&0xff) +} + +func (p TilePosition) String() string { + x, y, z := p.Pos() + return fmt.Sprintf("(%d, %d, %d)", x, y, z) +} + +func ToTilePos(x, y, z int) TilePosition { + return TilePosition((y&0xff) << 16 | (x&15) << 8 | (z&15)) +} diff --git a/bot/world/world_chunk.go b/bot/world/world_chunk.go index 691f791..4157e90 100644 --- a/bot/world/world_chunk.go +++ b/bot/world/world_chunk.go @@ -1,14 +1,17 @@ package world import ( + "github.com/Tnze/go-mc/bot/world/entity" "github.com/Tnze/go-mc/data/block" pk "github.com/Tnze/go-mc/net/packet" + "github.com/Tnze/go-mc/net/ptypes" ) // Chunk store a 256*16*16 area of blocks, sharded on the Y axis into 16 // sections. type Chunk struct { - Sections [16]Section + Sections [16]Section + TileEntities map[TilePosition]entity.BlockEntity } // Section implements storage of blocks within a fixed 16*16*16 area. @@ -29,6 +32,23 @@ type ChunkLoc struct { X, Z int } +// Signs returns a list of signs on the server within viewing range. +func (w *World) Signs() []entity.BlockEntity { + w.chunkLock.RLock() + defer w.chunkLock.RUnlock() + + out := make([]entity.BlockEntity, 0, 4) + for _, c := range w.Chunks { + for _, e := range c.TileEntities { + if e.ID == "minecraft:sign" { + out = append(out, e) + } + } + } + + return out +} + // GetBlockStatus return the state ID of the block at the given position. func (w *World) GetBlockStatus(x, y, z int) BlockStatus { w.chunkLock.RLock() @@ -67,6 +87,10 @@ func (w *World) UnaryBlockUpdate(pos pk.Position, bStateID BlockStatus) bool { sec.SetBlock(bIdx, bStateID) c.Sections[sIdx] = sec } else { + tp := ToTilePos(pos.X, pos.Y, pos.Z) + if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID { + delete(c.TileEntities, tp) + } sec.SetBlock(bIdx, bStateID) } return true @@ -90,9 +114,16 @@ func (w *World) MultiBlockUpdate(loc ChunkLoc, sectionY int, blocks []pk.VarLong } for _, b := range blocks { - bStateID := b >> 12 - x, z, y := (b>>8)&0xf, (b>>4)&0xf, b&0xf - sec.SetBlock(sectionIdx(int(x&15), int(y&15), int(z&15)), BlockStatus(bStateID)) + var ( + bStateID = BlockStatus(b >> 12) + x, z, y = (b >> 8) & 0xf, (b >> 4) & 0xf, b & 0xf + bIdx = sectionIdx(int(x&15), int(y&15), int(z&15)) + tp = ToTilePos(int(x), int(y), int(z)) + ) + if _, ok := c.TileEntities[tp]; ok && sec.GetBlock(bIdx) != bStateID { + delete(c.TileEntities, tp) + } + sec.SetBlock(bIdx, bStateID) } return true @@ -104,3 +135,18 @@ func (w *World) LoadChunk(x, z int, c *Chunk) { w.Chunks[ChunkLoc{X: x, Z: z}] = c w.chunkLock.Unlock() } + +func (w *World) TileEntityUpdate(pkt ptypes.TileEntityData) error { + w.chunkLock.Lock() + defer w.chunkLock.Unlock() + c := w.Chunks[ChunkLoc{X: pkt.Pos.X >> 4, Z: pkt.Pos.Z >> 4}] + if c == nil { + return nil + } + + switch pkt.Action { + case 9: // Sign update + c.TileEntities[ToTilePos(pkt.Pos.X, pkt.Pos.Y, pkt.Pos.Z)] = pkt.Data + } + return nil +} diff --git a/net/ptypes/chunk.go b/net/ptypes/chunk.go index 59d6b8a..764a8f8 100644 --- a/net/ptypes/chunk.go +++ b/net/ptypes/chunk.go @@ -5,6 +5,7 @@ import ( "bytes" "fmt" + "github.com/Tnze/go-mc/bot/world/entity" "github.com/Tnze/go-mc/nbt" pk "github.com/Tnze/go-mc/net/packet" ) @@ -77,8 +78,7 @@ func (b *biomesData) Decode(r pk.DecodeReader) error { } type chunkData []byte -type blockEntities []blockEntity -type blockEntity struct{} +type blockEntities []entity.BlockEntity // Decode implement net.packet.FieldDecoder func (c *chunkData) Decode(r pk.DecodeReader) error { @@ -108,3 +108,21 @@ func (b *blockEntities) Decode(r pk.DecodeReader) error { } return nil } + +// TileEntityData describes a change to a tile entity. +type TileEntityData struct { + Pos pk.Position + Action pk.UnsignedByte + Data entity.BlockEntity +} + +func (p *TileEntityData) Decode(pkt pk.Packet) error { + r := bytes.NewReader(pkt.Data) + if err := p.Pos.Decode(r); err != nil { + return fmt.Errorf("position: %v", err) + } + if err := p.Action.Decode(r); err != nil { + return fmt.Errorf("action: %v", err) + } + return nbt.NewDecoder(r).Decode(&p.Data) +}