From 7abf31ea821a0bbddb836adb1a63d0fec2ceee4f Mon Sep 17 00:00:00 2001
From: Leonard Kugis <leonard@kug.is>
Date: Mon, 25 Apr 2022 18:41:24 +0200
Subject: Initial commit

---
 .../com/encrox/instancedregions/Commander.java     |  48 +++
 .../encrox/instancedregions/DereferencedBlock.java |  20 ++
 .../InstancedProtectedCuboidRegion.java            | 296 ++++++++++++++++
 .../encrox/instancedregions/InstancedRegions.java  |  40 +++
 .../instancedregions/chunkmap/ChunkData.java       |  16 +
 .../instancedregions/chunkmap/ChunkLayer.java      |  11 +
 .../instancedregions/chunkmap/ChunkMapBuffer.java  |  65 ++++
 .../instancedregions/chunkmap/ChunkMapManager.java | 384 +++++++++++++++++++++
 .../instancedregions/chunkmap/ChunkReader.java     | 100 ++++++
 .../instancedregions/chunkmap/ChunkWriter.java     | 108 ++++++
 .../instancedregions/packet/SpawnEntity.java       |  13 +
 .../encrox/instancedregions/types/BlockState.java  |  11 +
 12 files changed, 1112 insertions(+)
 create mode 100755 src/main/java/com/encrox/instancedregions/Commander.java
 create mode 100755 src/main/java/com/encrox/instancedregions/DereferencedBlock.java
 create mode 100755 src/main/java/com/encrox/instancedregions/InstancedProtectedCuboidRegion.java
 create mode 100755 src/main/java/com/encrox/instancedregions/InstancedRegions.java
 create mode 100755 src/main/java/com/encrox/instancedregions/chunkmap/ChunkData.java
 create mode 100755 src/main/java/com/encrox/instancedregions/chunkmap/ChunkLayer.java
 create mode 100755 src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapBuffer.java
 create mode 100755 src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapManager.java
 create mode 100755 src/main/java/com/encrox/instancedregions/chunkmap/ChunkReader.java
 create mode 100755 src/main/java/com/encrox/instancedregions/chunkmap/ChunkWriter.java
 create mode 100755 src/main/java/com/encrox/instancedregions/packet/SpawnEntity.java
 create mode 100755 src/main/java/com/encrox/instancedregions/types/BlockState.java

(limited to 'src/main/java/com')

