package session import ( "bytes" "github.com/cespare/xxhash/v2" "github.com/df-mc/dragonfly/server/block/cube" "github.com/df-mc/dragonfly/server/world" "github.com/df-mc/dragonfly/server/world/chunk" "github.com/sandertv/gophertunnel/minecraft/nbt" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" ) // subChunkRequests is set to true to enable the sub-chunk request system. This can (likely) cause unexpected issues, // but also solves issues with block entities such as item frames and lecterns as of v1.19.10. const subChunkRequests = true // ViewChunk ... func (s *Session) ViewChunk(pos world.ChunkPos, dim world.Dimension, blockEntities map[cube.Pos]world.Block, c *chunk.Chunk) { if !s.conn.ClientCacheEnabled() { s.sendNetworkChunk(pos, dim, c, blockEntities) return } s.sendBlobHashes(pos, dim, c, blockEntities) } // ViewSubChunks ... func (s *Session) ViewSubChunks(centre world.SubChunkPos, offsets []protocol.SubChunkOffset, tx *world.Tx) { r := tx.Range() entries := make([]protocol.SubChunkEntry, 0, len(offsets)) transaction := make(map[uint64]struct{}) for _, offset := range offsets { ind := int16(centre.Y()) + int16(offset[1]) - int16(r[0])>>4 if ind < 0 || ind > int16(r.Height()>>4) { entries = append(entries, protocol.SubChunkEntry{Result: protocol.SubChunkResultIndexOutOfBounds, Offset: offset}) continue } col, ok := s.chunkLoader.Chunk(world.ChunkPos{ centre.X() + int32(offset[0]), centre.Z() + int32(offset[2]), }) if !ok { entries = append(entries, protocol.SubChunkEntry{Result: protocol.SubChunkResultChunkNotFound, Offset: offset}) continue } entries = append(entries, s.subChunkEntry(offset, ind, col, transaction)) } if s.conn.ClientCacheEnabled() && len(transaction) > 0 { s.blobMu.Lock() s.openChunkTransactions = append(s.openChunkTransactions, transaction) s.blobMu.Unlock() } dim, _ := world.DimensionID(tx.World().Dimension()) s.writePacket(&packet.SubChunk{ Dimension: int32(dim), Position: protocol.SubChunkPos(centre), CacheEnabled: s.conn.ClientCacheEnabled(), SubChunkEntries: entries, }) } func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col *world.Column, transaction map[uint64]struct{}) protocol.SubChunkEntry { chunkMap := col.HeightMap() subMapType, subMap := byte(protocol.HeightMapDataHasData), make([]int8, 256) higher, lower := true, true for x := uint8(0); x < 16; x++ { for z := uint8(0); z < 16; z++ { y, i := chunkMap.At(x, z), (uint16(z)<<4)|uint16(x) otherInd := col.SubIndex(y) switch { case otherInd > ind: subMap[i], lower = 16, false case otherInd < ind: subMap[i], higher = -1, false default: subMap[i], lower, higher = int8(y-col.SubY(otherInd)), false, false } } } if higher { subMapType, subMap = protocol.HeightMapDataTooHigh, nil } else if lower { subMapType, subMap = protocol.HeightMapDataTooLow, nil } sub := col.Sub()[ind] if sub.Empty() { return protocol.SubChunkEntry{ Result: protocol.SubChunkResultSuccessAllAir, HeightMapType: subMapType, HeightMapData: subMap, RenderHeightMapType: subMapType, RenderHeightMapData: subMap, Offset: offset, } } serialisedSubChunk := chunk.EncodeSubChunk(col.Chunk, chunk.NetworkEncoding, int(ind)) blockEntityBuf := bytes.NewBuffer(nil) enc := nbt.NewEncoderWithEncoding(blockEntityBuf, nbt.NetworkLittleEndian) for pos, b := range col.BlockEntities { if n, ok := b.(world.NBTer); ok && col.SubIndex(int16(pos.Y())) == ind { d := n.EncodeNBT() d["x"], d["y"], d["z"] = int32(pos[0]), int32(pos[1]), int32(pos[2]) _ = enc.Encode(d) } } entry := protocol.SubChunkEntry{ Result: protocol.SubChunkResultSuccess, RawPayload: append(serialisedSubChunk, blockEntityBuf.Bytes()...), HeightMapType: subMapType, HeightMapData: subMap, RenderHeightMapType: subMapType, RenderHeightMapData: subMap, Offset: offset, } if s.conn.ClientCacheEnabled() { if hash := xxhash.Sum64(serialisedSubChunk); s.trackBlob(hash, serialisedSubChunk) { transaction[hash] = struct{}{} entry.BlobHash = hash entry.RawPayload = blockEntityBuf.Bytes() } } return entry } // dimensionID returns the dimension ID of the world that the session is in. func (s *Session) dimensionID(dim world.Dimension) int32 { d, _ := world.DimensionID(dim) return int32(d) } // sendBlobHashes sends chunk blob hashes of the data of the chunk and stores the data in a map of blobs. Only // data that the client doesn't yet have will be sent over the network. func (s *Session) sendBlobHashes(pos world.ChunkPos, dim world.Dimension, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block) { if subChunkRequests { biomes := chunk.EncodeBiomes(c, chunk.NetworkEncoding) if hash := xxhash.Sum64(biomes); s.trackBlob(hash, biomes) { s.writePacket(&packet.LevelChunk{ Dimension: s.dimensionID(dim), SubChunkCount: protocol.SubChunkRequestModeLimited, Position: protocol.ChunkPos(pos), HighestSubChunk: c.HighestFilledSubChunk(), BlobHashes: []uint64{hash}, RawPayload: []byte{0}, CacheEnabled: true, }) return } } var ( data = chunk.Encode(c, chunk.NetworkEncoding) count = uint32(len(data.SubChunks)) blobs = append(data.SubChunks, data.Biomes) hashes = make([]uint64, len(blobs)) m = make(map[uint64]struct{}, len(blobs)) ) for i, blob := range blobs { h := xxhash.Sum64(blob) hashes[i], m[h] = h, struct{}{} } s.blobMu.Lock() s.openChunkTransactions = append(s.openChunkTransactions, m) if l := len(s.blobs); l > 4096 { s.blobMu.Unlock() s.conf.Log.Error("too many blobs pending", "n", l) return } for i := range hashes { s.blobs[hashes[i]] = blobs[i] } s.blobMu.Unlock() // Length of 1 byte for the border block count. raw := bytes.NewBuffer(make([]byte, 1, 32)) enc := nbt.NewEncoderWithEncoding(raw, nbt.NetworkLittleEndian) for bp, b := range blockEntities { if n, ok := b.(world.NBTer); ok { d := n.EncodeNBT() d["x"], d["y"], d["z"] = int32(bp[0]), int32(bp[1]), int32(bp[2]) _ = enc.Encode(d) } } s.writePacket(&packet.LevelChunk{ Dimension: s.dimensionID(dim), Position: protocol.ChunkPos{pos.X(), pos.Z()}, SubChunkCount: count, CacheEnabled: true, BlobHashes: hashes, RawPayload: raw.Bytes(), }) } // sendNetworkChunk sends a network encoded chunk to the client. func (s *Session) sendNetworkChunk(pos world.ChunkPos, dim world.Dimension, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block) { if subChunkRequests { s.writePacket(&packet.LevelChunk{ Dimension: s.dimensionID(dim), SubChunkCount: protocol.SubChunkRequestModeLimited, Position: protocol.ChunkPos(pos), HighestSubChunk: c.HighestFilledSubChunk(), RawPayload: append(chunk.EncodeBiomes(c, chunk.NetworkEncoding), 0), }) return } data := chunk.Encode(c, chunk.NetworkEncoding) chunkBuf := bytes.NewBuffer(nil) for _, s := range data.SubChunks { _, _ = chunkBuf.Write(s) } _, _ = chunkBuf.Write(data.Biomes) // Length of 1 byte for the border block count. chunkBuf.WriteByte(0) enc := nbt.NewEncoderWithEncoding(chunkBuf, nbt.NetworkLittleEndian) for bp, b := range blockEntities { if n, ok := b.(world.NBTer); ok { d := n.EncodeNBT() d["x"], d["y"], d["z"] = int32(bp[0]), int32(bp[1]), int32(bp[2]) _ = enc.Encode(d) } } s.writePacket(&packet.LevelChunk{ Dimension: s.dimensionID(dim), Position: protocol.ChunkPos{pos.X(), pos.Z()}, SubChunkCount: uint32(len(data.SubChunks)), RawPayload: append([]byte(nil), chunkBuf.Bytes()...), }) } // trackBlob attempts to track the given blob. If the player has too many pending blobs, it returns false and closes the // connection. func (s *Session) trackBlob(hash uint64, blob []byte) bool { s.blobMu.Lock() if l := len(s.blobs); l > 4096 { s.blobMu.Unlock() s.conf.Log.Error("too many blobs pending", "n", l) return false } s.blobs[hash] = blob s.blobMu.Unlock() return true }