package net.glowstone.chunk; import javax.annotation.Nullable; import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.ints.IntListIterator; import com.flowpowered.network.util.ByteBufUtils; import net.glowstone.util.NibbleArray; import net.glowstone.util.VariableValueArray; import net.glowstone.util.nbt.CompoundTag; /** * A single cubic section of a chunk, with all data. */ public final class ChunkSection { /** * The number of blocks in a chunk section, and thus the number of elements * in all arrays used for it. */ public static final int ARRAY_SIZE = GlowChunk.WIDTH * GlowChunk.HEIGHT * GlowChunk.SEC_DEPTH; /** * Block and sky light levels to use for empty chunk sections. */ public static final byte EMPTY_BLOCK_LIGHT = 0, EMPTY_SKYLIGHT = 0; /** * The default values for block and sky light, used on new chunk sections. */ public static final byte DEFAULT_BLOCK_LIGHT = 0, DEFAULT_SKYLIGHT = 0xF; /** * The number of bits per block used in the global palette. */ public static final int GLOBAL_PALETTE_BITS_PER_BLOCK = 13; /** * The palette */ @Nullable private IntList palette; private VariableValueArray data; /** * The block light and sky light arrays. These arrays are always set, even * in dimensions without skylight. */ private NibbleArray skyLight, blockLight; /** * The number of non-air blocks in this section, used to determine whether * it is empty. */ private int count; /** * Create a new, empty ChunkSection. */ public ChunkSection() { this(new char[ARRAY_SIZE]); } /** * Create a new, unlit chunk section with the specified chunk data. This * ChunkSection assumes ownership of the arrays passed in, and they should * not be further modified. * * @param types An array of block state IDs for this chunk section (containing type and metadata) */ public ChunkSection(char[] types) { this(types, new NibbleArray(ARRAY_SIZE, DEFAULT_SKYLIGHT), new NibbleArray(ARRAY_SIZE, DEFAULT_BLOCK_LIGHT)); } /** * Create a ChunkSection with the specified chunk data. This * ChunkSection assumes ownership of the arrays passed in, and they * should not be further modified. * * @param types An array of block types for this chunk section. * @param skyLight An array for skylight data for this chunk section. * @param blockLight An array for blocklight data for this chunk section. */ public ChunkSection(char[] types, NibbleArray skyLight, NibbleArray blockLight) { if (types.length != ARRAY_SIZE || skyLight.size() != ARRAY_SIZE || blockLight.size() != ARRAY_SIZE) { throw new IllegalArgumentException("An array length was not " + ARRAY_SIZE + ": " + types.length + " " + skyLight.size() + " " + blockLight.size()); } this.skyLight = skyLight; this.blockLight = blockLight; loadTypeArray(types); } /** * Create a ChunkSection with the specified chunk data. This * ChunkSection assumes ownership of the arrays passed in, and they * should not be further modified. * * @param data An array of blocks in this section. * @param palette The palette that is associated with that data. If null, the global palette is used. * @param skyLight An array for skylight data for this chunk section. * @param blockLight An array for blocklight data for this chunk section. */ public ChunkSection(VariableValueArray data, @Nullable IntList palette, NibbleArray skyLight, NibbleArray blockLight) { if (data.getCapacity() != ARRAY_SIZE || skyLight.size() != ARRAY_SIZE || blockLight.size() != ARRAY_SIZE) { throw new IllegalArgumentException("An array length was not " + ARRAY_SIZE + ": " + data.getCapacity() + " " + skyLight.size() + " " + blockLight.size()); } if (palette == null) { if (data.getBitsPerValue() != GLOBAL_PALETTE_BITS_PER_BLOCK) { throw new IllegalArgumentException("Must use " + GLOBAL_PALETTE_BITS_PER_BLOCK + " bits per block when palette is null (using global palette); got " + data.getBitsPerValue()); } } else { if (data.getBitsPerValue() < 4 || data.getBitsPerValue() > 8) { throw new IllegalArgumentException("Bits per block must be between 4 and 8 (inclusive) when using a section palette; got " + data.getBitsPerValue()); } } this.data = data; this.palette = palette; this.skyLight = skyLight; this.blockLight = blockLight; } /** * Creates a new unlit chunk section containing the given types. * * @param types An array of block IDs, with metadata * @return A matching chunk section. */ public static ChunkSection fromStateArray(short[] types) { if (types.length != ARRAY_SIZE) { throw new IllegalArgumentException("Types array length was not " + ARRAY_SIZE + ": " + types.length); } char[] charTypes = new char[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { charTypes[i] = (char) (types[i]); } return new ChunkSection(charTypes); } /** * Creates a new unlit chunk section containing the given types. * * @param types An array of block IDs, without metadata. * @return A matching chunk section. */ public static ChunkSection fromIdArray(short[] types) { if (types.length != ARRAY_SIZE) { throw new IllegalArgumentException("Types array length was not " + ARRAY_SIZE + ": " + types.length); } char[] charTypes = new char[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { charTypes[i] = (char) (types[i] << 4); } return new ChunkSection(charTypes); } /** * Creates a new unlit chunk section containing the given types. * * @param types An array of block IDs, without metadata. * @return A matching chunk section. */ public static ChunkSection fromIdArray(byte[] types) { if (types.length != ARRAY_SIZE) { throw new IllegalArgumentException("Types array length was not " + ARRAY_SIZE + ": " + types.length); } char[] charTypes = new char[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { charTypes[i] = (char) (types[i] << 4); } return new ChunkSection(charTypes); } /** * Creates a new chunk section from the given NBT blob. * * @param sectionTag The tag to read from * @return The section */ public static ChunkSection fromNBT(CompoundTag sectionTag) { byte[] rawTypes = sectionTag.getByteArray("Blocks"); NibbleArray extTypes = sectionTag.containsKey("Add") ? new NibbleArray(sectionTag.getByteArray("Add")) : null; NibbleArray data = new NibbleArray(sectionTag.getByteArray("Data")); NibbleArray blockLight = new NibbleArray(sectionTag.getByteArray("BlockLight")); NibbleArray skyLight = new NibbleArray(sectionTag.getByteArray("SkyLight")); char[] types = new char[rawTypes.length]; for (int i = 0; i < rawTypes.length; i++) { types[i] = (char) ((extTypes == null ? 0 : extTypes.get(i)) << 12 | (rawTypes[i] & 0xff) << 4 | data.get(i)); } return new ChunkSection(types, skyLight, blockLight); } /** * Calculate the index into internal arrays for the given coordinates. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * * @return The index. */ public int index(int x, int y, int z) { if (x < 0 || z < 0 || x >= GlowChunk.WIDTH || z >= GlowChunk.HEIGHT) { throw new IndexOutOfBoundsException("Coords (x=" + x + ",z=" + z + ") out of section bounds"); } return (y & 0xf) << 8 | z << 4 | x; } /** * Loads the contents of this chunk section from the given type array, * initializing the palette. * * @param type The type array. */ public void loadTypeArray(char[] types) { if (types.length != ARRAY_SIZE) { throw new IllegalArgumentException("Types array length was not " + ARRAY_SIZE + ": " + types.length); } // Build the palette, and the count this.count = 0; this.palette = new IntArrayList(); for (char type : types) { if (type != 0) { count++; } if (!palette.contains(type)) { palette.add(type); } } // Now that we've built a palette, build the list int bitsPerBlock = VariableValueArray.calculateNeededBits(palette.size()); if (bitsPerBlock < 4) { bitsPerBlock = 4; } else if (bitsPerBlock > 8) { palette = null; bitsPerBlock = GLOBAL_PALETTE_BITS_PER_BLOCK; } this.data = new VariableValueArray(bitsPerBlock, ARRAY_SIZE); for (int i = 0; i < ARRAY_SIZE; i++) { if (palette != null) { data.set(i, palette.indexOf(types[i])); } else { data.set(i, types[i]); } } } /** * Optimizes this chunk section, removing unneeded palette entries and * recounting non-air blocks. This is an expensive operation, but * occasionally performing it will improve sending the section. */ public void optimize() { loadTypeArray(getTypes()); } /** * Recount the amount of non-air blocks in the chunk section. */ public void recount() { count = 0; for (int i = 0; i < ARRAY_SIZE; i++) { int type = data.get(i); if (palette != null) { type = palette.getInt(type); } if (type != 0) { count++; } } } /** * Take a snapshot of this section which will not reflect future changes. * * @return The snapshot for this section. */ public ChunkSection snapshot() { return new ChunkSection(data.clone(), palette == null ? null : new IntArrayList(palette), skyLight.snapshot(), blockLight.snapshot()); } /** * Gets the type at the given coordinates. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * * @return A type ID */ public char getType(int x, int y, int z) { int value = data.get(index(x, y, z)); if (palette != null) { value = palette.getInt(value); } return (char) value; } /** * Sets the type at the given coordinates. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * @param value The new type ID for that coordinate. */ public void setType(int x, int y, int z, char value) { int oldType = getType(x, y, z); if (oldType != 0) { count--; } if (value != 0) { count++; } int encoded; if (palette != null) { encoded = palette.indexOf(value); if (encoded == -1) { encoded = palette.size(); palette.add(value); if (encoded > data.getLargestPossibleValue()) { // This is the situation where it can become expensive: // resize the array if (data.getBitsPerValue() == 8) { data = data.increaseBitsPerValueTo(GLOBAL_PALETTE_BITS_PER_BLOCK); // No longer using the global palette; need to manually // recalculate for (int i = 0; i < ARRAY_SIZE; i++) { int oldValue = data.get(i); int newValue = palette.getInt(oldValue); data.set(i, newValue); } palette = null; encoded = value; } else { // Using the global palette: automatically resize data = data.increaseBitsPerValueTo(data.getBitsPerValue() + 1); } } } } else { encoded = value; } data.set(index(x, y, z), encoded); } /** * Returns the block type array. Do not modify this array. * * @return The block type array. */ public char[] getTypes() { char[] types = new char[ARRAY_SIZE]; for (int i = 0; i < ARRAY_SIZE; i++) { int type = data.get(i); if (palette != null) { type = palette.getInt(type); } types[i] = (char) type; } return types; } /** * Gets the block light at the given block. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * @return The block light at the given coordinates. */ public byte getBlockLight(int x, int y, int z) { return blockLight.get(index(x, y, z)); } /** * Sets the block light at the given block. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * @param light The new light level. */ public void setBlockLight(int x, int y, int z, byte value) { blockLight.set(index(x, y, z), value); } /** * Gets the block light array. * * @return The block light array. */ public NibbleArray getBlockLight() { return blockLight; } /** * Gets the sky light at the given block. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * @return The sky light at the given coordinates. */ public byte getSkyLight(int x, int y, int z) { return skyLight.get(index(x, y, z)); } /** * Sets the sky light at the given block. * * @param x The x coordinate, for east and west. * @param y The y coordinate, for up and down. * @param z The z coordinate, for north and south. * @param light The new light level. */ public void setSkyLight(int x, int y, int z, byte value) { skyLight.set(index(x, y, z), value); } /** * Gets the sky light array. * * @return The sky light array. If the dimension of this chunk section's * chunk's world is not the overworld, this array contains only * maximum light levels. */ public NibbleArray getSkyLight() { return skyLight; } /** * Is this chunk section empty, IE doesn't need to be sent or saved? * * @return True if this chunk section is empty and can be removed. * @implNote This implementation has the same issue that causes <a * href="https://bugs.mojang.com/browse/MC-80966">MC-80966</a>: It * assumes that a chunk section with only air blocks has no meaningful * data. This assumption is incorrect for sections near light * sources, which can create lighting bugs. However, it is more * expensive to send additional sections with just light data. */ public boolean isEmpty() { return count == 0; } /** * Writes this chunk section to the given ByteBuf. * @param buf The buffer to write to. * @param skylight True if skylight should be included. * @throws IllegalStateException If this chunk section {@linkplain #isEmpty() is empty} */ public void writeToBuf(ByteBuf buf, boolean skylight) throws IllegalStateException { if (this.isEmpty()) { throw new IllegalStateException("Can't write empty sections"); } buf.writeByte(data.getBitsPerValue()); // Bit per value -> varies if (palette == null) { ByteBufUtils.writeVarInt(buf, 0); // Palette size -> 0 -> Use the global palette } else { ByteBufUtils.writeVarInt(buf, palette.size()); // Palette size // Foreach loops can't be used due to autoboxing IntListIterator itr = palette.iterator(); while (itr.hasNext()) { ByteBufUtils.writeVarInt(buf, itr.nextInt()); // The palette entry } } long[] backing = data.getBacking(); ByteBufUtils.writeVarInt(buf, backing.length); buf.ensureWritable(backing.length * 8 + blockLight.byteSize() + (skylight ? skyLight.byteSize() : 0)); for (long value : backing) { buf.writeLong(value); } buf.writeBytes(blockLight.getRawData()); if (skylight) { buf.writeBytes(skyLight.getRawData()); } } /** * Writes this chunk section to a NBT compound. Note that the Y coordinate * is not written. * * @param sectionTag The tag to write to */ public void writeToNBT(CompoundTag sectionTag) { char[] types = this.getTypes(); byte[] rawTypes = new byte[ChunkSection.ARRAY_SIZE]; NibbleArray extTypes = null; NibbleArray data = new NibbleArray(ChunkSection.ARRAY_SIZE); for (int j = 0; j < ChunkSection.ARRAY_SIZE; j++) { char type = types[j]; rawTypes[j] = (byte) (type >> 4 & 0xFF); byte extType = (byte) (type >> 12); if (extType > 0) { if (extTypes == null) { extTypes = new NibbleArray(ChunkSection.ARRAY_SIZE); } extTypes.set(j, extType); } data.set(j, (byte) (type & 0xF)); } sectionTag.putByteArray("Blocks", rawTypes); if (extTypes != null) { sectionTag.putByteArray("Add", extTypes.getRawData()); } sectionTag.putByteArray("Data", data.getRawData()); sectionTag.putByteArray("BlockLight", blockLight.getRawData()); sectionTag.putByteArray("SkyLight", skyLight.getRawData()); } }