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

221 lines
8.0 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.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 io.airlift.compress.zstd.ZstdInputStream;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
/*
* LinearFormat:
*
* REGION-FILE:
* 8 byte - MAGIC value
* 1 byte - version
* 8 byte - region timestamp
* 1 byte - compression level
* 2 byte - chunk count
* 4 byte - data-length in bytes
* 8 byte - data-hash
* ? byte - data
* 8 byte - MAGIC value
*
* DATA: (zstd compressed)
* 32 * 32 * 8 - header:
* 4 byte - chunk-data-length
* 4 byte - timestamp
* ? - chunks
*
*/
public class LinearRegion implements Region {
public static final String FILE_SUFFIX = ".linear";
private static final long MAGIC = 0xc3ff13183cca9d9aL;
private final MCAWorld world;
private final Path regionFile;
private final Vector2i regionPos;
private boolean initialized = false;
private byte version;
private long newestTimestamp;
private byte compressionLevel;
private short chunkCount;
private int dataLength;
private long dataHash;
private byte[] compressedData;
public LinearRegion(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 LinearRegion(MCAWorld world, Vector2i regionPos) throws IllegalArgumentException {
this.world = world;
this.regionPos = regionPos;
this.regionFile = world.getRegionFolder().resolve(getRegionFileName(regionPos.getX(), regionPos.getY()));
}
private synchronized void init() throws IOException {
if (initialized) return;
if (Files.notExists(regionFile)) return;
long fileLength = Files.size(regionFile);
if (fileLength == 0) return;
try (
InputStream in = Files.newInputStream(regionFile, StandardOpenOption.READ);
BufferedInputStream bIn = new BufferedInputStream(in);
DataInputStream dIn = new DataInputStream(bIn)
) {
if (dIn.readLong() != MAGIC)
throw new IOException("Linear region-file format: invalid header magic");
// read the header
version = dIn.readByte();
newestTimestamp = dIn.readLong();
compressionLevel = dIn.readByte();
chunkCount = dIn.readShort();
dataLength = dIn.readInt();
dataHash = dIn.readLong();
if (version < 1 || version > 2)
throw new IOException("Linear region-file format: Unsupported version: " + version);
if (fileLength != dataLength + 40) // 40 = header + footer
throw new IOException("Linear region-file format: Invalid file length. Expected " + (dataLength + 40) + " but got " + fileLength);
compressedData = new byte[dataLength];
dIn.readFully(compressedData, 0, dataLength);
if (dIn.readLong() != MAGIC)
throw new IOException("Linear region-file format: invalid footer magic");
}
initialized = true;
}
@Override
public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
if (!initialized) init();
int chunkStartX = regionPos.getX() * 32;
int chunkStartZ = regionPos.getY() * 32;
byte[] chunkDataBuffer = null;
try (
InputStream in = new ZstdInputStream(new ByteArrayInputStream(compressedData));
BufferedInputStream bIn = new BufferedInputStream(in);
DataInputStream dIn = new DataInputStream(bIn)
) {
int[] chunkDataLengths = new int[1024];
int[] chunkTimestamps = new int[1024];
for (int i = 0 ; i < 1024 ; i++) {
chunkDataLengths[i] = dIn.readInt();
chunkTimestamps[i] = dIn.readInt();
}
int i = 0;
int toBeSkipped = 0;
for (int z = 0; z < 32; z++) {
for (int x = 0; x < 32; x++) {
int length = chunkDataLengths[i];
if (length > 0) {
int chunkX = chunkStartX + x;
int chunkZ = chunkStartZ + z;
long timestamp = version == 2 ? chunkTimestamps[i] : newestTimestamp;
if (consumer.filter(chunkX, chunkZ, timestamp)) {
if (toBeSkipped > 0) skipNBytes(dIn, toBeSkipped);
if (chunkDataBuffer == null || chunkDataBuffer.length < length)
chunkDataBuffer = new byte[length];
dIn.readFully(chunkDataBuffer, 0, length);
MCAChunk chunk = world.getChunkLoader().load(chunkDataBuffer, 0, length, Compression.NONE);
consumer.accept(chunkX, chunkZ, chunk);
} else {
// skip before reading the next chunk, but only if there is a next chunk
// that we actually want to read, to avoid decompressing unnecessary data
toBeSkipped += length;
}
}
i++;
}
}
}
}
public static String getRegionFileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX;
}
/**
* This method is taken here from a newer version of {@link InputStream},
* to ensure Java 11 compatibility.
*/
private static void skipNBytes(InputStream in, long n) throws IOException {
while (n > 0) {
long ns = in.skip(n);
if (ns > 0 && ns <= n) {
// adjust number to skip
n -= ns;
} else if (ns == 0) { // no bytes skipped
// read one byte to check for EOS
if (in.read() == -1) {
throw new EOFException();
}
// one byte read so decrement number to skip
n--;
} else { // skipped negative or too many bytes
throw new IOException("Unable to skip exactly");
}
}
}
}