BlueMap/BlueMapCore/src/main/java/de/bluecolored/bluemap/core/world/mca/region/MCARegion.java

200 lines
7.4 KiB
Java

/*
* This file is part of BlueMap, licensed under the MIT License (MIT).
*
* Copyright (c) Blue (Lukas Rieger) <https://bluecolored.de>
* 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(this, 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);
}
}