@Override
  protected void freeChunk(Point p) {
    int x = (int) p.getX() >> Chunk.BLOCKS.BITS;
    int y = (int) p.getY() >> Chunk.BLOCKS.BITS; // + SEALEVEL_CHUNK;
    int z = (int) p.getZ() >> Chunk.BLOCKS.BITS;

    if (y < 0 || y >= p.getWorld().getHeight() >> Chunk.BLOCKS.BITS) {
      return;
    }

    TIntSet column = initializedChunks.get(x, z);
    CompressedChunkMessage CCMsg;
    if (column != null) {
      column.remove(y);
      if (column.isEmpty()) {
        if (initializedChunks.remove(x, z) != null) {
          activeChunks.remove(x, z);
        }
        CCMsg = new CompressedChunkMessage(x, z, true, null, null, null, true);
      } else {
        CCMsg = new CompressedChunkMessage(x, z, false, null, null, null, true);
      }

      session.send(false, CCMsg);
    }
  }
 @Override
 protected boolean canSendChunk(Chunk c, Set<Chunk> unsendable) {
   if (!c.canSend()) {
     if (unsendable != null) {
       unsendable.add(c);
     }
     c.populate(true, true, true);
     return false;
   }
   if (activeChunks.contains(c.getX(), c.getZ())) {
     return true;
   }
   boolean canSend = true;
   Collection<Chunk> chunks = chunkInit.getChunks(c);
   for (Chunk cc : chunks) {
     if (!cc.canSend()) {
       if (unsendable != null) {
         unsendable.add(c);
       }
       canSend = false;
       cc.populate(true, true, true);
     }
   }
   return canSend;
 }
  @Override
  public void preSnapshot() {
    super.preSnapshot();

    Long key;
    while ((key = this.emptyColumns.poll()) != null) {
      int x = IntPairHashed.key1(key);
      int z = IntPairHashed.key2(key);
      TIntSet column = initializedChunks.get(x, z);
      if (column.isEmpty()) {
        column = initializedChunks.remove(x, z);
        activeChunks.remove(x, z);
        session.send(
            false, new ChunkDataMessage(x, z, true, null, null, null, true, player.getSession()));
      }
    }
  }
  @Override
  public void sendChunk(Chunk c) {

    int x = c.getX();
    int y = c.getY(); // + SEALEVEL_CHUNK;
    int z = c.getZ();

    if (y < 0 || y >= c.getWorld().getHeight() >> Chunk.BLOCKS.BITS) {
      return;
    }

    initChunk(c.getBase());

    if (activeChunks.add(x, z)) {
      Point p = c.getBase();
      int[][] heights = getColumnHeights(p);
      BlockMaterial[][] materials = getColumnTopmostMaterials(p);

      byte[][] packetChunkData = new byte[16][];

      for (int cube = 0; cube < 16; cube++) {
        packetChunkData[cube] = getChunkHeightMap(heights, materials, cube);
      }

      Chunk chunk = p.getWorld().getChunkFromBlock(p);
      byte[] biomeData = new byte[Chunk.BLOCKS.AREA];
      for (int dx = x; dx < x + Chunk.BLOCKS.SIZE; ++dx) {
        for (int dz = z; dz < z + Chunk.BLOCKS.SIZE; ++dz) {
          Biome biome = chunk.getBiomeType(dx & Chunk.BLOCKS.MASK, 0, dz & Chunk.BLOCKS.MASK);
          if (biome instanceof VanillaBiome) {
            biomeData[(dz & Chunk.BLOCKS.MASK) << 4 | (dx & Chunk.BLOCKS.MASK)] =
                (byte) ((VanillaBiome) biome).getBiomeId();
          }
        }
      }

      CompressedChunkMessage CCMsg =
          new CompressedChunkMessage(x, z, true, new boolean[16], packetChunkData, biomeData);
      owner.getSession().send(false, CCMsg);
    }

    ChunkSnapshot snapshot =
        c.getSnapshot(SnapshotType.BOTH, EntityType.NO_ENTITIES, ExtraData.NO_EXTRA_DATA);
    short[] rawBlockIdArray = snapshot.getBlockIds();
    short[] rawBlockData = snapshot.getBlockData();
    byte[] rawBlockLight = snapshot.getBlockLight();
    byte[] rawSkyLight = snapshot.getSkyLight();
    byte[] fullChunkData = new byte[Chunk.BLOCKS.HALF_VOLUME * 5];

    int arrIndex = 0;
    for (int i = 0; i < rawBlockIdArray.length; i++) {
      short convert = getMinecraftId(rawBlockIdArray[i]);
      fullChunkData[arrIndex++] = (byte) (convert & 0xFF);
    }

    for (int i = 0; i < rawBlockData.length; i += 2) {
      fullChunkData[arrIndex++] = (byte) ((rawBlockData[i + 1] << 4) | (rawBlockData[i] & 0xF));
    }

    System.arraycopy(rawBlockLight, 0, fullChunkData, arrIndex, rawBlockLight.length);
    arrIndex += rawBlockLight.length;

    System.arraycopy(rawSkyLight, 0, fullChunkData, arrIndex, rawSkyLight.length);

    arrIndex += rawSkyLight.length;

    byte[][] packetChunkData = new byte[16][];
    packetChunkData[y] = fullChunkData;
    CompressedChunkMessage CCMsg =
        new CompressedChunkMessage(x, z, false, new boolean[16], packetChunkData, null);
    owner.getSession().send(false, CCMsg);
  }
  @Override
  public Collection<Chunk> sendChunk(Chunk c) {

    int x = c.getX();
    int y = c.getY(); // + SEALEVEL_CHUNK;
    int z = c.getZ();

    if (y < 0 || y >= c.getWorld().getHeight() >> Chunk.BLOCKS.BITS) {
      return null;
    }

    initChunk(c.getBase());

    Collection<Chunk> chunks = null;

    if (activeChunks.add(x, z)) {
      Point p = c.getBase();
      int[][] heights = getColumnHeights(p);
      BlockMaterial[][] materials = getColumnTopmostMaterials(p);

      byte[][] packetChunkData = new byte[16][];

      for (int cube = 0; cube < 16; cube++) {
        Point pp =
            new Point(
                c.getWorld(),
                x << Chunk.BLOCKS.BITS,
                cube << Chunk.BLOCKS.BITS,
                z << Chunk.BLOCKS.BITS);
        packetChunkData[cube] = chunkInit.getChunkData(heights, materials, pp);
      }

      Chunk chunk = p.getWorld().getChunkFromBlock(p);
      byte[] biomeData = new byte[Chunk.BLOCKS.AREA];
      for (int dx = x; dx < x + Chunk.BLOCKS.SIZE; ++dx) {
        for (int dz = z; dz < z + Chunk.BLOCKS.SIZE; ++dz) {
          Biome biome = chunk.getBiome(dx & Chunk.BLOCKS.MASK, 0, dz & Chunk.BLOCKS.MASK);
          if (biome instanceof VanillaBiome) {
            biomeData[(dz & Chunk.BLOCKS.MASK) << 4 | (dx & Chunk.BLOCKS.MASK)] =
                (byte) ((VanillaBiome) biome).getBiomeId();
          }
        }
      }

      ChunkDataMessage CCMsg =
          new ChunkDataMessage(
              x, z, true, new boolean[16], packetChunkData, biomeData, player.getSession());
      player.getSession().send(false, CCMsg);

      chunks = chunkInit.getChunks(c);
    }

    byte[] fullChunkData = ChunkInit.getChunkFullData(c);

    byte[][] packetChunkData = new byte[16][];
    packetChunkData[y] = fullChunkData;
    ChunkDataMessage CCMsg =
        new ChunkDataMessage(
            x, z, false, new boolean[16], packetChunkData, null, player.getSession());
    player.getSession().send(false, CCMsg);

    return chunks;
  }
  @Override
  protected Collection<Chunk> sendChunk(Chunk c, boolean force) {

    if (!force) {
      return sendChunk(c);
    }

    int x = c.getX();
    int y = c.getY(); // + SEALEVEL_CHUNK;
    int z = c.getZ();

    RepositionManager rm = getRepositionManager();

    int cY = rm.convertChunkY(y);

    if (cY < 0 || cY >= c.getWorld().getHeight() >> Chunk.BLOCKS.BITS) {
      return null;
    }

    initChunkRaw(c.getBase());

    Collection<Chunk> chunks = null;

    List<ProtocolEvent> events = new ArrayList<ProtocolEvent>();

    if (activeChunks.add(x, z)) {
      Point p = c.getBase();
      int[][] heights = getColumnHeights(p);
      BlockMaterial[][] materials = getColumnTopmostMaterials(p);

      byte[][] packetChunkData = new byte[16][];

      for (int cube = 0; cube < 16; cube++) {
        int serverCube = rm.getInverse().convertChunkY(cube);
        Point pp =
            new Point(
                c.getWorld(),
                x << Chunk.BLOCKS.BITS,
                serverCube << Chunk.BLOCKS.BITS,
                z << Chunk.BLOCKS.BITS);
        packetChunkData[cube] = chunkInit.getChunkData(heights, materials, pp, events);
      }

      Chunk chunk = p.getWorld().getChunkFromBlock(p);
      byte[] biomeData = new byte[Chunk.BLOCKS.AREA];
      for (int dx = x; dx < x + Chunk.BLOCKS.SIZE; ++dx) {
        for (int dz = z; dz < z + Chunk.BLOCKS.SIZE; ++dz) {
          Biome biome = chunk.getBiome(dx & Chunk.BLOCKS.MASK, 0, dz & Chunk.BLOCKS.MASK);
          if (biome instanceof VanillaBiome) {
            biomeData[(dz & Chunk.BLOCKS.MASK) << 4 | (dx & Chunk.BLOCKS.MASK)] =
                (byte) ((VanillaBiome) biome).getBiomeId();
          }
        }
      }

      ChunkDataMessage CCMsg =
          new ChunkDataMessage(
              x,
              z,
              true,
              new boolean[16],
              packetChunkData,
              biomeData,
              player.getSession(),
              getRepositionManager());
      player.getSession().send(false, CCMsg);

      chunks = chunkInit.getChunks(c);
    }

    if (chunks == null || !chunks.contains(c)) {

      byte[] fullChunkData = ChunkInit.getChunkFullData(c, events);

      byte[][] packetChunkData = new byte[16][];
      packetChunkData[cY] = fullChunkData;
      ChunkDataMessage CCMsg =
          new ChunkDataMessage(
              x,
              z,
              false,
              new boolean[16],
              packetChunkData,
              null,
              player.getSession(),
              getRepositionManager());
      player.getSession().send(false, CCMsg);

      if (chunks == null) {
        chunks = new ArrayList<Chunk>(1);
      }
      chunks.add(c);
    }

    for (ProtocolEvent e : events) {
      this.callProtocolEvent(e);
    }

    return chunks;
  }