diff --git a/protocol/src/main/java/mc/protocol/packets/play/server/ChunkDataPacket.java b/protocol/src/main/java/mc/protocol/packets/play/server/ChunkDataPacket.java index 89af3dc..fa4370a 100644 --- a/protocol/src/main/java/mc/protocol/packets/play/server/ChunkDataPacket.java +++ b/protocol/src/main/java/mc/protocol/packets/play/server/ChunkDataPacket.java @@ -2,15 +2,22 @@ package mc.protocol.packets.play.server; import io.netty.buffer.Unpooled; import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import mc.protocol.buffer.NetByteBuf; import mc.protocol.packets.ServerSidePacket; -import mc.protocol.pool.ObjectPool; import mc.protocol.pool.ProtocolObjectPool; +import mc.protocol.utils.NibbleArray; +import mc.protocol.utils.PaletteChunkSection; +import mc.protocol.utils.PaletteChunkSection.PaletteCoords; +import mc.protocol.world.Block; +import mc.protocol.world.Chunk; +import mc.protocol.world.ChunkSection; /** * Данные чанка. * - *

Структура пакета

+ *

Структура пакета

*
  * | FIELD                    | TYPE         | NOTES                                                                              |
  * |--------------------------|------------- |------------------------------------------------------------------------------------|
@@ -19,101 +26,229 @@ import mc.protocol.pool.ProtocolObjectPool;
  * | Is Full chunk            | Boolean      | См. Chunk Format                                                                   |
  * | Available Sections       | VarInt       | Битовая маска, где каждый бит - это часть чанка (0-15)                             |
  * | Size of Data             | VarInt       | Размер поля "Data"                                                                 |
- * | Data                     | Byte array   | Данные чанка. См. Chunk Format                                                     |
+ * | Data                     | Byte array   | Данные чанка. См. Data Structure                                                   |
  * | Number of block entities | VarInt       | Количество элементов в поле "Block entities"                                       |
  * | Block entities           | Array of NBT | Все сущности в чанке                                                               |
  * 
* + *

Data Structure

+ *
+ * | FIELD  | TYPE                   | NOTES                                                      |
+ * |--------|------------------------|------------------------------------------------------------|
+ * | Data   | Array of Chunk Section | См. Chunk Section Structure                                |
+ * | Biomes | Byte array             | Optional. Отправляются только если "Is Full chunk" == true |
+ * 
+ * + *

Chunk Section Structure

+ *
+ * | FIELD             | TYPE          | NOTES                                                               |
+ * |-------------------|---------------|---------------------------------------------------------------------|
+ * | Bits Per Block    | Unsigned Byte | Определяет, сколько битов используется для кодирования блока        |
+ * | Palette           | Byte array    | См. Palette Structure                                               |
+ * | Data Array Length | VarInt        |                                                                     |
+ * | Data Array        | Array of Long |                                                                     |
+ * | Block Light       | Byte array    | Половина байна на блок                                              |
+ * | Sky Light         | Byte array    | Optional. Только для LevelType == Overworld. Половина байна на блок |
+ * 
+ * + *

Palette Structure

+ *

Есть два типа: Indirect и Direct.

+ *

+ * Indirect используется, если "Bits Per Block" < 9. При этом, если "Bits Per Block" <= 4, + * то должно использоваться значение 4. + *

+ *

Для Indirect формат следующий

+ *
+ * | FIELD          | TYPE            | NOTES                          |
+ * |----------------|-----------------|--------------------------------|
+ * | Palette Length | VarInt          | Количество элементов в массиве |
+ * | Palette        | Array of VarInt | Идентификаторы блоков          |
+ * 
+ * + *

Direct используется, если "Bits Per Block" >= 9

+ *

Для Direct формат следующий

+ *
+ * | FIELD                | TYPE   | NOTES       |
+ * |----------------------|--------|-------------|
+ * | Dummy Palette Length | VarInt | Всегда == 0 |
+ * 
+ * * @see Chunk Data * @see Chunk Format */ @Data public class ChunkDataPacket implements ServerSidePacket { - private static NetByteBuf voidData; + private static final int FULL_BIT_MASK = 0b11111111_11111111; + private static final int _16_16_16 = 16 * 16 * 16; + private static final int SIZE_OF_LONG_IN_BITS = 64; - private int x; - private int z; + private Chunk chunk; - @SuppressWarnings("java:S125") @Override public void writeSelf(NetByteBuf netByteBuf) { - netByteBuf.writeInt(x); - netByteBuf.writeInt(z); - /* Временное отключение кода - netByteBuf.writeBoolean(true); // Is Full chunk - netByteBuf.writeVarInt(0b11111111); // Available Sections + netByteBuf.writeInt(chunk.getX()); // Chunk X + netByteBuf.writeInt(chunk.getZ()); // Chunk Z - NetByteBuf data = new NetByteBuf(Unpooled.buffer()); - // - for (int i = 0; i < 16; i++) { - NetByteBuf dataBuff = new NetByteBuf(Unpooled.wrappedBuffer(new byte[4096])); - NetByteBuf blockLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048])); - NetByteBuf skyLight = new NetByteBuf(Unpooled.wrappedBuffer(new byte[2048])); - NetByteBuf biomes = new NetByteBuf(Unpooled.wrappedBuffer(new byte[256])); - - // - data.writeUnsignedByte(13); // Bits Per Block - // - data.writeUnsignedByte(0); // Palette Length (for direct) - // - // - data.writeVarInt(dataBuff.readableBytes()); // Data Array Length - data.writeBytes(dataBuff); // Data Array - data.writeBytes(blockLight); // Block Light - data.writeBytes(skyLight); // Sky Light - // - data.writeBytes(biomes); // Biomes - } - // + AvailableSections availableSections = createAvailableSections(); + boolean fullChunk = availableSections.getBitMask() == FULL_BIT_MASK; + netByteBuf.writeBoolean(fullChunk); // Is Full chunk + netByteBuf.writeVarInt(availableSections.getBitMask()); // Available Sections + NetByteBuf data = createDataStructure(availableSections.getMaxHeight(), fullChunk); netByteBuf.writeVarInt(data.readableBytes()); // Size of Data netByteBuf.writeBytes(data); // Data + netByteBuf.writeVarInt(0); // Number of block entities - // write NBT's - */ - - netByteBuf.writeBytes(voidData); - - voidData.resetReaderIndex(); - voidData.resetWriterIndex(); + // Block entities (NBT's) } - static { - ObjectPool pool = ProtocolObjectPool.getNetByteBufPool(); - - voidData = pool.borrowObject().setByteBuf(Unpooled.buffer()); - voidData.writeBoolean(true); // Is Full chunk - voidData.writeVarInt(0b11111111); // Available Sections - - NetByteBuf data = pool.borrowObject().setByteBuf(Unpooled.buffer()); - for (int i = 0; i < 16; i++) { - NetByteBuf dataBuff = pool.borrowObject().setByteBuf(Unpooled.wrappedBuffer(new byte[4096])); - NetByteBuf blockLight = pool.borrowObject().setByteBuf(Unpooled.wrappedBuffer(new byte[2048])); - NetByteBuf skyLight = pool.borrowObject().setByteBuf(Unpooled.wrappedBuffer(new byte[2048])); - NetByteBuf biomes = pool.borrowObject().setByteBuf(Unpooled.wrappedBuffer(new byte[256])); - - data.writeUnsignedByte(13); - data.writeUnsignedByte(0); - data.writeVarInt(dataBuff.readableBytes()); - data.writeBytes(dataBuff); - data.writeBytes(blockLight); - data.writeBytes(skyLight); - data.writeBytes(biomes); - - pool.returnObject(biomes); - pool.returnObject(skyLight); - pool.returnObject(blockLight); - pool.returnObject(dataBuff); + private AvailableSections createAvailableSections() { + int bitMask = 0; + int maxH = 0; + for (int h = 15; h >= 0; h--) { + bitMask = bitMask << 1; + ChunkSection chunkSection = chunk.getSection(h); + if (chunkSection != null && chunkSection.getY() == h) { + bitMask |= 0x01; + maxH++; + } else { + bitMask |= 0x00; + } } - voidData.writeVarInt(data.readableBytes()); - voidData.writeBytes(data); - voidData.writeVarInt(0); + return new AvailableSections(bitMask, maxH); + } - voidData.markReaderIndex(); - voidData.markWriterIndex(); + private NetByteBuf createDataStructure(int maxHeight, boolean fillBiomes) { +// NetByteBuf dataStructure = ProtocolObjectPool.getNetByteBufPool().borrowObject().setByteBuf(Unpooled.buffer()); + NetByteBuf dataStructure = new NetByteBuf().setByteBuf(Unpooled.buffer()); +// NetByteBuf biomes = fillBiomes ? ProtocolObjectPool.getNetByteBufPool().borrowObject().setByteBuf(Unpooled.buffer()) : null; + NetByteBuf biomes = fillBiomes ? new NetByteBuf().setByteBuf(Unpooled.buffer()) : null; - pool.returnObject(data); + for (int h = 0; h < maxHeight; h++) { + ChunkSection section = chunk.getSection(h); + if (section == null) { + continue; + } + + dataStructure.writeBytes(createData(section, biomes)); // Data + } + + if (fillBiomes) { + dataStructure.writeBytes(biomes); // Biomes + } + + return dataStructure; + } + + private NetByteBuf createData(ChunkSection section, NetByteBuf biomes) { +// NetByteBuf data = ProtocolObjectPool.getNetByteBufPool().borrowObject().setByteBuf(Unpooled.buffer()); + NetByteBuf data = new NetByteBuf().setByteBuf(Unpooled.buffer()); + + PaletteChunkSection paletteSection = new PaletteChunkSection(); + NibbleArray blockLight = new NibbleArray(); + NibbleArray skyLight = new NibbleArray(); + fillPalette(section, paletteSection, blockLight, skyLight); + + // + int bitsPerBlock = paletteSection.bitsPerBlock(); + data.writeUnsignedByte(bitsPerBlock); + // + + // + paletteSection.writePalette(data); + // + + // + int dataLength = (_16_16_16 * bitsPerBlock) / SIZE_OF_LONG_IN_BITS; + data.writeVarInt(dataLength); + // + + // + //TODO алгоритм побитовой записи в long вынести в utils + // Возможно даже пораднив с NibbleArray + int lastPos = 0; + long value = 0; + boolean fairy = false; + long fairyValue = 0; + boolean writeBiomes = biomes != null; + + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + //@formatter:off + int blockNumber = (((y << 4) + z) << 4) + x; + int startLong = ( blockNumber * bitsPerBlock ) / SIZE_OF_LONG_IN_BITS; + int startOffset = ( blockNumber * bitsPerBlock ) % SIZE_OF_LONG_IN_BITS; + int endLong = ((blockNumber + 1) * bitsPerBlock - 1) / SIZE_OF_LONG_IN_BITS; + //@formatter:on + + long idxBlockInPalette = paletteSection.getIndexBlockInPalette(x, y, z); + + if (startLong != lastPos) { + data.writeLong(value); + lastPos = startLong; + if (fairy) { + value = fairyValue; + fairy = false; + } else { + value = 0; + } + } + + value |= (idxBlockInPalette << startOffset); + + if (startLong != endLong) { + fairyValue = idxBlockInPalette >> (SIZE_OF_LONG_IN_BITS - startOffset); + fairy = true; + } + + if (writeBiomes) { + biomes.writeByte(chunk.getBiome( + (chunk.getX() << 4) + x, + (chunk.getZ() << 4) + z)); + + if (x == 15 && z == 15) { + writeBiomes = false; + } + } + } + } + } + data.writeLong(value); + // + + // + data.writeBytes(blockLight.getRawData()); + // + + // + data.writeBytes(skyLight.getRawData()); + // + + return data; + } + + private void fillPalette(ChunkSection section, PaletteChunkSection paletteSection, NibbleArray blockLight, NibbleArray skyLight) { + for (int y = 0; y < 16; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + Block block = section.getBlock(x, y, z); + PaletteCoords paletteCoords = PaletteCoords.createByBlock(block); + + paletteSection.addBlock(x, y, z, block); + blockLight.set(paletteCoords.getX(), paletteCoords.getY(), paletteCoords.getZ(), block.getLight()); + skyLight.set(paletteCoords.getX(), paletteCoords.getY(), paletteCoords.getZ(), section.getSkyLight(x, y, z)); + } + } + } + } + + @RequiredArgsConstructor + @Getter + private static class AvailableSections { + private final int bitMask; + private final int maxHeight; } } diff --git a/protocol/src/main/java/mc/protocol/utils/NibbleArray.java b/protocol/src/main/java/mc/protocol/utils/NibbleArray.java new file mode 100644 index 0000000..20acdb6 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/utils/NibbleArray.java @@ -0,0 +1,56 @@ +package mc.protocol.utils; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class NibbleArray { + + private final byte[] data; + + public NibbleArray(int capacity) { + this.data = new byte[capacity]; + } + + public NibbleArray() { + this(2048); + } + + public int get(int x, int y, int z) { + int idx = coordsToIndex(x, y, z); + + int ni = nibbleIndex(idx); + return isLowerNibble(idx) ? this.data[ni] & 0x0F : this.data[ni] >> 4 & 0x0F; + } + + public void set(int x, int y, int z, int value) { + //@formatter:off + if (value < 0) value = 0; + else if (value > 15) value = 15; + //@formatter:on + + int idx = coordsToIndex(x, y, z); + int ni = nibbleIndex(idx); + + if (isLowerNibble(idx)) { + this.data[ni] = (byte) (value); + } else { + this.data[ni] = (byte) (this.data[ni] | value << 4); + } + } + + public byte[] getRawData() { + return data; + } + + private int coordsToIndex(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private int nibbleIndex(int index) { + return index >> 1; + } + + private boolean isLowerNibble(int index) { + return (index & 1) == 0; + } +} diff --git a/protocol/src/main/java/mc/protocol/utils/PaletteChunkSection.java b/protocol/src/main/java/mc/protocol/utils/PaletteChunkSection.java new file mode 100644 index 0000000..2638f32 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/utils/PaletteChunkSection.java @@ -0,0 +1,77 @@ +package mc.protocol.utils; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.protocol.buffer.NetByteBuf; +import mc.protocol.world.Block; + +import java.util.ArrayList; +import java.util.List; + +public class PaletteChunkSection { + + private final byte[] blocks = new byte[4096]; + private final List palette = new ArrayList<>(); + + public void addBlock(int x, int y, int z, Block block) { + blocks[coordsToIndex(x, y, z)] = putPalette(block); + } + + public int bitsPerBlock() { + if (palette.size() <= 15) { + return 4; + } else if (palette.size() <= 31) { + return 5; + } else if (palette.size() <= 63) { + return 6; + } else if (palette.size() <= 127) { + return 7; + } else if (palette.size() <= 255) { + return 8; + } else { + return 13; + } + } + + public void writePalette(NetByteBuf netByteBuf) { + netByteBuf.writeVarInt(palette.size()); // Size of palette + palette.forEach(netByteBuf::writeVarInt); // Palette + } + + public long getIndexBlockInPalette(int x, int y, int z) { + return blocks[coordsToIndex(x, y, z)]; + } + + private int coordsToIndex(int x, int y, int z) { + return y << 8 | z << 4 | x; + } + + private byte putPalette(Block block) { + int blockState = (block.getId() << 4) | block.getMeta(); + + int idx = palette.indexOf(blockState); + if (idx == -1) { + palette.add(blockState); + idx = palette.size() - 1; + } + + return (byte) idx; + } + + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + @Getter + public static class PaletteCoords { + private final int x; + private final int y; + private final int z; + + public static PaletteCoords createByBlock(Block block) { + int bx = (int) block.getLocation().getX() - (((int) block.getLocation().getX() >> 4) << 4); + int by = (int) block.getLocation().getY() - (((int) block.getLocation().getY() >> 4) << 4); + int bz = (int) block.getLocation().getZ() - (((int) block.getLocation().getZ() >> 4) << 4); + + return new PaletteCoords(bx, by, bz); + } + } +} diff --git a/protocol/src/main/java/mc/protocol/world/Block.java b/protocol/src/main/java/mc/protocol/world/Block.java new file mode 100644 index 0000000..91717cc --- /dev/null +++ b/protocol/src/main/java/mc/protocol/world/Block.java @@ -0,0 +1,14 @@ +package mc.protocol.world; + +import mc.protocol.model.Location; + +public interface Block { + + int getId(); + int getMeta(); + + Location getLocation(); + + int getLight(); + void setLight(int value); +} diff --git a/protocol/src/main/java/mc/protocol/world/Chunk.java b/protocol/src/main/java/mc/protocol/world/Chunk.java index ec4a551..10eff95 100644 --- a/protocol/src/main/java/mc/protocol/world/Chunk.java +++ b/protocol/src/main/java/mc/protocol/world/Chunk.java @@ -4,4 +4,7 @@ public interface Chunk { int getX(); int getZ(); + + ChunkSection getSection(int index); + byte getBiome(int x, int z); } diff --git a/protocol/src/main/java/mc/protocol/world/ChunkSection.java b/protocol/src/main/java/mc/protocol/world/ChunkSection.java new file mode 100644 index 0000000..8b04966 --- /dev/null +++ b/protocol/src/main/java/mc/protocol/world/ChunkSection.java @@ -0,0 +1,9 @@ +package mc.protocol.world; + +public interface ChunkSection { + + int getY(); + + Block getBlock(int x, int y, int z); + int getSkyLight(int x, int y, int z); +} diff --git a/server/src/main/java/mc/server/processor/ProcessorLogin.java b/server/src/main/java/mc/server/processor/ProcessorLogin.java index f57ff53..980bebe 100644 --- a/server/src/main/java/mc/server/processor/ProcessorLogin.java +++ b/server/src/main/java/mc/server/processor/ProcessorLogin.java @@ -99,8 +99,7 @@ public class ProcessorLogin implements PacketProcessor { Chunk chunk = world.getChunk((int) chunkLocation.getX(), (int) chunkLocation.getZ()); var chunkDataPacket = new ChunkDataPacket(); - chunkDataPacket.setX(chunk.getX()); - chunkDataPacket.setZ(chunk.getZ()); + chunkDataPacket.setChunk(chunk); player.getCtx().write(chunkDataPacket); @@ -113,8 +112,7 @@ public class ProcessorLogin implements PacketProcessor { for (int x = minX; x <= maxX; x++) { if ((z == minZ || z == maxZ) || (x == minX || x == maxX)) { chunkDataPacket = new ChunkDataPacket(); - chunkDataPacket.setX(x); - chunkDataPacket.setZ(z); + chunkDataPacket.setChunk(world.getChunk(x, z)); player.getCtx().write(chunkDataPacket); } } diff --git a/server/src/main/java/mc/server/world/AirBlock.java b/server/src/main/java/mc/server/world/AirBlock.java new file mode 100644 index 0000000..1bb6dd7 --- /dev/null +++ b/server/src/main/java/mc/server/world/AirBlock.java @@ -0,0 +1,33 @@ +package mc.server.world; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.protocol.model.Location; +import mc.protocol.world.Block; + +@RequiredArgsConstructor +public class AirBlock implements Block { + + @Getter + private final Location location; + + @Override + public int getId() { + return 0; + } + + @Override + public int getMeta() { + return 0; + } + + @Override + public int getLight() { + return 0; + } + + @Override + public void setLight(int value) { + throw new UnsupportedOperationException(); + } +} diff --git a/server/src/main/java/mc/server/world/VoidChunk.java b/server/src/main/java/mc/server/world/VoidChunk.java index 2aefcaf..303ecc6 100644 --- a/server/src/main/java/mc/server/world/VoidChunk.java +++ b/server/src/main/java/mc/server/world/VoidChunk.java @@ -2,10 +2,25 @@ package mc.server.world; import lombok.Data; import mc.protocol.world.Chunk; +import mc.protocol.world.ChunkSection; + +import java.util.HashMap; +import java.util.Map; @Data public class VoidChunk implements Chunk { private final int x; private final int z; + private final Map sections = new HashMap<>(); + + @Override + public ChunkSection getSection(int height) { + return sections.computeIfAbsent(height, VoidChunkSection::new); + } + + @Override + public byte getBiome(int x, int z) { + return 127; // 127 | 7F | minecraft:void | The Void + } } diff --git a/server/src/main/java/mc/server/world/VoidChunkSection.java b/server/src/main/java/mc/server/world/VoidChunkSection.java new file mode 100644 index 0000000..bc0cdf2 --- /dev/null +++ b/server/src/main/java/mc/server/world/VoidChunkSection.java @@ -0,0 +1,29 @@ +package mc.server.world; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import mc.protocol.model.Location; +import mc.protocol.world.Block; +import mc.protocol.world.ChunkSection; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +@Getter +public class VoidChunkSection implements ChunkSection { + + private final int y; + private final Map blocks = new HashMap<>(); + + @Override + public Block getBlock(int x, int y, int z) { + return blocks.computeIfAbsent(new Location().set(x, y, z), AirBlock::new); + } + + @Override + public int getSkyLight(int x, int y, int z) { + return 0; + } + +} \ No newline at end of file diff --git a/server/src/main/java/mc/server/world/VoidWorld.java b/server/src/main/java/mc/server/world/VoidWorld.java index 2331416..5bdffd4 100644 --- a/server/src/main/java/mc/server/world/VoidWorld.java +++ b/server/src/main/java/mc/server/world/VoidWorld.java @@ -1,13 +1,16 @@ package mc.server.world; import mc.protocol.model.Location; +import mc.protocol.pool.ProtocolObjectPool; import mc.protocol.utils.LevelType; +import mc.protocol.utils.Table; import mc.protocol.world.Chunk; import mc.protocol.world.World; public class VoidWorld implements World { - private static final Location spawn = new Location().set(7d, 130d, 7d); + private static final Location spawn = ProtocolObjectPool.getLocationPool().borrowObject().set(7d, 130d, 7d); + private final Table chunkTable = new Table<>(); @Override public LevelType getLevelType() { @@ -21,6 +24,12 @@ public class VoidWorld implements World { @Override public Chunk getChunk(int x, int z) { - return new VoidChunk(x, z); + VoidChunk chunk = chunkTable.getColumnAndRow(x, z); + if (chunk == null) { + chunk = new VoidChunk(x, z); + chunkTable.put(x, z, chunk); + } + + return chunk; } }