/* * This file is part of Spoutcraft. * * Copyright (c) 2011 SpoutcraftDev <http://spoutcraft.org/> * Spoutcraft is licensed under the GNU Lesser General Public License. * * Spoutcraft is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Spoutcraft is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.spoutcraft.client.chunkcache; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import org.spoutcraft.api.util.map.TIntPairObjectHashMap; import org.spoutcraft.client.io.FileUtil; public class HeightMap { private String worldName; private final static int INITIAL_CAPACITY = 500; private final TIntPairObjectHashMap<HeightChunk> cache = new TIntPairObjectHashMap<HeightChunk>(INITIAL_CAPACITY); private static final HashMap<String, HeightMap> heightMaps = new HashMap<String, HeightMap>(); private static HeightMap lastMap = null; // Faster access to last height-map (which will be the case in most cases) private int minX = 0, maxX = 0, minZ = 0, maxZ = 0; private boolean initBounds = false; private File file = null; private HeightChunk lastChunk = null; // Faster access to last accessed chunk private static HeightMapSaveThread saveThread; private boolean dirty = true; public class HeightChunk { public short heightmap[] = new short[16 * 16]; public final int x, z; public byte[] idmap = new byte[16 * 16]; public byte [] datamap = new byte[16 * 16]; { for (int i = 0; i < 256; i++) { heightmap[i] = -1; idmap[i] = -1; } } public HeightChunk(int x, int z) { this.x = x; this.z = z; } public short getHeight(int x, int z) { return heightmap[z << 4 | x]; } public byte getBlockId(int x, int z) { return idmap[z << 4 | x]; } public void setHeight(int x, int z, short h) { heightmap[z << 4 | x] = h; } public void setBlockId(int x, int z, byte id) { idmap[z << 4 | x] = id; } public byte getData(int x, int z) { return datamap[z << 4 | x]; } public void setData(int x, int z, byte data) { datamap[z << 4 | x] = data; } } public void clear() { cache.clear(); initBounds = false; dirty = false; } public static HeightMap getHeightMap(String worldName) { return getHeightMap(worldName, getFile(worldName)); } public static HeightMap getHeightMap(String worldName, File file) { if (lastMap != null && lastMap.getWorldName().equals(worldName)) { //lastMap.file = file; return lastMap; } HeightMap ret = null; if (heightMaps.containsKey(worldName)) { ret = heightMaps.get(worldName); //ret.file = file; } else { HeightMap map = new HeightMap(worldName); map.file = file; heightMaps.put(worldName, map); if (file.exists()) { map.load(); } ret = map; } lastMap = ret; return ret; } private HeightMap(String worldName) { this.worldName = worldName; } /* * Format of the file is this: * worldName:String * minX:int * maxX:int * minZ:int * maxZ:int * height-map:short[], where map[0] is at minX, minZ and map[last] is at maxX, maxZ */ public void load() { synchronized (cache) { clear(); try { DataInputStream in = new DataInputStream(new FileInputStream(file)); int version = 0; if (file.getAbsolutePath().endsWith(".hm2")) { version = in.readInt(); // Read version } StringBuilder builder = new StringBuilder(); short size = in.readShort(); for (int i = 0; i < size; i++) { builder.append(in.readChar()); } String name = builder.toString(); if (!name.equals(getWorldName())) { System.out.println("World names do not match: " + name + " [file] != " + getWorldName() + " [game]. Compensating..."); // TODO Compensate } minX = in.readInt(); maxX = in.readInt(); minZ = in.readInt(); maxZ = in.readInt(); initBounds = true; int x = minX; int z = minZ; try { while (true) { x = in.readInt(); z = in.readInt(); HeightChunk chunk = new HeightChunk(x, z); for (int i = 0; i < 256; i++) { chunk.heightmap[i] = in.readShort(); chunk.idmap[i] = in.readByte(); if (version >= 1) { chunk.datamap[i] = in.readByte(); } } addChunk(chunk); } } catch (EOFException e) {} in.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); cache.clear(); initBounds = false; System.out.println("Error while loading persistent copy of the heightmap. Clearing the cache."); } File progress = new File(file.getAbsoluteFile() + ".inProgress.hm2"); if (progress.exists()) { System.out.println("Found in-progress file!"); HeightMap progressMap = new HeightMap(getWorldName()); progressMap.file = progress; progressMap.load(); for (HeightChunk chunk:progressMap.cache.valueCollection()) { if (chunk.getHeight(0, 0) != -1) { addChunk(chunk); } } heightMaps.remove(progressMap); progress.delete(); } } if (file.getAbsolutePath().endsWith(".hma")) { file = new File(file.getAbsolutePath().replace(".hma", ".hm2")); } dirty = false; } private void addChunk(HeightChunk chunk) { dirty = true; synchronized (cache) { int x = chunk.x; int z = chunk.z; cache.put(x, z, chunk); if (!initBounds) { minX = x; maxX = x; minZ = z; maxZ = z; initBounds = true; } else { minX = Math.min(minX, x); maxX = Math.max(maxX, x); minZ = Math.min(minZ, z); maxZ = Math.max(maxZ, z); } } } public void save() { if (!dirty) { return; // Don't need to save when not touched... } synchronized (cache) { try { File progress = new File(file.getAbsoluteFile() + ".inProgress.hm2"); DataOutputStream out = new DataOutputStream(new FileOutputStream(progress)); out.writeInt(1); // This is the version String name = getWorldName(); out.writeShort(name.length()); for (int i = 0; i < name.length(); i++) { out.writeChar(name.charAt(i)); } out.writeInt(minX); out.writeInt(maxX); out.writeInt(minZ); out.writeInt(maxZ); for (HeightChunk chunk : cache.valueCollection()) { if (chunk == null) { continue; } else { out.writeInt(chunk.x); out.writeInt(chunk.z); for (int i = 0; i < 256; i++) { out.writeShort(chunk.heightmap[i]); out.writeByte(chunk.idmap[i]); out.writeByte(chunk.datamap[i]); } } } out.close(); // Make sure that we don't loose older stuff when someone quits. File older = new File(file.getAbsoluteFile() + ".old"); file.renameTo(older); progress.renameTo(file); older.delete(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } dirty = false; } public void saveThreaded() { if (saveThread == null) { saveThread = new HeightMapSaveThread(); saveThread.addMap(this); } } private static File getFile(String worldName) { File folder = new File(FileUtil.getConfigDir(), "heightmap"); if (!folder.isDirectory()) { folder.delete(); } if (!folder.exists()) { folder.mkdirs(); } File oldFormat = new File(FileUtil.getConfigDir(), "heightmap/" + worldName + ".hma"); File newFormat = new File(FileUtil.getConfigDir(), "heightmap/" + worldName + ".hm2"); if (newFormat.exists() || !oldFormat.exists()) { return newFormat; } return oldFormat; } /*public boolean hasHeight(int x, int z) { synchronized (cache) { return cache.containsKey(x, z); } }*/ public HeightChunk getChunk(int x, int z) { return getChunk(x, z, false); } public HeightChunk getChunk(int x, int z, boolean force) { dirty = true; // We don't know what they do with the chunk, so it could be dirtied... if (lastChunk != null && lastChunk.x == x && lastChunk.z == z) { return lastChunk; } else { synchronized (cache) { lastChunk = cache.get(x, z); if (lastChunk == null) { lastChunk = new HeightChunk(x, z); addChunk(lastChunk); } return lastChunk; } } } public short getHeight(int x, int z) { int cX = (x >> 4); int cZ = (z >> 4); x &= 0xF; z &= 0xF; if (lastChunk != null && lastChunk.x == cX && lastChunk.z == cZ) { return lastChunk.heightmap[z << 4 | x]; } synchronized (cache) { if (cache.containsKey(cX, cZ)) { lastChunk = cache.get(cX, cZ); return lastChunk.heightmap[z << 4 | x]; } else { return -1; } } } public byte getBlockId(int x, int z) { int cX = (x >> 4); int cZ = (z >> 4); x &= 0xF; z &= 0xF; if (lastChunk != null && lastChunk.x == cX && lastChunk.z == cZ) { return lastChunk.idmap[z << 4 | x]; } synchronized (cache) { if (cache.containsKey(cX, cZ)) { lastChunk = cache.get(cX, cZ); return lastChunk.idmap[z << 4 | x]; } else { return -1; } } } public byte getData(int x, int z) { int cX = (x >> 4); int cZ = (z >> 4); x &= 0xF; z &= 0xF; if (lastChunk != null && lastChunk.x == cX && lastChunk.z == cZ) { return lastChunk.datamap[z << 4 | x]; } synchronized (cache) { if (cache.containsKey(cX, cZ)) { lastChunk = cache.get(cX, cZ); return lastChunk.datamap[z << 4 | x]; } else { return -1; } } } public void setHighestBlock(int x, int z, short height, byte id) { dirty = true; int cX = (x >> 4); int cZ = (z >> 4); x &= 0xF; z &= 0xF; synchronized (cache) { if (!(lastChunk != null && lastChunk.x == cX && lastChunk.z == cZ)) { if (cache.containsKey(cX, cZ)) { lastChunk = cache.get(cX, cZ); } else { HeightChunk chunk = new HeightChunk(cX, cZ); chunk.heightmap[z << 4 | x] = height; chunk.idmap [z << 4 | x] = id; lastChunk = chunk; addChunk(chunk); return; } } lastChunk.heightmap[z << 4 | x] = height; lastChunk.idmap[z << 4 | x] = id; } } public String getWorldName() { return worldName; } public int getMinX() { return minX; } public int getMaxX() { return maxX; } public int getMinZ() { return minZ; } public int getMaxZ() { return maxZ; } public static void joinSaveThread() { if (saveThread != null) { try { System.out.println("Waiting for heightmap to save..."); saveThread.join(); } catch (InterruptedException e) { } } } public boolean isDirty() { return dirty; } }