public class Debug {
	public static final int LVL_INFO = 2;
	public static final int LVL_WARN = 1;
	public static final int LVL_ERROR = 0;

	public static final int LEVEL_OF_LOG = 1;

	// -1: Turn off debugging
	// 0: Errors
	// 1: Warnings and errors
	// 2: Info, warnings and errors
	
	static SimpleDateFormat sdf = new SimpleDateFormat("h:mm:ss a");
	
	public static void init() {
	}
	
	public static String getTime() {
		return sdf.format(new Timestamp(new Date().getTime()));
	}
	
	public static void info(String message) {
		consoleOutput("INFO", LVL_INFO, message);
	}	
	public static void warning(String message) {
		consoleOutput("WARNING", LVL_WARN, message);
	}
	public static void error(String message) {
		consoleOutput("SEVERE", LVL_ERROR, message);
	}
	
	public static void consoleOutput(String prefix, int level, String message) {
		if (LEVEL_OF_LOG >= level)
			System.out.println(getTime() + ":" + prefix + " : " + message);
	}
} public class Main {
	
	// VERSION: <Alpha/Beta/Release> <Version> Snapshot <Year>w<Week><Build>
	
	public static final String VERSION_NO = "Pre-Alpha 0.6.0 Release Snapshot 15w022";
	
	public static final int FPS = 60;
	
	public static RenderEngine renderer = new RenderEngine();
	
	public static int width;
	public static int height;
	
	public static Vector3f spawn = new Vector3f();
	public static Player player;
	
	public static float dt;
	private long lastTime;
	
	public static int chunkUpdates = 0;
	public static int verts = 0;
	public static int blocks = 0;
	
	public Timer saveTimer = new Timer();
	public Timer tickTimer = new Timer();
	public Timer chunkTimer = new Timer();
	public Timer physicsTimer = new Timer();
	
	private boolean physicsTimerStarted = false;
	
	public static final Vector3f dawn = new Vector3f(0, 0, 0.5f);
	public static final Vector3f morning = new Vector3f(0, 0, 0.75f);
	public static final Vector3f afternoon = new Vector3f(0, 0.5f, 1);
	public static final Vector3f evening = new Vector3f(0, 0, 0.7f);
	public static final Vector3f night = new Vector3f(0, 0, 0.5f);
	public static final Vector3f midnight = new Vector3f(0, 0, 0);
	
	public static int CHUNK_LOAD_RADIUS = 2;
	
	public static boolean[][] chunkLoads = null;
		
	public static void main(String[] args) {
		try {
			Debug.info("Space Cubes version is " + VERSION_NO);
			System.out.println();

			Main Test1 = new Main();
			Debug.info("Created Space Cubes.");
			Test1.start();
		}
		catch (Exception e) {
			crash(e, null, 1);
		}
	}
	
	public void start() {
		// Check for 64-bit
		
		if (!Sys.is64Bit()) Debug.warning("It is better you run Space Cubes in 64-bit. But it's ok.");
		
		try {
			Display.setDisplayMode(new DisplayMode(800,800));
			Display.create();
			Display.setTitle("Space Cubes " + VERSION_NO);
			Display.sync(FPS);
			
			Debug.info("Created display.");
			
			// Get width and height
			
			width = Display.getWidth();
			height = Display.getHeight();
			
			// Lock mouse in frame
			
			Mouse.setGrabbed(true);
		} catch (LWJGLException e) {
			Debug.error("Space Cubes has crashed on startup!");
			Debug.error("-- BEGIN ERROR REPORT --");
			e.printStackTrace();
			Debug.error("-- END ERROR REPORT --");
			Debug.error("Please submit this report to limdingwen@gmail.com.");
			
			System.exit(1);
		}	
		
		// Init player
		
		player = new Player(null, new Vector3f(), 
				new Vector3f(0,0,0), 0, null);
		
		renderer.init();
		
		player.addToStage(RenderEngine.getStage());
		
		PluginEngine.loadPlugins();
		
		Debug.info("Initialized renderer.");
		
		Debug.info("Started Space Cubes!");
		
		saveTimer.scheduleAtFixedRate(new SaveTask(), 5000, 5000);
		tickTimer.scheduleAtFixedRate(new TickUpdateTask(), 0, 50);
		chunkTimer.scheduleAtFixedRate(new ChunkManagementTask(), 0, 200);

		while (!Display.isCloseRequested()) {
			long timeUsed = Sys.getTime();
			
			renderer.render();
			renderer.update();
			
			long renderTime = Sys.getTime() - timeUsed;
			int rrenderTime = (int) Math.floor(renderTime);
			
			long processTime = Sys.getTime();
			IOEngine.poll();
			player.playerUpdate();
			
			processTime = Sys.getTime() - processTime;
			int rprocessTime = (int) Math.floor(processTime);

			// Manage chunks
			
			Vector2i playerChunkPos = World.getChunkPosAtBlockCoords(
					World.coordRealToBlock(Main.player.translation));
			
			chunkLoads = new boolean[World.WORLD_LENGTH][World.WORLD_LENGTH];
			
			for (int ix = 0; ix < World.WORLD_LENGTH; ix++) {
				for (int iz = 0; iz < World.WORLD_LENGTH; iz++) {
					chunkLoads[ix][iz] = false;
					RenderEngine.world.chunks[ix][iz].render = false;
				}
			}
			
			for (int ix = playerChunkPos.x - CHUNK_LOAD_RADIUS; ix <= playerChunkPos.x + CHUNK_LOAD_RADIUS; ix++) {
				for (int iz = playerChunkPos.y - CHUNK_LOAD_RADIUS; iz <= playerChunkPos.y + CHUNK_LOAD_RADIUS; iz++) {
					int ix2 = ix - playerChunkPos.x;
					int iz2 = iz - playerChunkPos.y;
					
					byte sx = 0;
					byte sz = 0;
					
					if (ix2 < 0) sz = -1;
					else if (ix2 > 0) sz = 1;
					
					if (iz2 < 0) sx = -1;
					else if (iz2 > 0) sx = 1;
					
					// Set up rules
					
					byte ruleX = 0;
					byte ruleZ = 0;
					
					switch (player.direction) {
					case Player.NORTH: ruleZ = 1; break;
					case Player.NORTHEAST: ruleZ = 1; ruleX = 1; break;
					case Player.EAST: ruleX = 1; break;
					case Player.SOUTHEAST: ruleZ = -1; ruleX = 1; break;
					case Player.SOUTH: ruleZ = -1; break;
					case Player.SOUTHWEST: ruleZ = -1; ruleX = -1; break;
					case Player.WEST: ruleX = -1; break;
					case Player.NORTHWEST: ruleZ = 1; ruleX = -1; break;
					}

					if (sx == 0) sx = ruleX;
					if (sz == 0) sz = ruleZ;
										
					if ((sx == ruleX || ruleX == 0) && (sz == ruleZ || ruleZ == 0)) {
						try {
							RenderEngine.world.chunks[ix][iz].render = true;
						}
						catch (ArrayIndexOutOfBoundsException e) {
						}
					}
					
					try {
						chunkLoads[ix][iz] = true;
					}
					catch (ArrayIndexOutOfBoundsException e) {
					}
				}
			}
			
			Chunk playerChunk = null;
			
			try {
				playerChunk = RenderEngine.world.chunks[playerChunkPos.x][playerChunkPos.y];
			}
			catch (ArrayIndexOutOfBoundsException e) {
			}

			if (playerChunk != null) {
				if (!playerChunk.getIsLoaded()) {	
					// Load the playerChunk
					
					try {
						playerChunk.blocks = ChunkLevelParser.parseChunk("TestWorld", playerChunkPos.x, playerChunkPos.y).blocks;
						playerChunk.moveGraphics();
					} catch (IOException e) {
						Debug.warning("Unable to parse playerChunk " + playerChunkPos.x + ", " + playerChunkPos.y);
						Debug.warning("Generating playerChunk!");

						Logger.getLogger(ChunkLevelParser.class.getName()).log(Level.INFO, null, e);

						// Generate new playerChunk

						playerChunk.generate();
						playerChunk.moveGraphics();
					}

					playerChunk.loadChunk();
				}
			}
			
			if (!physicsTimerStarted) {
				physicsTimer.schedule(player.physic, 0, 15);
				physicsTimerStarted = true;
			}

			// Time sync
			
			dt = (Sys.getTime() - lastTime)/4f;
			lastTime = Sys.getTime();

			long fps;
			
			try {
				fps = Sys.getTimerResolution()/(Sys.getTime() - timeUsed);
			}
			catch (ArithmeticException e) {
				fps = 9001;
			}
			int rfps = (int) Math.floor(fps);
			
			Display.setTitle(
					"R:" + Integer.toString(rrenderTime) +
					"ms P: " + Integer.toString(rprocessTime) +
					"ms FPS: " + Integer.toString(rfps) +
					" CUpdt: " + Integer.toString(chunkUpdates) + 
					" V: " + Integer.toString(verts) +
					" B: " + Integer.toString(blocks) +
					" Hand: " + player.inHand.name + 
					" Time: " + (int) ((Math.floor(RenderEngine.world.time/60/20)+6)%24) + 
					":" + (int) Math.floor((RenderEngine.world.time/20)%60) + 
					" Mem: " + (Runtime.getRuntime().totalMemory() - 
									Runtime.getRuntime().freeMemory())
									/ Runtime.getRuntime().maxMemory() * 100 + "%" +
					" Dir: " + Player.decodeDirectionToString(player.direction));
			
			verts = 0;
			chunkUpdates = 0;
			blocks = 0;
			
			// Update display
		    Display.update();
			
			// Update sky color
			
			if (RenderEngine.world.time >= World.DAWN && RenderEngine.world.time < World.MORNING) GL11.glClearColor(dawn.x, dawn.y, dawn.z, 1);
			if (RenderEngine.world.time >= World.MORNING && RenderEngine.world.time < World.AFTERNOON) GL11.glClearColor(morning.x, morning.y, morning.z, 1);
			if (RenderEngine.world.time >= World.AFTERNOON && RenderEngine.world.time < World.EVENING) GL11.glClearColor(afternoon.x, afternoon.y, afternoon.z, 1);
			if (RenderEngine.world.time >= World.EVENING && RenderEngine.world.time < World.NIGHT) GL11.glClearColor(evening.x, evening.y, evening.z, 1);
			if (RenderEngine.world.time >= World.NIGHT && RenderEngine.world.time < World.MIDNIGHT) GL11.glClearColor(night.x, night.y, night.z, 1);
			if (RenderEngine.world.time >= World.MIDNIGHT) GL11.glClearColor(midnight.x, midnight.y, midnight.z, 1);
		
			if (fps > 60) {
				
			}
		}
		
		// Stopped (out of main loop)
		
		// Save level
				
		Display.destroy();
		ChunkLevelEncoder.encodeWorld("TestWorld", RenderEngine.world, true);
		try {
			LevelDataEncoder.encodeLevel("TestWorld");
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		tickTimer.cancel();
		saveTimer.cancel();
		chunkTimer.cancel();
		physicsTimer.cancel();

		Debug.info("Stopped timers");

		PluginEngine.disablePlugins();
		
		System.out.println();
		Debug.info("Destroyed display");
		Debug.info("Stopped with no errors!");
	}
	
	public static void crash(Throwable e, String message, int status) {
		Debug.error("Space Cubes has crashed!");
		Debug.error("== BEGIN FATAL ERROR REPORT ==");
		Debug.error("Respondant message:");
		
		if (message != null) {
			Debug.error(message);
		}
		else {
			Debug.error("There's no message, so this is an unexpected error!");
		}
		
		Logger.getLogger("CrashReporter").log(Level.SEVERE, "CrashStackTrace", e);
		
		Debug.error("== END FATAL ERROR REPORT ==");
		Debug.error("System exitted with status " + status);
		
		System.exit(status);
	}
	
	public static void cannotKeepUp() {
		Debug.warning("Cannot keep up! Did the system time change, or is the software overloaded?");
	}
} public class Block {
	public Vector3i translation = null;
	
	public byte material;
	public byte meta;
	public BoxCollider col;
	
	public boolean front = false;
	public boolean left = false;
	public boolean right = false;
	public boolean up = false;
	public boolean bottom = false;
	public boolean back = false;
	public boolean comCulled = false;
	
	public boolean notUnderTheSun = true;
	
	public Vector2i parentChunkPos;
	
	public Block(byte mat, byte m, Vector3i t, Vector2i pcp) {	
		translation = t;
		
		material = mat;
		meta = m;
		
		parentChunkPos = pcp;
	}
	
	public static void updateBlock(Block block, boolean counted, boolean spreadLighting) {
		if (counted) Main.chunkUpdates++;
		
		// Update collision
		
		float bs = BlockRenderEngine.BLOCK_SIZE;
		
		if (Material.getMaterialFromID(block.material, block.meta).fullCollider && Material.getMaterialFromID(block.material, block.meta).collidable) {
			Vector3f p1 = new Vector3f(
					block.translation.x * (bs * 2) - bs,
					block.translation.y * (bs * 2) - bs,
					block.translation.z * (bs * 2) - bs);
			Vector3f p2 = new Vector3f(
					block.translation.x * (bs * 2) + bs,
					block.translation.y * (bs * 2) + bs,
					block.translation.z * (bs * 2) + bs);
			
			block.col = new BoxCollider(p1, p2);
		}
		else {
			block.col = null;
		}
		
		// Update rendering faces

		World w = RenderEngine.world;
		// Culling
		// Less writing and better performance by getting the pointer first
		Chunk ch = w.getChunkAtBlockCoords(block.translation);
		Block[][][] b = ch.blocks;
		// Get local block coordinates
		Vector3i c = World.getLocalBlockCoords(block.translation);
		// get chunk length
		int cl = World.CHUNK_LENGTH;

		// Check if block is beside transparent ones (culling & *critically* better performance)

		block.front =
				block.left=
						block.right=
								block.up=
										block.bottom=
												block.back=
				false; // Set rendering to false, then set to true later on.

		boolean edge = false;

		if (c.x+1 >= cl || // X+
				c.x-1 < 0 || // X- 
				c.z+1 >= cl || // Z+
				c.z-1 < 0 || // Z-
				c.y+1 >= World.CHUNK_HEIGHT || // Y+
				c.y-1 < 0) // Y-
			edge = true;

		if (edge) {
			if (!(c.y+1 >= World.CHUNK_HEIGHT)) {
				if (Material.getMaterialFromID(b[(int) c.x][(int) c.y+1][(int) c.z].material, block.meta).seeThru) block.up = true;
			}
			if (!(c.y-1 < 0)) {
				if (Material.getMaterialFromID(b[(int) c.x][(int) c.y-1][(int) c.z].material, block.meta).seeThru) block.bottom = true;
			}
			if (!(c.x+1 >= World.CHUNK_LENGTH)) {
				if (Material.getMaterialFromID(b[(int) c.x+1][(int) c.y][(int) c.z].material, block.meta).seeThru) block.right = true;
			}
			if (!(c.x-1 < 0)) {
				if (Material.getMaterialFromID(b[(int) c.x-1][(int) c.y][(int) c.z].material, block.meta).seeThru) block.left = true;
			}
			if (!(c.z+1 >= World.CHUNK_LENGTH)) {
				if (Material.getMaterialFromID(b[(int) c.x][(int) c.y][(int) c.z+1].material, block.meta).seeThru) block.front = true;
			}
			if (!(c.z-1 < 0)) {
				if (Material.getMaterialFromID(b[(int) c.x][(int) c.y][(int) c.z-1].material, block.meta).seeThru) block.back = true;
			}
		}

		if (!edge) {
			if (Material.getMaterialFromID(b[(int) c.x+1][(int) c.y][(int) c.z].material, block.meta).seeThru) block.right = true;
			if (Material.getMaterialFromID(b[(int) c.x-1][(int) c.y][(int) c.z].material, block.meta).seeThru) block.left = true;
			if (Material.getMaterialFromID(b[(int) c.x][(int) c.y+1][(int) c.z].material, block.meta).seeThru) block.up = true;
			if (Material.getMaterialFromID(b[(int) c.x][(int) c.y-1][(int) c.z].material, block.meta).seeThru) block.bottom = true;
			if (Material.getMaterialFromID(b[(int) c.x][(int) c.y][(int) c.z+1].material, block.meta).seeThru) block.front = true;
			if (Material.getMaterialFromID(b[(int) c.x][(int) c.y][(int) c.z-1].material, block.meta).seeThru) block.back = true;
		}
		
		if (block.up || block.bottom || block.left ||
				block.right || block.front || block.back) {
			block.comCulled = false;
		}
		else {
			block.comCulled = true;
		}
		
		Block.updateLighting(block, spreadLighting);
	}
	
	public static void updateLighting(Block block, boolean spread) {
		Chunk ch = RenderEngine.world.getChunkAtBlockCoords(block.translation);
		Vector3i c = World.getLocalBlockCoords(block.translation);
		
		// Update lighting
		// Ray-trace upwards
		
		boolean isBlocked = false;
		
		for (int i = block.translation.y+1; i < World.CHUNK_HEIGHT; i++) {
			Block blockRay = ch.blocks[c.x][i][c.z];
			
			if (!Material.getMaterialFromID(blockRay.material, blockRay.meta).transparent)
				isBlocked = true;
		}
		
		block.notUnderTheSun = isBlocked;
		
		// Spread to same column

		if (spread) {
			for (int i = 0; i < World.CHUNK_HEIGHT; i++) {
				if (i != c.y) 
					Block.updateLighting(ch.blocks[c.x][i][c.z], false);
			}
		}
	}
} public class Material {
	// IDs
	
	public static final Material AIR = new Material().setName("Air").setColor(0, 0, 0).setTransparent().
			setNonCollidable().setNotSolid().setSeeThru().setID((byte) 0);
	public static final Material DIRT = new Material().setName("Dirt").setColor(0.8f, 0.47f, 0.27f).setID((byte) 1);
	public static final Material ROCK = new Material().setName("Rock").setColor(1, 1, 1).setID((byte) 2);
	public static final Material VOIDROCK = new Material().setName("VoidRock").setColor(1, 0, 0).setID((byte) 3);
	public static final Material GRASS = new Material().setName("Grass").setColor(0, 1, 0).setID((byte) 1).setMeta((byte) 0x1);
	public static final Material CRACKEDROCK = new Material().setName("Cracked Rock").setColor(0.7f, 0.7f, 0.7f).setID((byte) 2).setMeta((byte) 0x1);
	
	public Vector3f color;
	public boolean transparent = false;
	public boolean collidable = true;
	public boolean fullCollider = true;
	public boolean seeThru = false;
	public String name = "Unnamed";
	public boolean notSolid = false;
	public byte id;
	public byte meta = 0x00000000;
	
	public Material setColor(float r, float g, float b) {
		color = new Vector3f(r, g, b);
		return this;
	}
	
	public Material setTransparent() {
		transparent = true;
		return this;
	}
	
	public Material setNonCollidable() {
		collidable = false;
		return this;
	}
	
	public Material setNonFullCollider() {
		fullCollider = false;
		return this;
	}
	
	public Material setName(String n) {
		name = n;
		return this;
	}
	
	public Material setNotSolid() {
		notSolid = true;
		return this;
	}
	
	public Material setID(byte i) {
		id = i;
		return this;
	}
	
	public Material setMeta(byte m) {
		meta = m;
		return this;
	}
	
	public Material setSeeThru() {
		seeThru = true;
		return this;
	}
	
	public static Material getMaterialFromID(byte id, byte meta) {
		switch (id) {
		case 0: return AIR;
		case 1:
			switch (meta) {
				case 0: return DIRT;
				case 1: return GRASS;
				default: return AIR;
			}
		case 2:
			switch (meta) {
				case 0: return ROCK;
				case 1: return CRACKEDROCK;
				default: return AIR;
			}
		case 3: return VOIDROCK;
		default: return AIR;
		}
	}
} public class Vector2i {
	public int x;
	public int y;
	
	public Vector2i(int x, int y) {
		this.x = x;
		this.y = y;
	}

	public Vector2i() {
		x = 0;
		y = 0;
	}
	
	public String toString() {
		return x + "," + y;
	}
	
	public boolean equals(Vector2i vector) {
		return (x == vector.x && y == vector.y);
	}
} public class Vector3i {
	public int x;
	public int y;
	public int z;
	
	public Vector3i(int x, int y, int z) {
		this.x = x;
		this.y = y;
		this.z = z;
	}
	
	public String toString() {
		return "Vector3i[" + x + "," + y + "," + z + "]";
	}
} public class LevelData {
	public Vector3f playerPosition;
	public Vector3f playerRotation;
	
	public int time;
} public class LevelDataParser {	
	public static LevelData parseLevel(String worldName) throws IOException {
		DataInputStream dataStream;
		
		try {
		dataStream = new DataInputStream(new FileInputStream(
				new File(SystemNativesHelper.defaultDirectory() + "/saves/" + worldName + "/level.dat")));
		}
		catch (FileNotFoundException e) {
			Debug.warning("Cannot load level data for " + worldName + ": Cannot find file!");
			
			throw new IOException();
		}
		
		LevelData ld = new LevelData();
		
		float x = dataStream.readFloat();
		float y = dataStream.readFloat();
		float z = dataStream.readFloat();
		
		float xr = dataStream.readFloat();
		float yr = dataStream.readFloat();
		float zr = dataStream.readFloat();
		
		int time = dataStream.readInt();
		
		Main.spawn.x = dataStream.readFloat();
		Main.spawn.y = dataStream.readFloat();
		Main.spawn.z = dataStream.readFloat();
		
		dataStream.close();
		
		ld.playerPosition = new Vector3f(x,y,z);
		ld.playerRotation = new Vector3f(xr,yr,zr);
		ld.time = time;
		
		Debug.info("Successfully parsed level.dat");
		
		return ld;
	}
} public class Chunk {
	public Block[][][] blocks = new Block[World.CHUNK_LENGTH][World.CHUNK_HEIGHT][World.CHUNK_LENGTH];
	private boolean isLoaded = false;
	
	// Positions are based on chunk coords
	//
	// -----
	// |0,0 |
	// |    | (1 unit is one chunk.)
	// -----
	//
	protected int positionX;
	protected int positionY;
	
	public boolean render = true;
	
	public Chunk(Block[][][] b, int x, int y) {
		if (b != null) blocks = b.clone();
		
		positionX = x;
		positionY = y;
	}
	
	public int getPositionX() {
		return positionX;
	}

	public int getPositionY() {
		return positionY;
	}

	public Chunk generate() {
		System.out.println("(Re)generating chunk " + this.toString()+ "....");
		
		// Generate air and initialize chunk
		
		for (int ix = 0; ix < World.CHUNK_LENGTH; ix++) {
			for (int iy = 0; iy < World.CHUNK_HEIGHT; iy++) {
				for (int iz = 0; iz < World.CHUNK_LENGTH; iz++) {
					blocks[ix][iy][iz] = new Block(Material.AIR.id, Material.AIR.meta, new Vector3i(ix,iy,iz), new Vector2i(positionX, positionY));
				}
			}
		}
		
		// Generate dirt
	
		for (int ix = 0; ix < World.CHUNK_LENGTH; ix++) {
			for (int iz = 0; iz < World.CHUNK_LENGTH; iz++) {
				for (int iy = 0; iy < 65; iy++) {
					if (iy < 64)
					blocks[ix][iy][iz].material = Material.DIRT.id;
					
					if (iy >= 63 && iy < 64) {
						if (Math.floor(Math.random() * 2) == 0) {
							blocks[ix][iy][iz].material = Material.GRASS.id;
							blocks[ix][iy][iz].meta = Material.GRASS.meta;
						}
					}
					else if (iy == 64) {
						if (Math.floor(Math.random() * 4) == 0) {
							blocks[ix][iy][iz].material = Material.GRASS.id;
							blocks[ix][iy][iz].meta = Material.GRASS.meta;
						}
					}
				}
			}
		}
		
		System.out.println("Generated dirt.");
		
		// Generate stone
		
		for (int ix = 0; ix < World.CHUNK_LENGTH; ix++) {
			for (int iz = 0; iz < World.CHUNK_LENGTH; iz++) {
				for (int iy = 0; iy < 60; iy++) {
					if (Math.floor(Math.random() * (1 + iy/600)) == 0) {
						if (Math.floor(Math.random() * (0.5 + iy/40)) == 0) {
							blocks[ix][iy][iz].material = Material.ROCK.id;
						}
						else {
							blocks[ix][iy][iz].material = Material.CRACKEDROCK.id;
							blocks[ix][iy][iz].meta = Material.CRACKEDROCK.meta;
						}
					}
				}
			}
		}
		
		System.out.println("Generated rock");
		
		// Generate voidrock
		
		for (int ix = 0; ix < World.CHUNK_LENGTH; ix++) {
			for (int iz = 0; iz < World.CHUNK_LENGTH; iz++) {
				for (int iy = 0; iy < 3; iy++) {
					if (iy == 0) {
						blocks[ix][iy][iz].material = Material.VOIDROCK.id;
					}
					else if (iy == 1) {
						if (Math.floor(Math.random() * 4) == 0) {
							blocks[ix][iy][iz].material = Material.VOIDROCK.id;
						}
					}
					else if (iy == 2) {
						if (Math.floor(Math.random() * 8) == 0) {
							blocks[ix][iy][iz].material = Material.VOIDROCK.id;
						}
					}
				}
			}
		}
		
		System.out.println("Generated voidrock layer.");
		System.out.println("Finished (re)generating chunk " + this.toString() + "!");
				
		return this;
	}
	
	public void moveGraphics() {
		GraphicsMoveRunnable runnable = new GraphicsMoveRunnable(blocks, positionX, positionY);
		runnable.run();
		
		Debug.info("Updated graphics and collision to chunk coords relative.");
	}
	
	public void update(Vector3i pos, boolean spread, boolean counted) {
		int cl = World.CHUNK_LENGTH;
		
		Block.updateBlock(blocks[(int) pos.x][(int) pos.y][(int) pos.z], true, true);
		
		if (spread) {
			try {
				Block.updateBlock(blocks[(int) pos.x + 1][(int) pos.y][(int) pos.z], true, true);
			}
			catch (Exception e) {
			}

			try {
				Block.updateBlock(blocks[(int) pos.x - 1][(int) pos.y][(int) pos.z], true, true);
			}
			catch (Exception e) {
			}

			try {
				Block.updateBlock(blocks[(int) pos.x][(int) pos.y + 1][(int) pos.z], true, true);
			}
			catch (Exception e) {
			}

			try {
				Block.updateBlock(blocks[(int) pos.x][(int) pos.y - 1][(int) pos.z], true, true);
			}
			catch (Exception e) {
			}

			try {
				Block.updateBlock(blocks[(int) pos.x][(int) pos.y][(int) pos.z + 1], true, true);
			}
			catch (Exception e) {
			}

			try {
				Block.updateBlock(blocks[(int) pos.x][(int) pos.y][(int) pos.z - 1], true, true);
			}
			catch (Exception e) {
			}

			// Spread update to nearby blocks at nearby chunks.

			boolean morex = false;
			boolean lessx = false;
			boolean morez = false;
			boolean lessz = false;

			if (pos.x+1>=cl) morex = true;
			if (pos.x-1<0) lessx = true;
			if (pos.z+1>=cl) morez = true;
			if (pos.z-1<0) lessz = true;

			if (morex||lessx||morez||lessz) {
				World world = RenderEngine.world;
				Chunk[][] cs = world.chunks;
				Vector2i cpos = World.getChunkPosAtBlockCoords(pos);
				
				if (morex) {
					try {
						cs[cpos.x+1][cpos.y].update(new Vector3i(
								15-pos.x,
								pos.y,
								pos.z), false, true);
					}
					catch (Exception e) {
					}
				}

				if (lessx) {
					try {
						cs[(int) (cpos.x-1)][(int) (cpos.y)].update(new Vector3i(
								15-pos.x,
								pos.y,
								pos.z), false, true);
					}
					catch (Exception e) {
					}
				}

				if (morez) {
					try {
						cs[(int) (cpos.x)][(int) (cpos.y+1)].update(new Vector3i(
								pos.x,
								pos.y,
								15-pos.z), false, true);
					}
					catch (Exception e) {
					}
				}

				if (lessz) {
					try {
						cs[(int) (cpos.x)][(int) (cpos.y-1)].update(new Vector3i(
								pos.x,
								pos.y,
								15-pos.z), false, true);
					}
					catch (Exception e) {
					}
				}
			}
		}
	}

	public void loadChunk() {
		isLoaded = true;

		for (int cx = 0; cx < World.CHUNK_LENGTH; cx++) {
			for (int cy = 0; cy < World.CHUNK_HEIGHT; cy++) {
				for (int cz = 0; cz < World.CHUNK_LENGTH; cz++) {
					Block.updateBlock(blocks[cx][cy][cz], false, false);
				}
			}
		}
	}

	public void unloadChunk() {
		isLoaded = false;

		blocks = new Block[World.CHUNK_LENGTH][World.CHUNK_HEIGHT][World.CHUNK_LENGTH];
	}
	
	public void flagChunkUnload() {
		isLoaded = false;
	}
	
	public boolean getIsLoaded() {
		return isLoaded;
	}
} public class World {
	public static final int WORLD_LENGTH = 30;
	
	public Chunk[][] chunks = new Chunk[WORLD_LENGTH][WORLD_LENGTH];
	public static final int CHUNK_LENGTH = 16;
	public static final int CHUNK_HEIGHT = 128;
	
	public int time = 0;
	
	public static final int DAWN = 0;
	public static final int MORNING = 1000;
	public static final int AFTERNOON = 7600;
	public static final int EVENING = 15400;
	public static final int NIGHT = 16800;
	public static final int MIDNIGHT = 21600;
	public static final int END = 28800;
	
	public World() {
	}
	
	public void generate() {
		System.out.println("(Re)generating world " + this.toString());
		
		int total = (chunks.length + 1) * chunks.length;
		int progress = 0;
		System.out.println("Worldgen " + this.toString() + " completion " + progress + "/" + total + "!");

		for (int ix = 0; ix < chunks.length; ix++) {
			progress++;
			
			for (int iz = 0; iz < chunks.length; iz++) {
				progress++;
								
				chunks[ix][iz] = new Chunk(null, ix, iz).generate();
				
				Debug.info("Worldgen " + this.toString() + " completion " + progress + "/" + total + "!");
			}
		}
		
		Debug.info("Worldgen " + this.toString() + " completion " + progress + "/" + total + "!");
	}
	
	public static Vector3i coordRealToBlock(Vector3f pos) {
		Vector3i blockCoords = new Vector3i(
				(int) Math.floor(pos.x / (BlockRenderEngine.doubleBs)),
				(int) Math.floor(pos.y / (BlockRenderEngine.doubleBs)),
				(int) Math.floor(pos.z / (BlockRenderEngine.doubleBs)));
		
		return blockCoords;
	}
	
	public static Vector3f coordBlockToReal(Vector3i pos) {
		Vector3f blockCoords = new Vector3f(
				pos.x * (BlockRenderEngine.doubleBs),
				pos.y * (BlockRenderEngine.doubleBs),
				pos.z * (BlockRenderEngine.doubleBs));
		
	//	System.out.println(blockCoords);

		return blockCoords;
	}
	
	public void changeBlockAtToRealCoord(Vector3f pos, Material mat, boolean force) {
		changeBlockAtToBlockCoord(
				coordRealToBlock(pos),mat, force);
	}
	
	public void changeBlockAtToBlockCoord(Vector3i pos, Material mat, boolean force) {
		Vector2i chunkCoords = new Vector2i(
				pos.x / CHUNK_LENGTH,
				pos.z / CHUNK_LENGTH);
		
		chunkCoords.x = (int) Math.floor(chunkCoords.x);
		chunkCoords.y = (int) Math.floor(chunkCoords.y);
		
		Vector3i localBlockCoords = new Vector3i(
				pos.x % CHUNK_LENGTH,
				pos.y,
				pos.z % CHUNK_LENGTH);
				
		try {
			Block block = chunks[(int) chunkCoords.x]
					[(int) chunkCoords.y].blocks
					[(int) localBlockCoords.x]
							[(int) localBlockCoords.y]
									[(int) localBlockCoords.z];
			if (!force) {
				if (block.material == Material.AIR.id)	{
					block.material = mat.id;
					block.meta = mat.meta;
				}
			}
			else {
				block.material = mat.id;
				block.meta = mat.meta;
			}
			
			// Update chunks
			
			Chunk chunk = chunks[(int) chunkCoords.x]
					[(int) chunkCoords.y];
			chunk.update(localBlockCoords, true, true);
		}
		catch (ArrayIndexOutOfBoundsException e) {
			Debug.warning("Chunk refrence not in world!");
		}
	}
	
	public Chunk getChunkAtBlockCoords(Vector3i pos) {
		Vector2i chunkCoords = new Vector2i(
				pos.x / CHUNK_LENGTH,
				pos.z / CHUNK_LENGTH);
		
		chunkCoords.x = (int) Math.floor(chunkCoords.x);
		chunkCoords.y = (int) Math.floor(chunkCoords.y);
		
		try {
			return chunks[chunkCoords.x][chunkCoords.y];
		}
		catch (ArrayIndexOutOfBoundsException e) {
			return null; // Return null if chunk does not exist at that position
		}
	}
	
	public static Vector2i getChunkPosAtBlockCoords(Vector3i pos) {
		Vector2i chunkCoords = new Vector2i(
				pos.x / CHUNK_LENGTH,
				pos.z / CHUNK_LENGTH);
		
		chunkCoords.x = (int) Math.floor(chunkCoords.x);
		chunkCoords.y = (int) Math.floor(chunkCoords.y);
		
		return chunkCoords;
	}
	
	public static Vector3i getLocalBlockCoords(Vector3i pos) {
		Vector3i localBlockCoords = new Vector3i(
				pos.x % CHUNK_LENGTH,
				pos.y,
				pos.z % CHUNK_LENGTH);
		
		localBlockCoords.x = (int) Math.floor(localBlockCoords.x);
		localBlockCoords.y = (int) Math.floor(localBlockCoords.y);
		localBlockCoords.z = (int) Math.floor(localBlockCoords.z);
		
		return localBlockCoords;
	}
	
	public World setChunks(Chunk[][] chu) {
		chunks = chu;
		
		return this; // Chainability
	}
	
	public void moveGraphics() {
		for (int ix = 0; ix < WORLD_LENGTH; ix++) {
			for (int iz = 0; iz < WORLD_LENGTH; iz++) {
				if (chunks[ix][iz].getIsLoaded()) chunks[ix][iz].moveGraphics();
			}
		}
	}
	
	public Block getTopBlock(int x, int y) {
		Vector3i vec = new Vector3i(x, 0, y);
		
		Chunk ch = getChunkAtBlockCoords(vec);
		Vector3i bl = getLocalBlockCoords(vec);
		
		// Do raycast
		for (int i = CHUNK_HEIGHT-1; i >= 0; i--) {
			Block bloc = ch.blocks[bl.x][i][bl.z];
			
			if (Material.getMaterialFromID(bloc.material, bloc.meta) != Material.AIR) {
				return bloc;
			}
		}
		
		return null; // If no block is found
	}
	
	public static Vector3i toFirstChunk(Vector3i pos) {
		return new Vector3i(
				pos.x % CHUNK_LENGTH,
				pos.y,
				pos.z % CHUNK_LENGTH);
	}
} public class WorldEngine {
	public WorldEngine() {
		
	}
	
	public static World createWorld() {
		Display.setTitle("Building world....");
		
		World world = new World();
		world.generate();
		
		return world;
	}
	
	public static void loadWorldIntoMemory(World world) {
		Debug.info("Loading world " + world.toString() + " into memory...");
		world.moveGraphics();
/*
		try {
			for (int wx = 0; wx < world.chunks.length; wx++) {
				for (int wz = 0; wz < world.chunks.length; wz++) {
					// Every chunk in the world...

					world.chunks[wx][wz].loadChunk();
				}
			}

			Debug.info("World " + world.toString() + " loaded!");
		}
		catch (OutOfMemoryError e) {
			Main.crash(e, "Out of memory while loading world " + world.toString(), 2);
		}*/
	}
}
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import org.lwjgl.util.glu.GLU;
import org.lwjgl.util.vector.Vector3f;
import com.github.limdingwen.SpaceCubes.Debug;
import com.github.limdingwen.SpaceCubes.Main;
import com.github.limdingwen.SpaceCubes.BlockTypes.Block;
import com.github.limdingwen.SpaceCubes.BlockTypes.Material;
import com.github.limdingwen.SpaceCubes.DataTypes.Vector2i;
import com.github.limdingwen.SpaceCubes.DataTypes.Vector3i;
import com.github.limdingwen.SpaceCubes.File.ChunkLevelParser;
import com.github.limdingwen.SpaceCubes.File.LevelData;
import com.github.limdingwen.SpaceCubes.File.LevelDataParser;
import com.github.limdingwen.SpaceCubes.World.Chunk;
import com.github.limdingwen.SpaceCubes.World.World;
import com.github.limdingwen.SpaceCubes.World.WorldEngine;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.lwjgl.BufferUtils;
package com.github.limdingwen.SpaceCubes.Rendering;





public class RenderEngine {
	private static Stage stage = new Stage();
	public static Camera camera = new Camera(new Vector3f(0,0,0));
	public static BlockRenderEngine blockRenderEngine = new BlockRenderEngine();

	public static World world;

	public void init() {
		float density = 0.05f;
		float fogColor[] = {
				0.7f, 
				0.7f, 
				0.7f, 
				1.0f
		};
		FloatBuffer fogColorBuffer = BufferUtils.createFloatBuffer(4);
		fogColorBuffer.put(fogColor);
		fogColorBuffer.flip();

		// Init OpenGL

		GL11.glMatrixMode(GL11.GL_PROJECTION);
		GLU.gluPerspective(60, 1, 0.1f, 128);

		GL11.glMatrixMode(GL11.GL_MODELVIEW);
		//	GL11.glClearColor(0, 0.5f, 1, 1);

		GL11.glEnable(GL11.GL_DEPTH_TEST);
		GL11.glEnable(GL11.GL_FOG);
		GL11.glFogi(GL11.GL_FOG_MODE, GL11.GL_EXP2);
		GL11.glFog(GL11.GL_FOG_COLOR, fogColorBuffer);
		GL11.glFogf(GL11.GL_FOG_DENSITY, density);

		Debug.info("Initalized OpenGL");
		
		// World default

		Display.setTitle("Building world....");

		world = ChunkLevelParser.parseWorld("TestWorld");
		WorldEngine.loadWorldIntoMemory(world);

		// Set player and time

		LevelData ld = null;

		try {
			ld = LevelDataParser.parseLevel("TestWorld");