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;
}
}