diff --git a/src/main/java/com/encrox/instancedregions/Commander.java b/src/main/java/com/encrox/instancedregions/Commander.java
new file mode 100755
index 0000000..fa541d6
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/Commander.java
@@ -0,0 +1,48 @@
+package com.encrox.instancedregions;
+
+import java.util.Iterator;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+import org.bukkit.entity.Player;
+import org.bukkit.plugin.Plugin;
+
+import com.sk89q.worldedit.BlockVector;
+
+public class Commander implements CommandExecutor {
+	
+	private Plugin plugin;
+	
+	public Commander(Plugin plugin) {
+		this.plugin = plugin;
+	}
+
+	@Override
+	public boolean onCommand(CommandSender arg0, Command arg1, String arg2, String[] arg3) {
+		if(arg0 instanceof Player) {
+			Player player = (Player)arg0;
+			switch(arg3[0]) {
+			case "create":
+				InstancedProtectedCuboidRegion rg = new InstancedProtectedCuboidRegion(plugin, player.getWorld(), "testInstance", new BlockVector(Integer.parseInt(arg3[1]),0,Integer.parseInt(arg3[2])), new BlockVector(Integer.parseInt(arg3[3]),255,Integer.parseInt(arg3[4])));
+				rg.addPlayer(player);
+				InstancedRegions.region.add(rg);
+				return true;
+			case "dispose":
+				Iterator<InstancedProtectedCuboidRegion> iter = InstancedRegions.region.iterator();
+				while(iter.hasNext())
+					iter.next().dispose();
+				return true;
+			/*case "test":
+				InstancedRegions.region.addToChangeWhitelist(new BlockVector(Integer.parseInt(arg3[1]), Integer.parseInt(arg3[2]), Integer.parseInt(arg3[3])));
+				player.sendBlockChange(new Location(player.getWorld(), Integer.parseInt(arg3[1]), Integer.parseInt(arg3[2]), Integer.parseInt(arg3[3])), Material.STONE, (byte) 0);
+				return true;*/
+			}
+		}
+		return false;
+	}
+
+}
diff --git a/src/main/java/com/encrox/instancedregions/DereferencedBlock.java b/src/main/java/com/encrox/instancedregions/DereferencedBlock.java
new file mode 100755
index 0000000..2f77a52
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/DereferencedBlock.java
@@ -0,0 +1,20 @@
+package com.encrox.instancedregions;
+
+import org.bukkit.Location;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockState;
+
+public class DereferencedBlock {
+	
+	public Material type;
+	public Location location;
+	public BlockState blockState;
+	
+	public DereferencedBlock(Block block) {
+		type = block.getType();
+		location = block.getLocation();
+		blockState = block.getState();
+	}
+
+}
diff --git a/src/main/java/com/encrox/instancedregions/InstancedProtectedCuboidRegion.java b/src/main/java/com/encrox/instancedregions/InstancedProtectedCuboidRegion.java
new file mode 100755
index 0000000..3271c4d
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/InstancedProtectedCuboidRegion.java
@@ -0,0 +1,296 @@
+package com.encrox.instancedregions;
+
+import java.util.ArrayList;
+import java.util.UUID;
+
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.WorldType;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.block.BlockBreakEvent;
+import org.bukkit.event.block.BlockPlaceEvent;
+import org.bukkit.plugin.Plugin;
+
+import com.comphenix.protocol.PacketType;
+import com.comphenix.protocol.ProtocolLibrary;
+import com.comphenix.protocol.ProtocolManager;
+import com.comphenix.protocol.events.ListenerPriority;
+import com.comphenix.protocol.events.PacketAdapter;
+import com.comphenix.protocol.events.PacketContainer;
+import com.comphenix.protocol.events.PacketEvent;
+import com.comphenix.protocol.reflect.StructureModifier;
+import com.comphenix.protocol.wrappers.BlockPosition;
+import com.encrox.instancedregions.chunkmap.ChunkData;
+import com.encrox.instancedregions.chunkmap.ChunkMapManager;
+import com.encrox.instancedregions.types.BlockState;
+import com.sk89q.worldedit.BlockVector;
+import com.sk89q.worldedit.Vector;
+import com.sk89q.worldguard.protection.regions.ProtectedCuboidRegion;
+import com.sk89q.worldguard.protection.regions.ProtectedRegion;
+
+public class InstancedProtectedCuboidRegion extends ProtectedCuboidRegion implements Listener {
+	
+	private ArrayList<Player> players;
+	private ArrayList<DereferencedBlock> blockBreaks, blockPlaces;
+	private ArrayList<BlockVector> changeWhitelist;
+	private ProtocolManager pmgr;
+	private PacketAdapter mapAdapter, spawnEntityAdapter, spawnExperienceOrbAdapter, spawnMobAdapter, spawnPlayerAdapter, blockBreakAdapter, blockChangeAdapter, multiBlockChangeAdapter;
+	private World world;
+
+	public InstancedProtectedCuboidRegion(Plugin plugin, final World world, String id, BlockVector pt1, BlockVector pt2) {
+		super(id, pt1, pt2);
+		Bukkit.getPluginManager().registerEvents(this, plugin);
+		players = new ArrayList<Player>();
+		blockBreaks = new ArrayList<DereferencedBlock>();
+		blockPlaces = new ArrayList<DereferencedBlock>();
+		changeWhitelist = new ArrayList<BlockVector>();
+		this.world = world;
+		pmgr = ProtocolLibrary.getProtocolManager();
+		//catch chunk data
+		mapAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.MAP_CHUNK){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					PacketContainer packet = event.getPacket();
+					StructureModifier<Integer> ints = packet.getIntegers();
+					StructureModifier<byte[]> byteArray = packet.getByteArrays();
+					StructureModifier<Boolean> bools = packet.getBooleans();
+					ChunkData chunkData = new ChunkData();
+					chunkData.chunkX = ints.read(0);
+					chunkData.chunkZ = ints.read(1);
+					chunkData.groundUpContinuous = bools.read(0);
+					chunkData.primaryBitMask = ints.read(2);
+					chunkData.data = byteArray.read(0);
+					chunkData.isOverworld = event.getPlayer().getWorld().getWorldType() == WorldType.NORMAL;
+					ChunkMapManager cmm = new ChunkMapManager(chunkData);
+					ArrayList<ProtectedRegion> col = new ArrayList<ProtectedRegion>();
+					col.add(new ProtectedCuboidRegion("current", new BlockVector(chunkData.chunkX<<4, 0, chunkData.chunkZ<<4), new BlockVector((chunkData.chunkX<<4)|15, 256, (chunkData.chunkZ<<4)|15)));
+					try {
+						if(getIntersectingRegions(col).isEmpty()) {
+							PacketContainer unloadPacket = new PacketContainer(PacketType.Play.Server.UNLOAD_CHUNK);
+							unloadPacket.getIntegers().write(0, chunkData.chunkX).write(1, chunkData.chunkZ);
+							pmgr.sendServerPacket(player, unloadPacket);
+							cmm.init();
+							for(int y = 0; y<16; y++) {
+								for(int z = 0; z<16; z++) {
+									for(int x = 0; x<16; x++) {
+										try {
+											cmm.readNextBlock();
+										} catch(Exception e1) {
+											
+										}
+										//int blockData = cmm.readNextBlock();
+										BlockState blockState = new BlockState();
+										//ChunkMapManager.blockDataToState(blockData, blockState);
+										blockState.id = 0;
+										blockState.meta = 0;
+										//blockData = ChunkMapManager.blockStateToData(blockState);
+										cmm.writeOutputBlock(ChunkMapManager.blockStateToData(blockState));
+									}
+								}
+							}
+							cmm.finalizeOutput();
+							byteArray.write(0, cmm.createOutput());
+						}
+					} catch(Exception e) {
+						
+					}
+				}
+			}
+		};
+		//catch entity objects (chests, signs ...)
+		spawnEntityAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.SPAWN_ENTITY){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					PacketContainer packet = event.getPacket();
+					StructureModifier<Double> doubles = packet.getDoubles();
+					double x = doubles.read(0);
+					double y = doubles.read(1);
+					double z = doubles.read(2);
+					if(!contains(new Vector(x, y, z))) {
+						event.setCancelled(true);
+					}
+				}
+			}
+		};
+		//catch experience orbs
+		spawnExperienceOrbAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.SPAWN_ENTITY_EXPERIENCE_ORB){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					PacketContainer packet = event.getPacket();
+					StructureModifier<Double> doubles = packet.getDoubles();
+					double x = doubles.read(0);
+					double y = doubles.read(1);
+					double z = doubles.read(2);
+					if(!contains(new Vector(x, y, z))) {
+						event.setCancelled(true);
+					}
+				}
+			}
+		};
+		//catch mobs
+		spawnMobAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.SPAWN_ENTITY_LIVING){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					PacketContainer packet = event.getPacket();
+					StructureModifier<Double> doubles = packet.getDoubles();
+					double x = doubles.read(0);
+					double y = doubles.read(1);
+					double z = doubles.read(2);
+					if(!contains(new Vector(x, y, z))) {
+						event.setCancelled(true);
+					}
+				}
+			}
+		};
+		//catch players
+		spawnPlayerAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.NAMED_ENTITY_SPAWN){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					PacketContainer packet = event.getPacket();
+					StructureModifier<UUID> uuids = packet.getUUIDs();
+					UUID uuid = uuids.read(0);
+					for(int i = 0, size = players.size(); i<size; i++) {
+						if(!players.get(i).getUniqueId().equals(uuid)) {
+							event.setCancelled(true);
+						}
+					}
+				}
+			}
+		};
+		//catch block break animations
+		blockBreakAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.BLOCK_BREAK_ANIMATION){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					PacketContainer packet = event.getPacket();
+					long position = packet.getLongs().read(0);
+					long x = position >> 38;
+					long y = (position >> 26) & 0xFFF;
+					long z = position << 38 >> 38;
+					InstancedRegions.logger.info("block break: " + x + ", " + y + ", " + z);
+				}
+			}
+		};
+		//catch block changes
+		blockChangeAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.BLOCK_CHANGE){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					//InstancedRegions.logger.info("BLOCK_CHANGE");//erst pasten, dann region newen
+					PacketContainer packet = event.getPacket();
+					StructureModifier<BlockPosition> bps = packet.getBlockPositionModifier();
+					BlockPosition bp = bps.read(0);
+					//InstancedRegions.logger.info("BLOCK_CHANGE at: " + bp.getX() + ", " + bp.getY() + ", " + bp.getZ());
+					BlockVector bv = new BlockVector(bp.getX(), bp.getY(), bp.getZ());
+					if(!contains(bv)) {
+						if(changeWhitelist.contains(bv)) {
+							changeWhitelist.remove(bv);
+						} else {
+							event.setCancelled(true);
+						}
+					}
+				}
+			}
+		};
+		//catch multi block changes
+		multiBlockChangeAdapter = new PacketAdapter(plugin, ListenerPriority.NORMAL, PacketType.Play.Server.MULTI_BLOCK_CHANGE){
+			@Override
+			public void onPacketSending(PacketEvent event) {
+				Player player = event.getPlayer();
+				if(world.equals(player.getWorld()) && players.contains(event.getPlayer())) {
+					try {
+						//InstancedRegions.logger.info("MULTI_BLOCK_CHANGE");
+						PacketContainer packet = event.getPacket();
+						StructureModifier<int[]> ints = packet.getIntegerArrays();
+						int[] coords = ints.read(0);
+						int chunkX = coords[0];
+						int chunkZ = coords[1];
+						ArrayList<ProtectedRegion> col = new ArrayList<ProtectedRegion>();
+						col.add(new ProtectedCuboidRegion("current", new BlockVector(chunkX<<4, 0, chunkZ<<4), new BlockVector((chunkX<<4)|15, 256, (chunkZ<<4)|15)));
+						if(getIntersectingRegions(col).isEmpty()) {
+							event.setCancelled(true);
+						}
+					} catch(Exception e) {
+						
+					}
+				}
+			}
+		};
+	}
+	
+	public void addPlayer(Player player) {
+		players.add(player);
+	}
+	
+	public void removePlayer(Player player) {
+		players.remove(player);
+	}
+	
+	public void apply() {
+		pmgr.addPacketListener(mapAdapter);
+		pmgr.addPacketListener(spawnEntityAdapter);
+		pmgr.addPacketListener(spawnExperienceOrbAdapter);
+		pmgr.addPacketListener(spawnMobAdapter);
+		pmgr.addPacketListener(spawnPlayerAdapter);
+		pmgr.addPacketListener(blockBreakAdapter);
+		pmgr.addPacketListener(blockChangeAdapter);
+		pmgr.addPacketListener(multiBlockChangeAdapter);
+	}
+	
+	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+	public void onBlockBreak(BlockBreakEvent event) {
+		DereferencedBlock block = new DereferencedBlock(event.getBlock());
+		if(players.contains(event.getPlayer()) && contains(block.location.getBlockX(), block.location.getBlockY(), block.location.getBlockZ())) {
+			blockBreaks.add(block);
+		}
+	}
+	
+	@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+	public void onBlockPlace(BlockPlaceEvent event) {
+		DereferencedBlock block = new DereferencedBlock(event.getBlock());
+		if(players.contains(event.getPlayer()) && contains(block.location.getBlockX(), block.location.getBlockY(), block.location.getBlockZ())) {
+			blockPlaces.add(block);
+		}
+	}
+	
+	public void addToChangeWhitelist(BlockVector bv) {
+		changeWhitelist.add(bv);
+	}
+	
+	public void dispose() {
+		DereferencedBlock current;
+		for(int i = 0, size = blockBreaks.size(); i<size; i++) {
+			current = blockBreaks.get(i);
+			world.getBlockAt(current.location).setType(current.type);
+		}
+		for(int i = 0, size = blockPlaces.size(); i<size; i++) {
+			current = blockPlaces.get(i);
+			world.getBlockAt(current.location).setType(Material.AIR);
+		}
+		pmgr.removePacketListener(mapAdapter);
+		pmgr.removePacketListener(spawnEntityAdapter);
+		pmgr.removePacketListener(spawnExperienceOrbAdapter);
+		pmgr.removePacketListener(spawnMobAdapter);
+		pmgr.removePacketListener(spawnPlayerAdapter);
+		pmgr.removePacketListener(blockBreakAdapter);
+		pmgr.removePacketListener(blockChangeAdapter);
+		pmgr.removePacketListener(multiBlockChangeAdapter);
+	}
+
+}
diff --git a/src/main/java/com/encrox/instancedregions/InstancedRegions.java b/src/main/java/com/encrox/instancedregions/InstancedRegions.java
new file mode 100755
index 0000000..320fcad
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/InstancedRegions.java
@@ -0,0 +1,40 @@
+package com.encrox.instancedregions;
+
+import java.util.ArrayList;
+import java.util.logging.Logger;
+
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.java.JavaPlugin;
+
+import com.comphenix.protocol.ProtocolManager;
+import com.sk89q.worldedit.bukkit.WorldEditPlugin;
+import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
+
+public class InstancedRegions extends JavaPlugin {
+	
+	public static Logger logger;
+	public static PluginDescriptionFile pdf;
+	public static WorldGuardPlugin wg;
+	public static WorldEditPlugin we;
+	public static ProtocolManager pmgr;
+	
+	//test
+	public static ArrayList<InstancedProtectedCuboidRegion> region;
+	
+	public void onEnable() {
+		pdf = getDescription();
+		logger = Logger.getLogger("Minecraft");
+		if(setupMyself()) {
+			getCommand("instance").setExecutor(new Commander(this));
+			logger.info(pdf.getName() + " " + pdf.getVersion() + " has been enabled.");
+		} else {
+			logger.info(pdf.getName() + " " + pdf.getVersion() + " has been disabled.");
+		}
+	}
+	
+	private boolean setupMyself() {
+		region = new ArrayList<InstancedProtectedCuboidRegion>();
+		return true;
+	}
+
+}
diff --git a/src/main/java/com/encrox/instancedregions/chunkmap/ChunkData.java b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkData.java
new file mode 100755
index 0000000..eccac2f
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkData.java
@@ -0,0 +1,16 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+
+package com.encrox.instancedregions.chunkmap;
+
+public class ChunkData {
+	public int chunkX;
+	public int chunkZ;
+	public boolean groundUpContinuous;
+	public int primaryBitMask;
+	public byte[] data;
+	public boolean isOverworld;
+	public boolean useCache;
+}
diff --git a/src/main/java/com/encrox/instancedregions/chunkmap/ChunkLayer.java b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkLayer.java
new file mode 100755
index 0000000..04478bd
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkLayer.java
@@ -0,0 +1,11 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+
+package com.encrox.instancedregions.chunkmap;
+
+public class ChunkLayer {
+	public boolean hasData;
+	public int[] map;
+}
diff --git a/src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapBuffer.java b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapBuffer.java
new file mode 100755
index 0000000..e138c9f
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapBuffer.java
@@ -0,0 +1,65 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+
+package com.encrox.instancedregions.chunkmap;
+
+public class ChunkMapBuffer {
+	private static final int BITS_PER_BLOCK_SIZE = 1;
+	private static final int PALETTE_LENGTH_SIZE = 5;
+	private static final int DATA_ARRAY_LENGTH_SIZE = 5;
+	private static final int BLOCKS_PER_CHUNK_SECTION = 16 * 16 * 16;
+	private static final int DATA_ARRAY_SIZE = BLOCKS_PER_CHUNK_SECTION * 13 / 8;
+	private static final int BLOCK_LIGHT_SIZE = BLOCKS_PER_CHUNK_SECTION / 2;
+	private static final int SKY_LIGHT_SIZE = BLOCKS_PER_CHUNK_SECTION / 2;
+	private static final int COLUMNS_PER_CHUNK = 16;
+	
+	private static final int MAX_BYTES_PER_CHUNK =
+			COLUMNS_PER_CHUNK *
+			(
+				BITS_PER_BLOCK_SIZE
+				+ PALETTE_LENGTH_SIZE
+				+ DATA_ARRAY_LENGTH_SIZE
+				+ DATA_ARRAY_SIZE
+				+ BLOCK_LIGHT_SIZE
+				+ SKY_LIGHT_SIZE
+			);
+	
+	public int[] palette;
+	public byte[] output;
+	public int[] outputPalette;
+	public byte[] outputPaletteMap;
+	public ChunkWriter writer;
+	public ChunkLayer prevLayer;
+	public ChunkLayer curLayer;
+	public ChunkLayer nextLayer;
+	
+	public int bitsPerBlock;
+	public int paletteLength;
+	public int dataArrayLength;
+	public int lightArrayLength;
+	public int dataArrayStartIndex;
+	public int outputPaletteLength;
+	public int outputBitsPerBlock;
+    
+	public ChunkMapBuffer() {
+    	this.palette = new int[256];
+		this.output = new byte[MAX_BYTES_PER_CHUNK];
+		this.outputPalette = new int[256];
+		this.outputPaletteMap = new byte[65536];
+		this.writer = new ChunkWriter(this.output);
+		this.prevLayer = new ChunkLayer();
+		this.prevLayer.map = new int[16 * 16];
+		this.curLayer = new ChunkLayer();
+		this.curLayer.map = new int[16 * 16];
+		this.nextLayer = new ChunkLayer();
+		this.nextLayer.map = new int[16 * 16];
+	}
+	
+	public void clearLayers() {
+		this.prevLayer.hasData = false;
+		this.curLayer.hasData = false;
+		this.nextLayer.hasData = false;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapManager.java b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapManager.java
new file mode 100755
index 0000000..87fb3a1
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkMapManager.java
@@ -0,0 +1,384 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+
+package com.encrox.instancedregions.chunkmap;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import com.encrox.instancedregions.types.BlockState;
+
+public class ChunkMapManager {
+    private static final ThreadLocal<ChunkMapBuffer> _buffer = new ThreadLocal<ChunkMapBuffer>() {
+    	@Override
+        protected ChunkMapBuffer initialValue() {
+            return new ChunkMapBuffer();
+        }
+    };
+    
+    private ChunkMapBuffer buffer;
+    private ChunkData chunkData;
+    private ChunkReader reader;
+    private int sectionCount;
+    private int sectionIndex;
+    private int y;
+    private int minX;
+    private int maxX;
+    private int minZ;
+    private int maxZ;
+    private int blockIndex;
+    
+    public int getSectionCount() {
+    	return this.sectionCount;
+    }
+    
+    public int getY() {
+    	return this.y;
+    }
+    
+    public ChunkData getChunkData() {
+    	return this.chunkData;
+    }
+
+    public ChunkMapManager(ChunkData chunkData) {
+    	this.buffer = _buffer.get();
+    	this.chunkData = chunkData;
+    }
+    
+    public void init() throws IOException {
+    	this.reader = new ChunkReader(this.chunkData.data);
+    	this.sectionCount = 0;
+    	this.sectionIndex = -1;
+    	this.minX = this.chunkData.chunkX << 4;
+    	this.maxX = this.minX + 15;
+    	this.minZ = this.chunkData.chunkZ << 4;
+    	this.maxZ = this.minZ + 15;
+    	
+		this.buffer.lightArrayLength = 2048;
+		
+		if(this.chunkData.isOverworld) {
+			this.buffer.lightArrayLength <<= 1;
+		}
+    	
+    	this.buffer.writer.init();
+    	
+    	int mask = this.chunkData.primaryBitMask;
+    	
+    	while(mask != 0) {
+    		if((mask & 0x1) != 0) {
+    			this.sectionCount++;
+    		}
+    		
+    		mask >>>= 1;
+    	}
+    	
+    	this.buffer.clearLayers();
+
+    	moveToNextLayer();
+    }
+    
+    public boolean inputHasNonAirBlock() {
+    	return this.buffer.paletteLength > 1 || this.buffer.palette[0] != 0;
+    }
+    
+    public static void blockDataToState(int blockData, BlockState blockState) {
+    	blockState.id = blockData >>> 4;
+    	blockState.meta = blockData & 0xf;
+    }
+    
+    public static int getBlockIdFromData(int blockData) {
+    	return blockData >>> 4;
+    }
+
+    public static int getBlockMetaFromData(int blockData) {
+    	return blockData & 0xf;
+    }
+
+    public static int blockStateToData(BlockState blockState) {
+    	return (blockState.id << 4) | blockState.meta; 
+    }
+    
+    public static int getBlockDataFromId(int id) {
+    	return id << 4;
+    }
+
+    public boolean initOutputPalette() {
+    	if(this.buffer.paletteLength == 0 || this.buffer.paletteLength == 255) {
+    		this.buffer.outputPaletteLength = 0;
+    		return false;
+    	}
+    	
+    	Arrays.fill(this.buffer.outputPaletteMap, (byte)-1);
+    	
+    	this.buffer.outputPaletteLength = this.buffer.paletteLength;
+    	
+    	for(int i = 0; i < this.buffer.paletteLength; i++) {
+    		int blockData = this.buffer.palette[i];
+    		    		
+    		this.buffer.outputPalette[i] = blockData;
+    		
+    		if(blockData >= 0) {
+    			this.buffer.outputPaletteMap[blockData] = (byte)i;
+    		}
+    	}
+    	
+    	return true;
+    }
+    
+    public boolean addToOutputPalette(int blockData) {
+    	if(this.buffer.outputPaletteMap[blockData] >= 0) return true;
+    	
+    	//255 (-1 for byte) is special code in my algorithm
+    	if(this.buffer.outputPaletteLength == 254) {
+    		this.buffer.outputPaletteLength = 0;
+    		return false;
+    	}
+    	
+    	this.buffer.outputPalette[this.buffer.outputPaletteLength] = blockData;
+    	this.buffer.outputPaletteMap[blockData] = (byte)this.buffer.outputPaletteLength;
+    	
+    	this.buffer.outputPaletteLength++;
+    	
+    	return true;
+    }
+    
+    public void initOutputSection() throws IOException {
+    	calcOutputBitsPerBlock();
+    	
+    	this.buffer.writer.setBitsPerBlock(this.buffer.outputBitsPerBlock);
+    	
+    	//Bits Per Block
+    	this.buffer.writer.writeByte((byte)this.buffer.outputBitsPerBlock);
+    	
+    	//Palette Length
+    	this.buffer.writer.writeVarInt(this.buffer.outputPaletteLength);
+    	
+    	//Palette
+    	for(int i = 0; i < this.buffer.outputPaletteLength; i++) {
+    		this.buffer.writer.writeVarInt(this.buffer.outputPalette[i]);
+    	}
+    	
+    	int dataArrayLengthInBits = this.buffer.outputBitsPerBlock << 12;// multiply by 4096
+    	int outputDataArrayLength = dataArrayLengthInBits >>> 6;//divide by 64
+    	
+    	if((dataArrayLengthInBits & 0x3f) != 0) {
+    		outputDataArrayLength++;
+    	}
+    	
+    	//Data Array Length
+    	this.buffer.writer.writeVarInt(outputDataArrayLength);
+    	
+    	//Copy Block Light and Sky Light arrays
+    	int lightArrayStartIndex = this.buffer.dataArrayStartIndex + (this.buffer.dataArrayLength << 3);
+    	int outputLightArrayStartIndex = this.buffer.writer.getByteIndex() + (outputDataArrayLength << 3);
+    	
+    	System.arraycopy(
+    			this.chunkData.data,
+    			lightArrayStartIndex,
+    			this.buffer.output,
+    			outputLightArrayStartIndex,
+    			this.buffer.lightArrayLength
+    			);
+    }
+    
+    public void writeOutputBlock(int blockData) throws IOException {
+    	if(this.buffer.outputPaletteLength > 0) {
+    		long paletteIndex = this.buffer.outputPaletteMap[blockData] & 0xffL;
+    		
+    		if(paletteIndex == 255) {
+    			BlockState blockState = new BlockState();
+    			blockDataToState(blockData, blockState);
+    			throw new IllegalArgumentException("Block " + blockState.id + ":" + blockState.meta + " is absent in output palette.");
+    		}
+    		
+    		this.buffer.writer.writeBlockBits(paletteIndex);
+    	} else {
+    		this.buffer.writer.writeBlockBits(blockData);
+    	}
+    }
+    
+    public void finalizeOutput() throws IOException {
+    	if(this.buffer.writer.getByteIndex() == 0) return;
+    	
+    	this.buffer.writer.save();
+    	this.buffer.writer.skip(this.buffer.lightArrayLength);
+    }
+    
+    public byte[] createOutput() {
+    	int readerByteIndex = this.reader.getByteIndex();
+    	int writerByteIndex = this.buffer.writer.getByteIndex();
+    	int biomesSize = this.chunkData.data.length - readerByteIndex;
+    	byte[] output = new byte[writerByteIndex + biomesSize];
+    	
+    	System.arraycopy(this.buffer.output, 0, output, 0, writerByteIndex);
+    	
+    	if(biomesSize > 0) {
+	    	System.arraycopy(
+	    			this.chunkData.data,
+	    			readerByteIndex,
+	    			output,
+	    			writerByteIndex,
+	    			biomesSize
+	    			);
+    	}
+    	
+    	return output;
+    }
+    
+    private void calcOutputBitsPerBlock() {
+    	if(this.buffer.outputPaletteLength == 0) {
+    		this.buffer.outputBitsPerBlock = 13;
+    	} else {
+    		byte mask = (byte)this.buffer.outputPaletteLength;
+    		int index = 0;
+    		
+    		while((mask & 0x80) == 0) {
+    			index++;
+    			mask <<= 1;
+    		}
+    		
+    		this.buffer.outputBitsPerBlock = 8 - index;
+  			
+  			if(this.buffer.outputBitsPerBlock < 4) {
+  				this.buffer.outputBitsPerBlock = 4;
+  			}
+    	}
+    }
+    
+    public int readNextBlock() throws IOException {
+    	if(this.blockIndex == 16 * 16) {
+    		if(!moveToNextLayer()) return -1;
+    	}
+    	
+    	return this.buffer.curLayer.map[this.blockIndex++];
+    }
+    
+    public int get(int x, int y, int z) throws IOException {
+    	if(x < minX || x > maxX
+    			|| z < minZ || z > maxZ
+    			|| y > 255 || y < this.y - 1 || y > this.y + 1
+    			) {
+    		return -1;
+    	}
+    	
+    	ChunkLayer layer;
+    	
+    	if(y == this.y) layer = this.buffer.curLayer;
+    	else if(y == this.y - 1) layer = this.buffer.prevLayer;
+    	else layer = this.buffer.nextLayer;
+    	
+    	if(!layer.hasData) return -1;
+    	
+    	int blockIndex = ((z - this.minZ) << 4) | (x - this.minX);
+
+    	return layer.map[blockIndex];
+    }
+        
+    private boolean moveToNextLayer() throws IOException {
+    	if(!increaseY()) return false;
+		
+		shiftLayersDown();
+		
+		if(!this.buffer.curLayer.hasData) {
+			readLayer(this.buffer.curLayer);
+		}
+
+		if(((this.y + 1) >>> 4) > this.sectionIndex) {
+			int oldSectionIndex = this.sectionIndex;
+			
+			moveToNextSection();
+			
+			if(this.sectionIndex < 16 && oldSectionIndex + 1 == this.sectionIndex) {
+				readLayer(this.buffer.nextLayer);
+			}
+		} else {
+			readLayer(this.buffer.nextLayer);
+		}
+		
+		this.blockIndex = 0;
+		
+		return true;
+    }
+    
+    private boolean increaseY() throws IOException {
+		if(this.sectionIndex < 0) {
+			if(!moveToNextSection()) return false;
+			
+			this.y = this.sectionIndex << 4;
+		}
+		else {
+			this.y++;
+			
+			if((this.y & 0xf) == 0) {
+				if(this.sectionIndex > 15) return false;
+
+	    		if((this.y >>> 4) != this.sectionIndex) {
+					this.buffer.clearLayers();
+		    		this.y = this.sectionIndex << 4;
+	    		}
+			}
+		}
+		
+		return true;
+	}
+    
+    private void shiftLayersDown() {
+		ChunkLayer temp = this.buffer.prevLayer;
+		
+		this.buffer.prevLayer = this.buffer.curLayer;
+		this.buffer.curLayer = this.buffer.nextLayer;
+		this.buffer.nextLayer = temp;
+		this.buffer.nextLayer.hasData = false;
+	}
+    
+    private boolean moveToNextSection() throws IOException {
+    	if(this.sectionIndex >= 0) {
+    		this.reader.skip(this.buffer.lightArrayLength);
+    	}
+    	
+		do {
+			this.sectionIndex++;
+		} while(this.sectionIndex < 16 && (this.chunkData.primaryBitMask & (1 << this.sectionIndex)) == 0);
+		
+		if(this.sectionIndex >= 16) return false;
+		
+		readSectionHeader();
+    	    	
+    	return true;
+    }
+    
+    private void readLayer(ChunkLayer layer) throws IOException {
+    	for(int i = 0; i < 16 * 16; i++) {
+    		int blockData = this.reader.readBlockBits();
+    		
+        	if(this.buffer.paletteLength > 0) {
+        		blockData = blockData >= 0 && blockData < this.buffer.paletteLength
+        				? this.buffer.palette[blockData]
+        				: 0;
+        	}
+
+        	layer.map[i] = blockData;
+    	}
+    	
+    	layer.hasData = true;
+    }
+    
+    private void readSectionHeader() throws IOException {
+    	this.buffer.bitsPerBlock = this.reader.readByte();
+    	this.buffer.paletteLength = this.reader.readVarInt();
+    	
+    	for(int i = 0; i < this.buffer.paletteLength; i++) {
+    		int paletteData = this.reader.readVarInt();
+    		
+    		this.buffer.palette[i] = paletteData;
+    	}
+    	
+    	this.buffer.dataArrayLength = this.reader.readVarInt();
+    	
+    	this.buffer.dataArrayStartIndex = this.reader.getByteIndex();
+    	
+    	this.reader.setBitsPerBlock(this.buffer.bitsPerBlock);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/encrox/instancedregions/chunkmap/ChunkReader.java b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkReader.java
new file mode 100755
index 0000000..a05fa7a
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkReader.java
@@ -0,0 +1,100 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+
+package com.encrox.instancedregions.chunkmap;
+
+import java.io.IOException;
+
+public class ChunkReader {
+	private byte[] data;
+	private int bitsPerBlock;
+	private long maxValueMask; 
+	private int byteIndex;
+	private int bitIndex;
+	private long buffer;
+	
+	public ChunkReader(byte[] data) {
+		this.data = data;
+		this.byteIndex = 0;
+		this.bitIndex = 0;
+	}
+	
+	public int getByteIndex() {
+		return this.byteIndex;
+	}
+	
+	public void skip(int count) {
+		this.byteIndex += count;
+		this.bitIndex = 0;
+	}
+	
+	public void setBitsPerBlock(int bitsPerBlock) {
+		this.bitsPerBlock = bitsPerBlock;
+		this.maxValueMask = (1L << this.bitsPerBlock) - 1;
+	}
+	
+	public int readBlockBits() throws IOException {
+		if(this.bitIndex == 0 || this.bitIndex >= 64) {
+			readLong();
+			this.bitIndex = 0;
+		}
+		
+		int leftBits = 64 - this.bitIndex;
+		long result = this.buffer >>> this.bitIndex;
+		
+		if(leftBits >= this.bitsPerBlock) {
+			this.bitIndex += this.bitsPerBlock;
+		} else {
+			readLong();
+			
+			result |= this.buffer << leftBits;
+			
+			this.bitIndex = this.bitsPerBlock - leftBits;
+		}
+		
+		return (int)(result & this.maxValueMask);
+	}
+	
+	private void readLong() throws IOException {
+		if(this.byteIndex + 7 >= this.data.length) {
+			throw new IOException("No data to read.");
+		}
+		
+		this.buffer = ((this.data[this.byteIndex] & 0xffL) << 56)
+				| ((this.data[this.byteIndex + 1] & 0xffL) << 48)
+				| ((this.data[this.byteIndex + 2] & 0xffL) << 40)
+				| ((this.data[this.byteIndex + 3] & 0xffL) << 32)
+				| ((this.data[this.byteIndex + 4] & 0xffL) << 24)
+				| ((this.data[this.byteIndex + 5] & 0xffL) << 16)
+				| ((this.data[this.byteIndex + 6] & 0xffL) << 8)
+				| (this.data[this.byteIndex + 7] & 0xffL);
+		
+		this.byteIndex += 8;
+	}
+	
+	public int readVarInt() throws IOException {
+        int value = 0;
+        int size = 0;
+        int b;
+        
+        while(((b = readByte()) & 0x80) == 0x80) {
+            value |= (b & 0x7F) << (size++ * 7);
+            
+            if(size > 5) {
+                throw new IOException("Invalid VarInt.");
+            }
+        }
+
+        return value | ((b & 0x7F) << (size * 7));
+    }
+	
+	public int readByte() throws IOException {
+		if(this.byteIndex >= this.data.length) {
+			throw new IOException("No data to read.");
+		}
+		
+		return this.data[this.byteIndex++] & 0xff;
+	}
+}
diff --git a/src/main/java/com/encrox/instancedregions/chunkmap/ChunkWriter.java b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkWriter.java
new file mode 100755
index 0000000..398cb25
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/chunkmap/ChunkWriter.java
@@ -0,0 +1,108 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+
+package com.encrox.instancedregions.chunkmap;
+
+import java.io.IOException;
+
+public class ChunkWriter {
+	private byte[] data;
+	private int bitsPerBlock;
+	private int byteIndex;
+	private int bitIndex;
+	private long buffer;
+	
+	public ChunkWriter(byte[] data) {
+		this.data = data;
+	}
+	
+	public int getByteIndex() {
+		return this.byteIndex;
+	}
+	
+	public void init() {
+		this.byteIndex = 0;
+		this.bitIndex = 0;
+		this.buffer = 0;
+	}
+	
+	public void setBitsPerBlock(int bitsPerBlock) {
+		this.bitsPerBlock = bitsPerBlock;
+	}
+	
+	public void save() throws IOException {
+		writeLong();
+	}
+	
+	public void skip(int count) {
+		this.byteIndex += count;
+		this.bitIndex = 0;
+	}
+	
+	public void writeBytes(byte[] source, int index, int length) throws IOException {
+		if(this.byteIndex + length > this.data.length) {
+			throw new IOException("No space to write.");
+		}
+		
+		System.arraycopy(source, index, this.data, this.byteIndex, length);
+		
+		this.byteIndex += length;
+	}
+	
+	public void writeBlockBits(long bits) throws IOException {
+		if(this.bitIndex >= 64) {
+			writeLong();
+			this.bitIndex = 0;
+		}
+		
+		int leftBits = 64 - this.bitIndex;
+		
+		this.buffer |= bits << this.bitIndex; 
+		
+		if(leftBits >= this.bitsPerBlock) {
+			this.bitIndex += this.bitsPerBlock;
+		} else {
+			writeLong();
+			
+			this.buffer = bits >>> leftBits;
+			
+			this.bitIndex = this.bitsPerBlock - leftBits;
+		}
+	}
+	
+	private void writeLong() throws IOException {
+		if(this.byteIndex + 7 >= this.data.length) {
+			throw new IOException("No space to write.");
+		}
+		
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 56);
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 48);
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 40);
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 32);
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 24);
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 16);
+		this.data[this.byteIndex++] = (byte)(this.buffer >> 8);
+		this.data[this.byteIndex++] = (byte)this.buffer;
+		
+		this.buffer = 0;
+	}
+	
+	public void writeVarInt(int value) throws IOException {
+		while((value & ~0x7F) != 0) {
+            writeByte((value & 0x7F) | 0x80);
+            value >>>= 7;
+        }
+
+        writeByte(value);
+    }
+	
+	public void writeByte(int value) throws IOException {
+		if(this.byteIndex >= this.data.length) {
+			throw new IOException("No space to write.");
+		}
+		
+		this.data[this.byteIndex++] = (byte)value;
+	}
+}
diff --git a/src/main/java/com/encrox/instancedregions/packet/SpawnEntity.java b/src/main/java/com/encrox/instancedregions/packet/SpawnEntity.java
new file mode 100755
index 0000000..3e9d77d
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/packet/SpawnEntity.java
@@ -0,0 +1,13 @@
+package com.encrox.instancedregions.packet;
+
+import java.util.UUID;
+
+public class SpawnEntity {
+	
+	public int id, data;
+	public UUID uuid;
+	public byte type, pitch, yaw;
+	public double x, y, z;
+	public short velX, velY, velZ;
+
+}
diff --git a/src/main/java/com/encrox/instancedregions/types/BlockState.java b/src/main/java/com/encrox/instancedregions/types/BlockState.java
new file mode 100755
index 0000000..689f382
--- /dev/null
+++ b/src/main/java/com/encrox/instancedregions/types/BlockState.java
@@ -0,0 +1,11 @@
+/**
+ * @author Aleksey Terzi
+ *
+ */
+ 
+package com.encrox.instancedregions.types;
+
+public class BlockState {
+	public int id;
+	public int meta;
+}
\ No newline at end of file
-- 
cgit v1.2.1