tectonicus.world.World.java Source code

Java tutorial

Introduction

Here is the source code for tectonicus.world.World.java

Source

/*
 * Copyright (c) 2012-2016, John Campbell and other contributors.  All rights reserved.
 *
 * This file is part of Tectonicus. It is subject to the license terms in the LICENSE file found in
 * the top-level directory of this distribution.  The full list of project contributors is contained
 * in the AUTHORS file found in the same location.
 *
 */

package tectonicus.world;

import java.awt.Color;
import java.awt.Point;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.bind.DatatypeConverter;

import org.json.JSONObject;
import org.lwjgl.util.vector.Vector3f;

import tectonicus.BlockContext;
import tectonicus.BlockIds;
import tectonicus.BlockMaskFactory;
import tectonicus.BlockRegistryParser;
import tectonicus.BlockType;
import tectonicus.BlockTypeRegistry;
import tectonicus.Chunk;
import tectonicus.ChunkCoord;
import tectonicus.ChunkLocator;
import tectonicus.Minecraft;
import tectonicus.NullBlockFilter;
import tectonicus.NullBlockMaskFactory;
import tectonicus.RegionCache;
import tectonicus.RegionCoord;
import tectonicus.Util;
import tectonicus.blockTypes.Air;
import tectonicus.cache.BiomeCache;
import tectonicus.cache.PlayerSkinCache;
import tectonicus.cache.PlayerSkinCache.CacheEntry;
import tectonicus.configuration.Configuration.Dimension;
import tectonicus.configuration.LightFace;
import tectonicus.configuration.LightStyle;
import tectonicus.configuration.SignFilter;
import tectonicus.rasteriser.AlphaFunc;
import tectonicus.rasteriser.BlendFunc;
import tectonicus.rasteriser.PrimativeType;
import tectonicus.rasteriser.Rasteriser;
import tectonicus.raw.BiomeIds;
import tectonicus.raw.LevelDat;
import tectonicus.raw.Player;
import tectonicus.raw.RawChunk;
import tectonicus.raw.RawSign;
import tectonicus.renderer.Camera;
import tectonicus.renderer.Geometry;
import tectonicus.texture.TexturePack;
import tectonicus.util.BoundingBox;
import tectonicus.util.Colour4f;
import tectonicus.util.Vector3l;
import tectonicus.world.filter.BlockFilter;
import tectonicus.world.filter.CompositeBlockFilter;
import tectonicus.world.subset.RegionIterator;
import tectonicus.world.subset.WorldSubset;
import tectonicus.world.subset.WorldSubsetFactory;

public class World implements BlockContext {
    private static final int BATCH_SIZE = 100;

    private String textureVersion;

    private final Rasteriser rasteriser;

    private final File worldDir;
    private final File dimensionDir;

    private BlockTypeRegistry registry;

    private LevelDat levelDat;

    private ArrayList<Player> players;
    private PlayerSkinCache playerSkinCache;

    private TexturePack texturePack;

    private RegionCache regionCache;
    private ChunkLocator chunkLocator;

    private RawCache rawLoadedChunks;
    private GeometryCache geometryLoadedChunks;

    private LightStyle lightStyle;

    private int defaultBlockId;

    private BlockFilter blockFilter;
    private BlockMaskFactory blockMaskFactory;

    private final BiomeCache biomeCache;

    private WorldSubset worldSubset;

    private Geometry daySkybox, nightSkybox;

    private SignFilter signFilter;

