/* * This file is part of BlueMap, licensed under the MIT License (MIT). * * Copyright (c) Blue (Lukas Rieger) * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package de.bluecolored.bluemap.core.world.mca.region; import com.flowpowered.math.vector.Vector2i; import de.bluecolored.bluemap.core.storage.Compression; import de.bluecolored.bluemap.core.world.Chunk; import de.bluecolored.bluemap.core.world.ChunkConsumer; import de.bluecolored.bluemap.core.world.Region; import de.bluecolored.bluemap.core.world.mca.MCAWorld; import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunk; import lombok.Getter; import lombok.ToString; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @Getter @ToString public class MCARegion implements Region { public static final String FILE_SUFFIX = ".mca"; private final MCAWorld world; private final Path regionFile; private final Vector2i regionPos; public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentException { this.world = world; this.regionFile = regionFile; String[] filenameParts = regionFile.getFileName().toString().split("\\."); int rX = Integer.parseInt(filenameParts[1]); int rZ = Integer.parseInt(filenameParts[2]); this.regionPos = new Vector2i(rX, rZ); } public MCARegion(MCAWorld world, Vector2i regionPos) throws IllegalArgumentException { this.world = world; this.regionPos = regionPos; this.regionFile = world.getRegionFolder().resolve(getRegionFileName(regionPos.getX(), regionPos.getY())); } @Override public Chunk loadChunk(int chunkX, int chunkZ) throws IOException { if (Files.notExists(regionFile)) return Chunk.EMPTY_CHUNK; long fileLength = Files.size(regionFile); if (fileLength == 0) return Chunk.EMPTY_CHUNK; try (FileChannel channel = FileChannel.open(regionFile, StandardOpenOption.READ)) { int xzChunk = (chunkZ & 0b11111) << 5 | (chunkX & 0b11111); byte[] header = new byte[4]; channel.position(xzChunk * 4); readFully(channel, header, 0, 4); int offset = header[0] << 16; offset |= (header[1] & 0xFF) << 8; offset |= header[2] & 0xFF; offset *= 4096; int size = header[3] * 4096; if (size == 0) return Chunk.EMPTY_CHUNK; byte[] chunkDataBuffer = new byte[size]; channel.position(offset); readFully(channel, chunkDataBuffer, 0, size); return loadChunk(chunkDataBuffer, size); } } @Override public void iterateAllChunks(ChunkConsumer consumer) throws IOException { if (Files.notExists(regionFile)) return; long fileLength = Files.size(regionFile); if (fileLength == 0) return; int chunkStartX = regionPos.getX() * 32; int chunkStartZ = regionPos.getY() * 32; try (FileChannel channel = FileChannel.open(regionFile, StandardOpenOption.READ)) { byte[] header = new byte[1024 * 8]; byte[] chunkDataBuffer = null; // read the header readFully(channel, header, 0, header.length); // iterate over all chunks for (int x = 0; x < 32; x++) { for (int z = 0; z < 32; z++) { int xzChunk = (z & 0b11111) << 5 | (x & 0b11111); int size = header[xzChunk * 4 + 3] * 4096; if (size == 0) continue; int chunkX = chunkStartX + x; int chunkZ = chunkStartZ + z; int i = xzChunk * 4 + 4096; int timestamp = header[i++] << 24; timestamp |= (header[i++] & 0xFF) << 16; timestamp |= (header[i++] & 0xFF) << 8; timestamp |= header[i] & 0xFF; // load chunk only if consumers filter returns true if (consumer.filter(chunkX, chunkZ, timestamp * 1000L)) { i = xzChunk * 4; int offset = header[i++] << 16; offset |= (header[i++] & 0xFF) << 8; offset |= header[i] & 0xFF; offset *= 4096; if (chunkDataBuffer == null || chunkDataBuffer.length < size) chunkDataBuffer = new byte[size]; channel.position(offset); readFully(channel, chunkDataBuffer, 0, size); MCAChunk chunk = loadChunk(chunkDataBuffer, size); consumer.accept(chunkX, chunkZ, chunk); } } } } } private MCAChunk loadChunk(byte[] data, int size) throws IOException { int compressionTypeId = data[4]; Compression compression; switch (compressionTypeId) { case 0 : case 3 : compression = Compression.NONE; break; case 1 : compression = Compression.GZIP; break; case 2 : compression = Compression.DEFLATE; break; case 4 : compression = Compression.LZ4; break; default: throw new IOException("Unknown chunk compression-id: " + compressionTypeId); } return world.getChunkLoader().load(data, 5, size - 5, compression); } public static String getRegionFileName(int regionX, int regionZ) { return "r." + regionX + "." + regionZ + FILE_SUFFIX; } @SuppressWarnings("SameParameterValue") private static void readFully(ReadableByteChannel src, byte[] dst, int off, int len) throws IOException { readFully(src, ByteBuffer.wrap(dst), off, len); } private static void readFully(ReadableByteChannel src, ByteBuffer bb, int off, int len) throws IOException { int limit = off + len; if (limit > bb.capacity()) throw new IllegalArgumentException("buffer too small"); bb.limit(limit); bb.position(off); do { int read = src.read(bb); if (read < 0) // zero out all the remaining data from the buffer while (bb.remaining() > 0) bb.put((byte) 0); } while (bb.remaining() > 0); } }