/* Copyright (c) 2012-2016 Jesper Öqvist <jesper@llbit.se> * * This file is part of Chunky. * * Chunky is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Chunky 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with Chunky. If not, see <http://www.gnu.org/licenses/>. */ package se.llbit.chunky.world; import java.io.ByteArrayInputStream; import java.io.DataOutputStream; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Iterator; import java.util.Set; import java.util.zip.GZIPInputStream; import java.util.zip.InflaterInputStream; import se.llbit.log.Log; /** * Abstract region representation. Tracks loaded chunks and their timestamps. * * <p>If an error occurs it will usually be reported to STDERR instead of using * the logging framework, because the error dialogs can be so many for a * single corrupted region. Corrupted chunks are illustrated by a black square * with a red X and red outline in the map view. * * @author Jesper Öqvist <jesper@llbit.se> */ public class Region implements Iterable<Chunk> { /** * Region X chunk width */ public static final int CHUNKS_X = 32; /** * Region Z chunk width */ public static final int CHUNKS_Z = 32; private static final int NUM_CHUNKS = CHUNKS_X * CHUNKS_Z; private final Chunk[] chunks = new Chunk[NUM_CHUNKS]; private final ChunkPosition position; private final World world; private final String fileName; private long regionFileTime = 0; private final int[] chunkTimestamps = new int[NUM_CHUNKS]; /** * Create new region * * @param pos the region position */ public Region(ChunkPosition pos, World world) { this.world = world; fileName = pos.getMcaName(); position = pos; for (int z = 0; z < CHUNKS_Z; ++z) { for (int x = 0; x < CHUNKS_X; ++x) { chunks[x + z * 32] = EmptyChunk.INSTANCE; } } } /** * @return Chunk at (x, z) */ public Chunk getChunk(int x, int z) { return chunks[(x & 31) + (z & 31) * 32]; } /** * @param pos Chunk position * @return Chunk at given position */ public Chunk getChunk(ChunkPosition pos) { return chunks[(pos.x & 31) + (pos.z & 31) * 32]; } /** * Set chunk at given position. */ public void setChunk(ChunkPosition pos, Chunk chunk) { chunks[(pos.x & 31) + (pos.z & 31) * 32] = chunk; } /** * Delete a chunk. */ public synchronized void deleteChunk(ChunkPosition chunkPos) { deleteChunkFromRegion(chunkPos); Chunk chunk = getChunk(chunkPos); if (!chunk.isEmpty()) { chunk.reset(); setChunk(chunkPos, EmptyChunk.INSTANCE); world.chunkDeleted(chunkPos); } } /** * Parse the region file to discover chunks. */ public synchronized void parse() { File regionFile = new File(world.getRegionDirectory(), fileName); if (!regionFile.isFile()) { return; } long modtime = regionFile.lastModified(); if (regionFileTime == modtime) { return; } regionFileTime = modtime; try (RandomAccessFile file = new RandomAccessFile(regionFile, "r")) { long length = file.length(); if (length < 2 * SECTOR_SIZE) { System.err.println("Missing header in region file!"); return; } for (int z = 0; z < 32; ++z) { for (int x = 0; x < 32; ++x) { ChunkPosition pos = ChunkPosition.get((position.x << 5) + x, (position.z << 5) + z); Chunk chunk = getChunk(x, z); int loc = file.readInt(); if (loc != 0) { if (chunk.isEmpty()) { chunk = new Chunk(pos, world); setChunk(pos, chunk); } } else { if (!chunk.isEmpty()) { world.chunkDeleted(pos); } } } } for (int i = 0; i < NUM_CHUNKS; ++i) { chunkTimestamps[i] = file.readInt(); } world.regionUpdated(position); } catch (IOException e) { System.err.println("Failed to read region: " + e.getMessage()); } } /** * @return <code>true</code> if this is an empty or non-existent region */ public boolean isEmpty() { return false; } /** * @return The region position */ public final ChunkPosition getPosition() { return position; } @Override public String toString() { return "Region " + position.toString(); } /** * @param pos A region position * @return The region file name corresponding to the given region position */ public static String getFileName(ChunkPosition pos) { return String.format("r.%d.%d.mca", pos.x, pos.z); } /** * Sector size in bytes. */ private final static int SECTOR_SIZE = 4096; /** * Opens an input stream for the given chunk. * * @param chunkPos chunk position for the chunk to read * @return Chunk data source. The InputStream of the data source is * {@code null} if the chunk could not be read. */ public ChunkDataSource getChunkData(ChunkPosition chunkPos) { File regionDirectory = world.getRegionDirectory(); File regionFile = new File(regionDirectory, fileName); ChunkDataSource data = null; if (regionFile.exists()) { data = getChunkData(regionFile, chunkPos); } if (data == null) { data = new ChunkDataSource((int) System.currentTimeMillis(), null); } chunkTimestamps[(chunkPos.x & 31) + (chunkPos.z & 31) * 32] = data.timestamp; return data; } /** * Read chunk data from region file. * * @return {@code null} if the chunk could not be loaded */ public static ChunkDataSource getChunkData(File regionFile, ChunkPosition chunkPos) { int x = chunkPos.x & 31; int z = chunkPos.z & 31; int index = x + z * 32; try (RandomAccessFile file = new RandomAccessFile(regionFile, "r")) { long length = file.length(); if (length < 2 * SECTOR_SIZE) { Log.warn("Missing header in region file!"); return null; } file.seek(4 * index); int loc = file.readInt(); int numSectors = loc & 0xFF; int sectorOffset = loc >> 8; file.seek(SECTOR_SIZE + 4 * index); int timestamp = file.readInt(); if (length < (sectorOffset + numSectors) * SECTOR_SIZE) { System.err.println("Chunk is outside region file!"); return null; } file.seek(sectorOffset * SECTOR_SIZE); int chunkSize = file.readInt(); if (chunkSize > numSectors * SECTOR_SIZE) { System.err.println("Error: chunk length does not fit in allocated sectors!"); return null; } byte type = file.readByte(); if (type != 1 && type != 2) { System.err.println("Error: unknown chunk data compression method: " + type + "!"); return null; } byte[] buf = new byte[chunkSize - 1]; file.read(buf); ByteArrayInputStream in = new ByteArrayInputStream(buf); if (type == 1) { return new ChunkDataSource(timestamp, new GZIPInputStream(in)); } else { return new ChunkDataSource(timestamp, new InflaterInputStream(in)); } } catch (IOException e) { System.err.println("Failed to read chunk: " + e.getMessage()); return null; } } /** * Delete the chunk from the region file. */ public void deleteChunkFromRegion(ChunkPosition chunkPos) { // Just write zero in the entry for the chunk in the location table. File regionDirectory = world.getRegionDirectory(); int x = chunkPos.x & 31; int z = chunkPos.z & 31; File regionFile = new File(regionDirectory, fileName); int index = x + z * 32; try (RandomAccessFile file = new RandomAccessFile(regionFile, "rw")) { long length = file.length(); if (length < 2 * SECTOR_SIZE) { Log.warn("Missing header in region file!"); return; } file.seek(4 * index); file.writeInt(0); } catch (IOException e) { Log.warnf("Failed to delete chunk: %s", e.getMessage()); } } /** * Write this region to the output stream. * * @throws IOException */ public static synchronized void writeRegion(File regionDirectory, ChunkPosition regionPos, DataOutputStream out, Set<ChunkPosition> chunks) throws IOException { String fileName = regionPos.getMcaName(); File regionFile = new File(regionDirectory, fileName); try (RandomAccessFile file = new RandomAccessFile(regionFile, "r")) { int[] location = new int[32 * 32]; int[] loc_out = new int[32 * 32]; int nextFree = 2;// 2 sectors reserved for offsets and timestamps for (int i = 0; i < 32 * 32; ++i) { location[i] = file.readInt(); int offset = location[i]; if (offset != 0 && (chunks == null || chunks.contains(ChunkPosition.get(i & 31, i >> 5)))) { loc_out[i] = nextFree << 8 | offset & 0xFF; nextFree += offset & 0xFF; } } // Write offset table. for (int i = 0; i < 32 * 32; ++i) { out.writeInt(loc_out[i]); } // Write timestamp table. for (int i = 0; i < 32 * 32; ++i) { out.writeInt(file.readInt()); } // Write chunks. for (int i = 0; i < 32 * 32; ++i) { if (loc_out[i] == 0) { continue; } int loc = location[i]; int numSectors = loc & 0xFF; int sectorOffset = loc >> 8; file.seek(sectorOffset * SECTOR_SIZE); byte[] buffer = new byte[SECTOR_SIZE]; for (int j = 0; j < numSectors; ++j) { file.read(buffer); out.write(buffer); } } } } public boolean hasChanged() { File regionFile = new File(world.getRegionDirectory(), fileName); return regionFileTime != regionFile.lastModified(); } /** * @return {@code true} if the chunk has changed since the timestamp */ public boolean chunkChangedSince(ChunkPosition chunkPos, int timestamp) { return timestamp != chunkTimestamps[(chunkPos.x & 31) + (chunkPos.z & 31) * 32]; } @Override public Iterator<Chunk> iterator() { return new Iterator<Chunk>() { private int index = 0; @Override public boolean hasNext() { return index < NUM_CHUNKS; } @Override public Chunk next() { return chunks[index++]; } @Override public void remove() { chunks[index] = EmptyChunk.INSTANCE; } }; } }