    public World(Rasteriser rasteriser, File baseDir, Dimension dimension, File minecraftJar, File texturePackFile,
            List<File> modJars, BiomeCache biomeCache, MessageDigest hashAlgorithm, String singlePlayerName,
            WorldSubsetFactory subsetFactory, PlayerSkinCache playerSkinCache, SignFilter signFilter) {
        this.rasteriser = rasteriser;
        this.signFilter = signFilter;

        this.defaultBlockId = BlockIds.AIR;
        this.blockFilter = new NullBlockFilter();
        this.blockMaskFactory = new NullBlockMaskFactory();

        this.worldDir = baseDir;

        // Use the world dir and the dimension to find the dimension dir
        if (dimension == Dimension.Terra) {
            dimensionDir = worldDir;
        } else if (dimension == Dimension.Nether) {
            dimensionDir = new File(worldDir, "DIM-1");
        } else if (dimension == Dimension.Ender) {
            dimensionDir = new File(worldDir, "DIM1");
        } else {
            dimensionDir = worldDir;
        }

        System.out.println("Loading world from base dir " + worldDir.getPath() + " with dimension " + dimension);
        System.out.println("\tFull dimension dir: " + dimensionDir.getAbsolutePath());

        this.biomeCache = biomeCache;
        this.playerSkinCache = playerSkinCache;

        // Check that this looks like a world dir
        if (!Minecraft.isValidWorldDir(baseDir))
            throw new RuntimeException("Invalid world dir! No level.dat found at "
                    + Minecraft.findLevelDat(baseDir).getAbsolutePath());
        if (!Minecraft.isValidDimensionDir(dimensionDir))
            throw new RuntimeException("Invalid dimension dir! No /region/*.mcr or /region/*.mca found in "
                    + dimensionDir.getAbsolutePath());

        // TODO: Better error handling here.
        // World should throw Exception?
        try {
            System.out.println("Loading level.dat");
            levelDat = new LevelDat(Minecraft.findLevelDat(baseDir), singlePlayerName);

            if (levelDat.getVersion() == LevelDat.UNKNOWN_VERSION) {
                throw new RuntimeException("Error: Alpha map format no longer supported");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        if (dimension == Dimension.Ender) {
            levelDat.setSpawnPosition(100, 49, 0); // Location of obsidian platform where the player spawns
        }

        System.out.println("Loading textures");
        texturePack = new TexturePack(rasteriser, minecraftJar, texturePackFile, modJars);
        this.textureVersion = texturePack.getVersion();

        System.out.println("Creating block registry");
        loadBlockRegistry(null, true);

        System.out.println("Loading players");
        players = loadPlayers(worldDir, playerSkinCache);

        regionCache = new RegionCache(dimensionDir);
        chunkLocator = new ChunkLocator(dimensionDir, biomeCache, regionCache);

        rawLoadedChunks = new RawCache(100);
        geometryLoadedChunks = new GeometryCache(100);

        this.worldSubset = subsetFactory.create(this);

        this.lightStyle = LightStyle.None;

        this.daySkybox = SkyboxUtil.generateDaySkybox(rasteriser);
        this.nightSkybox = SkyboxUtil.generateNightSkybox(rasteriser);
    }

    public void loadBlockRegistry(String customConfigPath, final boolean useDefaultBlocks) {
        registry = new BlockTypeRegistry();
        registry.setDefaultBlock(new Air());

        BlockRegistryParser parser = new BlockRegistryParser(texturePack, biomeCache, signFilter);

        if (useDefaultBlocks && this.textureVersion == "1.4")
            parser.parse("defaultBlockConfigMC1.4.xml", registry);
        else if (useDefaultBlocks && this.textureVersion == "1.5")
            parser.parse("defaultBlockConfigMC1.5.xml", registry);
        else if (useDefaultBlocks && this.textureVersion == "1.678")
            parser.parse("defaultBlockConfigMC1.8.xml", registry);
        else if (useDefaultBlocks)
            parser.parse("defaultBlockConfig.xml", registry);

        if (customConfigPath != null && customConfigPath.length() > 0)
            parser.parse(customConfigPath, registry);

        flushChunkCache();
        flushGeometryCache();
    }

    public WorldSubset getWorldSubset() {
        return worldSubset;
    }

    public BiomeCache getBiomeCache() {
        return biomeCache;
    }

    public void setLightStyle(LightStyle style) {
        // Clear the geometry cache if style has changed
        if (this.lightStyle != style) {
            flushChunkCache();
        }

        this.lightStyle = style;
    }

    @Override
    public LightStyle getLightStyle() {
        return lightStyle;
    }

    public void setDefaultBlockId(final int blockId) {
        // Clear the geometry cache if id has changed
        if (this.defaultBlockId != blockId) {
            flushChunkCache();
        }

        this.defaultBlockId = blockId;
    }

    public void setBlockFilter(BlockFilter filter) {
        if (filter == null)
            throw new NullPointerException();

        flushChunkCache();

        this.blockFilter = filter;
    }

    public BlockFilter getBlockFilter() {
        return blockFilter;
    }

    public PlayerSkinCache getPlayerSkinCache() {
        return playerSkinCache;
    }

    public void setBlockMaskFactory(BlockMaskFactory factory) {
        if (factory == null)
            throw new NullPointerException();

        flushChunkCache();

        this.blockMaskFactory = factory;
    }

    public RegionIterator createRegionIterator() {
        return worldSubset.createRegionIterator(regionCache.getFormat());
    }

    public boolean contains(ChunkCoord coord) {
        return worldSubset.contains(coord);
    }

    /** Gets the players for a particular dimension, or null for all players */
    public ArrayList<Player> players(Dimension dimension) {
        ArrayList<Player> result = new ArrayList<Player>();
        for (Player p : players) {
            if (dimension == null || p.getDimension() == dimension)
                result.add(p);
        }
        return result;
    }

    public int numPlayers() {
        return players.size();
    }

    public LevelDat getLevelDat() {
        return levelDat;
    }

    public TexturePack getTexturePack() {
        return texturePack;
    }

    /* New, optimised version.
     * Project the frustum vertices down onto the ground plane, and find the bounding box.
     * Then iterate over each region that touches this box and check against the frustum
     * For regions which touch the frustum, then check all contained chunks.
     * This gives us a quad-tree-esque lookup method without having to actually store a heavy quad tree in memory.
     */
    public ArrayList<ChunkCoord> findVisible(Camera camera) {
        ArrayList<ChunkCoord> result = new ArrayList<ChunkCoord>();

        // Cast verts down onto landscape
        long minX = Long.MAX_VALUE;
        long minZ = Long.MAX_VALUE;
        long maxX = Long.MIN_VALUE;
        long maxZ = Long.MIN_VALUE;
        for (Vector3f corner : camera.getFrustumVertices()) {
            RegionCoord coord = RegionCoord.fromWorldCoord(corner.x, corner.z);
            minX = Math.min(minX, coord.x);
            minZ = Math.min(minZ, coord.z);
            maxX = Math.max(maxX, coord.x);
            maxZ = Math.max(maxZ, coord.z);
        }

        // First iterate over the touched regions and find which ones touch the camera
        for (long regionX = minX; regionX <= maxX; regionX++) {
            for (long regionZ = minZ; regionZ <= maxZ; regionZ++) {
                BoundingBox regionBounds = new BoundingBox(
                        new Vector3l(regionX * RegionCoord.REGION_WIDTH * RawChunk.WIDTH, 0,
                                regionZ * RegionCoord.REGION_HEIGHT * RawChunk.DEPTH),
                        RawChunk.WIDTH * RegionCoord.REGION_WIDTH, RawChunk.HEIGHT,
                        RawChunk.DEPTH * RegionCoord.REGION_HEIGHT);
                if (regionBounds.isVisible(camera)) {

                    // Now iterate over all chunks within the region
                    for (long chunkX = 0; chunkX < RegionCoord.REGION_WIDTH; chunkX++) {
                        for (long chunkZ = 0; chunkZ < RegionCoord.REGION_HEIGHT; chunkZ++) {
                            ChunkCoord chunkCoord = new ChunkCoord(regionX * RegionCoord.REGION_WIDTH + chunkX,
                                    regionZ * RegionCoord.REGION_HEIGHT + chunkZ);

                            if (worldSubset.contains(chunkCoord)) {
                                BoundingBox chunkBounds = new BoundingBox(
                                        new Vector3l(chunkCoord.x * RawChunk.WIDTH, 0,
                                                chunkCoord.z * RawChunk.DEPTH),
                                        RawChunk.WIDTH, RawChunk.HEIGHT, RawChunk.DEPTH);
                                if (chunkBounds.isVisible(camera)) {
                                    if (chunkLocator.exists(chunkCoord)) {
                                        result.add(chunkCoord);
                                    }
                                }
                            }
                        }
                    }

                }
            }
        }

        return result;
    }

    /*
    private void unloadAll()
    {
       for (Chunk c : geometryLoadedChunks.values())
       {
     c.unloadGeometry();
       }
           
       for (Chunk c : rawLoadedChunks.values())
       {
     c.unloadRaw();
       }
           
       geometryLoadedChunks.clear();
       rawLoadedChunks.clear();
    }
    */

    public void draw(Camera camera, final boolean showSky, final boolean genAlphaMask) {
        // Find visible chunks
        ArrayList<ChunkCoord> visible = findVisible(camera);

        // Sort back to front
        Collections.sort(visible, new BackToFrontSorter(camera));

        // Clear the alpha buffer
        if (genAlphaMask) {
            rasteriser.enableColourWriting(false, true);
            rasteriser.enableDepthTest(false);
            rasteriser.enableBlending(false);

            rasteriser.beginShape(PrimativeType.Quads);
            {
                if (genAlphaMask)
                    rasteriser.colour(0, 0, 0, 0);
                else
                    rasteriser.colour(0, 0, 0, 1);

                Vector3f[] quad = camera.getClearQuad();

                Vector3f offset = new Vector3f(camera.getForward());
                offset.scale(+0.20f); // Ugh. Arbitrary fudge factor because camera.getClearQuad doesn't seem to quite work for perspective camera 
                for (Vector3f v : quad) {
                    v.x += offset.x;
                    v.y += offset.y;
                    v.z += offset.z;
                }

                rasteriser.vertex(quad[0].x, quad[0].y, quad[0].z);
                rasteriser.vertex(quad[1].x, quad[1].y, quad[1].z);
                rasteriser.vertex(quad[2].x, quad[2].y, quad[2].z);
                rasteriser.vertex(quad[3].x, quad[3].y, quad[3].z);
            }
            rasteriser.endShape();

            rasteriser.enableDepthTest(true);
            rasteriser.enableColourWriting(true, false);
        }

        // Skybox first
        if (showSky) {
            Geometry skybox = lightStyle == LightStyle.Night ? nightSkybox : daySkybox;

            skybox.drawSolidSurfaces(camera.getEyePosition().x, camera.getEyePosition().y,
                    camera.getEyePosition().z);
            rasteriser.clearDepthBuffer();
        }

        // Now the chunks in batches
        ArrayList<ChunkCoord> toProcess = new ArrayList<ChunkCoord>(visible);
        while (!toProcess.isEmpty()) {
            ArrayList<ChunkCoord> batch = new ArrayList<ChunkCoord>();

            final int nextBatchSize = Math.min(BATCH_SIZE, toProcess.size());
            for (int i = 0; i < nextBatchSize; i++) {
                batch.add(toProcess.remove(0));
            }

            draw(camera, batch, genAlphaMask);
        }
    }

    private void draw(Camera camera, ArrayList<ChunkCoord> visible, final boolean genAlphaMask) {
        // Find adjacent chunks (need adjacent chunks loaded for proper geometry gen)
        Set<ChunkCoord> adjacent = new HashSet<ChunkCoord>();
        adjacent.addAll(visible);
        for (ChunkCoord base : visible) {
            adjacent.add(new ChunkCoord(base.x + 1, base.z));
            adjacent.add(new ChunkCoord(base.x - 1, base.z));
            adjacent.add(new ChunkCoord(base.x, base.z + 1));
            adjacent.add(new ChunkCoord(base.x, base.z - 1));
        }
        adjacent.remove(null);

        for (ChunkCoord coord : adjacent) {
            // Load raw if not present
            if (!rawLoadedChunks.contains(coord)) {
                if (worldSubset.contains(coord)) {
                    CompositeBlockFilter composite = new CompositeBlockFilter();
                    composite.add(blockFilter);
                    composite.add(worldSubset.getBlockFilter(coord));

                    // Not loaded, so load it now
                    Chunk c = chunkLocator.loadChunk(coord, composite);
                    if (c != null) {
                        rawLoadedChunks.put(coord, c);
                    }
                }
            } else {
                rawLoadedChunks.touch(coord);
            }
        }

        for (ChunkCoord coord : visible) {
            // Create geometry if not present
            if (!geometryLoadedChunks.contains(coord)) {
                Chunk c = rawLoadedChunks.get(coord);
                if (c != null) {
                    // Actually create the geometry

                    final boolean ok = c.createGeometry(rasteriser, this, registry, blockMaskFactory, texturePack);
                    assert ok;

                    geometryLoadedChunks.put(coord, c);
                }
            } else {
                geometryLoadedChunks.touch(coord);
            }
        }

        // Now actually find all visible chunks we've managed to load
        ArrayList<Chunk> visibleChunks = new ArrayList<Chunk>();
        for (ChunkCoord coord : visible) {
            Chunk c = geometryLoadedChunks.get(coord);
            if (c != null)
                visibleChunks.add(c);
        }
        //System.out.println("Num visible chunks: " + visibleChunks.size());
        rasteriser.enableDepthTest(true);
        rasteriser.setBlendFunc(BlendFunc.Regular);

        drawGeometry(camera, visibleChunks);

        // Render the alpha mask
        if (genAlphaMask) {
            rasteriser.enableBlending(true);
            rasteriser.enableDepthTest(true);

            rasteriser.enableColourWriting(false, true);

            // Write the new alpha values
            {
                rasteriser.setBlendFunc(BlendFunc.Additive);

                drawGeometry(camera, visibleChunks);

                rasteriser.setBlendFunc(BlendFunc.Regular);
            }

            rasteriser.enableColourWriting(true, false);
        }

        rawLoadedChunks.trimToMaxSize();
        geometryLoadedChunks.trimToMaxSize();
    }

    private void drawGeometry(Camera camera, ArrayList<Chunk> visible) {
        rasteriser.enableDepthWriting(true);

        // Solid pass

        rasteriser.enableBlending(false);
        rasteriser.enableAlphaTest(false);

        for (Chunk c : visible) {
            c.drawSolid(camera);
        }

        // Alpha test pass

        rasteriser.enableAlphaTest(true);
        rasteriser.setAlphaFunc(AlphaFunc.Greater, 0.6f); // TODO: Figure out what this value should actually be
        rasteriser.enableBlending(false);

        for (Chunk c : visible) {
            c.drawAlphaTestedSurfaces(camera);
        }

        // Transparency pass

        rasteriser.enableDepthWriting(false); //TODO: This is the cause of the weirdness involving ice as well as glass blocks in a beam
        rasteriser.enableBlending(true);
        rasteriser.enableAlphaTest(false);

        for (Chunk c : visible) {
            c.drawTransparentSurfaces(camera);
        }

        rasteriser.enableDepthWriting(true);
    }

    public static ChunkCoord getTileCoord(File datFile) {
        try {
            // "c.0.0.dat"
            String name = datFile.getName();

            final int datPos = name.indexOf(".dat");
            if (datPos != -1) {
                name = name.substring(0, datPos);
                final int dotPos = name.indexOf('.');
                if (dotPos != -1 && dotPos < name.length() - 1) {
                    name = name.substring(dotPos + 1, name.length());

                    long first = Util.fromBase36(name.substring(0, name.indexOf('.')));
                    long second = Util.fromBase36(name.substring(name.indexOf('.') + 1, name.length()));

                    return new ChunkCoord(first, second);
                }
            }
        } catch (Exception e) {
            System.err.println("Couldn't get tile coord from " + datFile.getAbsolutePath() + " (" + e + ")");
            System.err.println("File will be ignored");
        }

        return null;
    }

    private Location resolve(ChunkCoord chunkCoord, int x, int y, int z) {
        long chunkX = chunkCoord.x;
        long chunkZ = chunkCoord.z;

        while (x < 0) {
            x += RawChunk.WIDTH;
            chunkX--;
        }
        while (x >= RawChunk.WIDTH) {
            x -= RawChunk.WIDTH;
            chunkX++;
        }

        while (z < 0) {
            z += RawChunk.DEPTH;
            chunkZ--;
        }
        while (z >= RawChunk.DEPTH) {
            z -= RawChunk.DEPTH;
            chunkZ++;
        }

        return new Location(new ChunkCoord(chunkX, chunkZ), x, y, z);
    }

    public Vector3l getSpawnPosition() {
        return levelDat.getSpawnPosition();
    }

    @Override
    public int getBlockId(ChunkCoord chunkCoord, int x, int y, int z) {
        if (y < 0 || y >= RawChunk.HEIGHT)
            return defaultBlockId;

        Location loc = resolve(chunkCoord, x, y, z);
        Chunk c = rawLoadedChunks.get(loc.coord);
        if (c == null) {
            return defaultBlockId;
        } else
            return c.getBlockId(loc.x, loc.y, loc.z, defaultBlockId);
    }

    @Override
    public BlockType getBlockType(ChunkCoord chunkCoord, int x, int y, int z) {
        if (y < 0 || y >= RawChunk.HEIGHT)
            return registry.find(defaultBlockId, 0);

        Location loc = resolve(chunkCoord, x, y, z);
        Chunk c = rawLoadedChunks.get(loc.coord);
        if (c == null) {
            return registry.find(defaultBlockId, 0);
        } else {
            final int id = c.getBlockId(loc.x, loc.y, loc.z, defaultBlockId);
            final int data = c.getRawChunk().getBlockData(loc.x, loc.y, loc.z);
            return registry.find(id, data);
        }
    }

    @Override
    public int getBiomeId(ChunkCoord chunkCoord, int x, int y, int z) {
        if (y < 0 || y >= RawChunk.HEIGHT)
            return BiomeIds.UNKNOWN;

        Location loc = resolve(chunkCoord, x, y, z);
        Chunk c = rawLoadedChunks.get(loc.coord);
        if (c == null) {
            return BiomeIds.UNKNOWN;
        } else
            return c.getBiomeId(loc.x, loc.y, loc.z);
    }

    @Override
    public float getLight(ChunkCoord chunkCoord, final int x, final int y, final int z, LightFace face) {
        Location loc = resolve(chunkCoord, x, y, z);
        Chunk c = rawLoadedChunks.get(loc.coord);
        RawChunk raw = c != null ? c.getRawChunk() : null;

        return Chunk.getLight(lightStyle, face, raw, loc.x, loc.y, loc.z);
    }

    /*
    private float getSkyLight(ChunkCoord chunkCoord, final int x, final int y, final int z)
    {
       if (y < 0)
     return 0;
       if (y >= RawChunk.HEIGHT)
     return 1.0f;
           
       Location loc = resolve(chunkCoord, x, y, z);
       Chunk c = chunks.get(loc.coord);
       if (c == null)
     return 1.0f;
       else
     return c.getSkyLight(loc.x, loc.y, loc.z) / (float)RawChunk.MAX_LIGHT;
    }
        
    private float getBlockLight(ChunkCoord chunkCoord, final int x, final int y, final int z)
    {
       if (y < 0)
     return 0;
       if (y >= RawChunk.HEIGHT)
     return 1.0f;
           
       Location loc = resolve(chunkCoord, x, y, z);
       Chunk c = chunks.get(loc.coord);
       if (c == null)
     return 1.0f;
       else
     return c.getBlockLight(loc.x, loc.y, loc.z) / (float)RawChunk.MAX_LIGHT;
    }
    */

    public void flushChunkCache() {
        if (rawLoadedChunks != null) {
            // Flush geometry
            geometryLoadedChunks.unloadAll();

            // Flush raw loaded chunks
            rawLoadedChunks.unloadAll();
        }
    }

    public void flushGeometryCache() {
        if (geometryLoadedChunks != null) {
            geometryLoadedChunks.unloadAll();
        }
    }

    public void dumpMemStats() {
        System.out.println("---------------");
        System.out.println("World Mem Stats");
        System.out.println("---------------");

        //   System.out.println("Total chunks: "+chunks.size());
        System.out.println("Loaded raw chunks: " + rawLoadedChunks.size());
        System.out.println("Loaded geometry chunks: " + geometryLoadedChunks.size());

        long rawMemTotal = rawLoadedChunks.getRawMemorySize();
        rawMemTotal = rawMemTotal / 1024 / 1024; // bytes to megabytes
        System.out.println("Estimated raw memory: " + rawMemTotal + " Mb");

        long geometryMemTotal = geometryLoadedChunks.getGeometryMemorySize();
        geometryMemTotal = geometryMemTotal / 1024 / 1024; // bytes to megabytes
        System.out.println("Estimated geometry memory: " + geometryMemTotal + " Mb");

        System.out.println();

        Runtime runtime = Runtime.getRuntime();

        final long maxMemory = runtime.maxMemory();
        final long allocatedMemory = runtime.totalMemory();
        final long freeMemory = runtime.freeMemory();

        NumberFormat format = NumberFormat.getInstance();
        System.out.println("Max memory: " + format.format(maxMemory / 1024.0f) + "Mb");
        System.out.println("Allocated memory: " + format.format(allocatedMemory / 1024.0f) + "Mb");
        System.out.println("Free memory: " + format.format(freeMemory / 1024.0f) + "Mb");

        /*
        System.out.println("Geometry stats:");
        for (Chunk c : geometryLoadedChunks.values())
        {
           c.printGeometryStats();
        }
        */
    }

    private static class BackToFrontSorter implements Comparator<ChunkCoord> {
        private Camera camera;

        public BackToFrontSorter(Camera camera) {
            this.camera = camera;
        }

        @Override
        public int compare(ChunkCoord lhs, ChunkCoord rhs) {

            final float lhsDist = Math.abs(getDistance(camera, lhs));
            final float rhsDist = Math.abs(getDistance(camera, rhs));

            final float scale = 1000000000;

            //   return Math.round(lhsDist * scale - rhsDist * scale); // correct
            return Math.round(rhsDist * scale - lhsDist * scale); // correct

            /*
            // Hacky and works, but won't work for perspective views
            Point lhsScreen = camera.project(lhs.getBounds().getCenter());
            Point rhsScreen = camera.project(rhs.getBounds().getCenter());
                
            return lhsScreen.y - rhsScreen.y;
            */
        }

        private static float getDistance(Camera camera, ChunkCoord coord) {
            final float worldX = coord.x * RawChunk.WIDTH + (RawChunk.WIDTH / 2.0f);
            final float worldY = RawChunk.HEIGHT / 2.0f;
            final float worldZ = coord.z * RawChunk.DEPTH + (RawChunk.DEPTH / 2.0f);
            final float centerDist = Chunk.getDistance(camera, worldX, worldY, worldZ);
            return centerDist;
        }
    }

    private static class Location {
        public final ChunkCoord coord;
        public final int x, y, z;

        public Location(ChunkCoord coord, final int x, final int y, final int z) {
            this.coord = coord;

            this.x = x;
            this.y = y;
            this.z = z;
        }
    }

    public static ArrayList<Player> loadPlayers(File worldDir, PlayerSkinCache playerSkinCache) {
        File playersDir = Minecraft.findPlayersDir(worldDir);

        System.out.println("Loading players from " + playersDir.getAbsolutePath());

        ArrayList<Player> players = new ArrayList<Player>();
        File[] playerFiles = playersDir.listFiles();
        if (playerFiles != null) {
            for (File playerFile : playerFiles) {
                if (playerFile.getName().endsWith(".dat")) {
                    try {
                        Player player = new Player(playerFile);

                        CacheEntry ce = playerSkinCache.getCacheEntry(player.getUUID());
                        if (ce != null) {
                            final long age = System.currentTimeMillis() - ce.fetchedTime;
                            if (age < 1000 * 60 * 60 * 60) // one hour in ms
                            {
                                player.setName(ce.playerName);
                                player.setSkinURL(ce.skinURL);
                            }
                        } else {
                            if (player.getUUID().equals(player.getName())) {
                                player.setSkinURL("http://www.minecraft.net/skin/" + player.getName() + ".png");
                            } else {
                                String urlString = "https://sessionserver.mojang.com/session/minecraft/profile/"
                                        + player.getUUID();
                                URL url = new URL(urlString);
                                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                                connection.setRequestMethod("GET");
                                connection.addRequestProperty("Content-Type", "application/json");
                                connection.setReadTimeout(15 * 1000);
                                connection.connect();

                                BufferedReader reader = new BufferedReader(
                                        new InputStreamReader(connection.getInputStream()));
                                StringBuilder builder = new StringBuilder();

                                String line = null;
                                while ((line = reader.readLine()) != null) {
                                    builder.append(line + "\n");
                                }
                                reader.close();
                                JSONObject obj = new JSONObject(builder.toString());
                                player.setName(obj.getString("name"));
                                JSONObject textures = obj.getJSONArray("properties").getJSONObject(0);
                                byte[] decoded = DatatypeConverter
                                        .parseBase64Binary(textures.get("value").toString());
                                obj = new JSONObject(new String(decoded, "UTF-8"));
                                boolean hasSkin = obj.getJSONObject("textures").has("SKIN");
                                String textureUrl = null;
                                if (hasSkin == true)
                                    textureUrl = obj.getJSONObject("textures").getJSONObject("SKIN")
                                            .getString("url");
                                player.setSkinURL(textureUrl);
                            }
                        }

                        players.add(player);
                        System.out.println("Loaded " + player.getName());
                    } catch (Exception e) {
                        System.err.println("Couldn't load player info from " + playerFile.getName());
                        System.err.println(
                                "You are only allowed to contact the Mojang session server once per minute per player.  Wait for a minute and try again.");
                        //e.printStackTrace();
                    }
                }
            }
        }

        System.out.println("\tloaded " + players.size() + " players");

        return players;
    }

    public BlockTypeRegistry getBlockTypeRegistry() {
        return registry;
    }

    public File getWorldDir() {
        return worldDir;
    }

    public File getDimensionDir() {
        return dimensionDir;
    }

    public RawSign[] getLoadedSigns() {
        ArrayList<RawSign> result = new ArrayList<RawSign>();

        for (Chunk c : rawLoadedChunks.values()) {
            result.addAll(c.getSigns());
        }

        return result.toArray(new RawSign[0]);
    }

    @Override
    public Colour4f getGrassColour(ChunkCoord chunkCoord, int x, int y, int z) {
        final int biomeId = getBiomeId(chunkCoord, x, y, z);
        final int northId = getBiomeId(chunkCoord, x, y, z - 1);
        final int southId = getBiomeId(chunkCoord, x, y, z + 1);
        final int eastId = getBiomeId(chunkCoord, x + 1, y, z);
        final int westId = getBiomeId(chunkCoord, x - 1, y, z);

        Colour4f centerColour = getGrassColour(biomeId);
        Colour4f northColour = getGrassColour(northId);
        Colour4f southColour = getGrassColour(southId);
        Colour4f eastColour = getGrassColour(eastId);
        Colour4f westColour = getGrassColour(westId);

        Colour4f colour = new Colour4f(centerColour);
        colour.add(northColour);
        colour.add(southColour);
        colour.add(eastColour);
        colour.add(westColour);
        colour.divide(5);

        return colour;
    }

    private Colour4f getGrassColour(final int id) {
        Point colourCoord = BiomeIds.getColourCoord(id);
        Color awtColour = texturePack.getGrassColour(colourCoord.x, colourCoord.y);
        Colour4f colour = new Colour4f(awtColour);
        return colour;
    }
}