forked from mirror/BlueMap
Reimplement Linear region file format support
This commit is contained in:
parent
ff1e38a7e1
commit
dbde93c9f5
@ -53,7 +53,7 @@ public class MCAWorld implements World {
|
||||
private final Path dimensionFolder;
|
||||
private final Path regionFolder;
|
||||
|
||||
private final ChunkLoader chunkLoader = new ChunkLoader();
|
||||
private final ChunkLoader chunkLoader = new ChunkLoader(this);
|
||||
private final LoadingCache<Vector2i, Region> regionCache = Caffeine.newBuilder()
|
||||
.executor(BlueMap.THREAD_POOL)
|
||||
.maximumSize(64)
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import de.bluecolored.bluemap.core.storage.Compression;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
|
||||
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -16,6 +16,12 @@
|
||||
|
||||
public class ChunkLoader {
|
||||
|
||||
private final MCAWorld world;
|
||||
|
||||
public ChunkLoader(MCAWorld world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
// sorted list of chunk-versions, loaders at the start of the list are preferred over loaders at the end
|
||||
private static final List<ChunkVersionLoader<?>> CHUNK_VERSION_LOADERS = List.of(
|
||||
new ChunkVersionLoader<>(Chunk_1_18.Data.class, Chunk_1_18::new, 2844),
|
||||
@ -26,7 +32,7 @@ public class ChunkLoader {
|
||||
|
||||
private ChunkVersionLoader<?> lastUsedLoader = CHUNK_VERSION_LOADERS.get(0);
|
||||
|
||||
public MCAChunk load(MCARegion region, byte[] data, int offset, int length, Compression compression) throws IOException {
|
||||
public MCAChunk load(byte[] data, int offset, int length, Compression compression) throws IOException {
|
||||
InputStream in = new ByteArrayInputStream(data, offset, length);
|
||||
in.mark(-1);
|
||||
|
||||
@ -34,7 +40,7 @@ public MCAChunk load(MCARegion region, byte[] data, int offset, int length, Comp
|
||||
ChunkVersionLoader<?> usedLoader = lastUsedLoader;
|
||||
MCAChunk chunk;
|
||||
try (InputStream decompressedIn = new BufferedInputStream(compression.decompress(in))) {
|
||||
chunk = usedLoader.load(region, decompressedIn);
|
||||
chunk = usedLoader.load(world, decompressedIn);
|
||||
}
|
||||
|
||||
// check version and reload chunk if the wrong loader has been used and a better one has been found
|
||||
@ -42,7 +48,7 @@ public MCAChunk load(MCARegion region, byte[] data, int offset, int length, Comp
|
||||
if (actualLoader != null && usedLoader != actualLoader) {
|
||||
in.reset(); // reset read position
|
||||
try (InputStream decompressedIn = new BufferedInputStream(compression.decompress(in))) {
|
||||
chunk = actualLoader.load(region, decompressedIn);
|
||||
chunk = actualLoader.load(world, decompressedIn);
|
||||
}
|
||||
lastUsedLoader = actualLoader;
|
||||
}
|
||||
@ -62,12 +68,12 @@ public MCAChunk load(MCARegion region, byte[] data, int offset, int length, Comp
|
||||
private static class ChunkVersionLoader<D extends MCAChunk.Data> {
|
||||
|
||||
private final Class<D> dataType;
|
||||
private final BiFunction<MCARegion, D, MCAChunk> constructor;
|
||||
private final BiFunction<MCAWorld, D, MCAChunk> constructor;
|
||||
private final int dataVersion;
|
||||
|
||||
public MCAChunk load(MCARegion region, InputStream in) throws IOException {
|
||||
public MCAChunk load(MCAWorld world, InputStream in) throws IOException {
|
||||
D data = MCAUtil.BLUENBT.read(in, dataType);
|
||||
return mightSupport(data.getDataVersion()) ? constructor.apply(region, data) : new MCAChunk(region, data) {};
|
||||
return mightSupport(data.getDataVersion()) ? constructor.apply(world, data) : new MCAChunk(world, data) {};
|
||||
}
|
||||
|
||||
public boolean mightSupport(int dataVersion) {
|
||||
|
@ -7,7 +7,7 @@
|
||||
import de.bluecolored.bluemap.core.world.DimensionType;
|
||||
import de.bluecolored.bluemap.core.world.LightData;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
|
||||
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
|
||||
import de.bluecolored.bluenbt.NBTName;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -38,8 +38,8 @@ public class Chunk_1_13 extends MCAChunk {
|
||||
|
||||
final int[] biomes;
|
||||
|
||||
public Chunk_1_13(MCARegion region, Data data) {
|
||||
super(region, data);
|
||||
public Chunk_1_13(MCAWorld world, Data data) {
|
||||
super(world, data);
|
||||
|
||||
Level level = data.level;
|
||||
|
||||
@ -50,7 +50,7 @@ public Chunk_1_13(MCARegion region, Data data) {
|
||||
STATUS_POSTPROCESSED.equals(level.status);
|
||||
this.inhabitedTime = level.inhabitedTime;
|
||||
|
||||
DimensionType dimensionType = getRegion().getWorld().getDimensionType();
|
||||
DimensionType dimensionType = getWorld().getDimensionType();
|
||||
this.skyLight = dimensionType.hasSkylight() ? 16 : 0;
|
||||
|
||||
this.worldSurfaceHeights = level.heightmaps.worldSurface;
|
||||
|
@ -1,12 +1,12 @@
|
||||
package de.bluecolored.bluemap.core.world.mca.chunk;
|
||||
|
||||
import de.bluecolored.bluemap.core.world.Biome;
|
||||
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
|
||||
|
||||
public class Chunk_1_15 extends Chunk_1_13 {
|
||||
|
||||
public Chunk_1_15(MCARegion region, Data data) {
|
||||
super(region, data);
|
||||
public Chunk_1_15(MCAWorld world, Data data) {
|
||||
super(world, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -7,8 +7,8 @@
|
||||
import de.bluecolored.bluemap.core.world.DimensionType;
|
||||
import de.bluecolored.bluemap.core.world.LightData;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
|
||||
import de.bluecolored.bluemap.core.world.mca.PackedIntArrayAccess;
|
||||
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
|
||||
import de.bluecolored.bluenbt.NBTName;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -37,8 +37,8 @@ public class Chunk_1_16 extends MCAChunk {
|
||||
|
||||
private final int[] biomes;
|
||||
|
||||
public Chunk_1_16(MCARegion region, Data data) {
|
||||
super(region, data);
|
||||
public Chunk_1_16(MCAWorld world, Data data) {
|
||||
super(world, data);
|
||||
|
||||
Level level = data.level;
|
||||
|
||||
@ -46,7 +46,7 @@ public Chunk_1_16(MCARegion region, Data data) {
|
||||
this.hasLightData = STATUS_FULL.equals(level.status);
|
||||
this.inhabitedTime = level.inhabitedTime;
|
||||
|
||||
DimensionType dimensionType = getRegion().getWorld().getDimensionType();
|
||||
DimensionType dimensionType = getWorld().getDimensionType();
|
||||
this.skyLight = dimensionType.hasSkylight() ? 16 : 0;
|
||||
|
||||
int worldHeight = dimensionType.getHeight();
|
||||
|
@ -7,8 +7,8 @@
|
||||
import de.bluecolored.bluemap.core.world.DimensionType;
|
||||
import de.bluecolored.bluemap.core.world.LightData;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
|
||||
import de.bluecolored.bluemap.core.world.mca.PackedIntArrayAccess;
|
||||
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
|
||||
import de.bluecolored.bluenbt.NBTName;
|
||||
import lombok.Getter;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -37,14 +37,14 @@ public class Chunk_1_18 extends MCAChunk {
|
||||
private final Section[] sections;
|
||||
private final int sectionMin, sectionMax;
|
||||
|
||||
public Chunk_1_18(MCARegion region, Data data) {
|
||||
super(region, data);
|
||||
public Chunk_1_18(MCAWorld world, Data data) {
|
||||
super(world, data);
|
||||
|
||||
this.generated = !STATUS_EMPTY.equals(data.status);
|
||||
this.hasLightData = STATUS_FULL.equals(data.status);
|
||||
this.inhabitedTime = data.inhabitedTime;
|
||||
|
||||
DimensionType dimensionType = getRegion().getWorld().getDimensionType();
|
||||
DimensionType dimensionType = getWorld().getDimensionType();
|
||||
this.worldMinY = dimensionType.getMinY();
|
||||
this.skyLight = dimensionType.hasSkylight() ? 16 : 0;
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import de.bluecolored.bluemap.core.world.BlockState;
|
||||
import de.bluecolored.bluemap.core.world.Chunk;
|
||||
import de.bluecolored.bluemap.core.world.mca.region.MCARegion;
|
||||
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@ -20,11 +20,11 @@ public abstract class MCAChunk implements Chunk {
|
||||
protected static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
protected static final BlockState[] EMPTY_BLOCKSTATE_ARRAY = new BlockState[0];
|
||||
|
||||
private final MCARegion region;
|
||||
private final MCAWorld world;
|
||||
private final int dataVersion;
|
||||
|
||||
public MCAChunk(MCARegion region, Data chunkData) {
|
||||
this.region = region;
|
||||
public MCAChunk(MCAWorld world, Data chunkData) {
|
||||
this.world = world;
|
||||
this.dataVersion = chunkData.getDataVersion();
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,211 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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()));
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
byte[] chunkDataBuffer = null;
|
||||
byte[] compressedData;
|
||||
|
||||
byte version;
|
||||
long newestTimestamp;
|
||||
byte compressionLevel;
|
||||
short chunkCount;
|
||||
int dataLength;
|
||||
long dataHash;
|
||||
|
||||
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");
|
||||
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -168,7 +168,7 @@ private MCAChunk loadChunk(byte[] data, int size) throws IOException {
|
||||
default: throw new IOException("Unknown chunk compression-id: " + compressionTypeId);
|
||||
}
|
||||
|
||||
return world.getChunkLoader().load(this, data, 5, size - 5, compression);
|
||||
return world.getChunkLoader().load(data, 5, size - 5, compression);
|
||||
}
|
||||
|
||||
public static String getRegionFileName(int regionX, int regionZ) {
|
||||
|
@ -34,8 +34,8 @@
|
||||
|
||||
public enum RegionType {
|
||||
|
||||
MCA (MCARegion::new, MCARegion.FILE_SUFFIX, MCARegion::getRegionFileName);
|
||||
//LINEAR (LinearRegion::new, LinearRegion.FILE_SUFFIX, LinearRegion::getRegionFileName);
|
||||
MCA (MCARegion::new, MCARegion.FILE_SUFFIX, MCARegion::getRegionFileName),
|
||||
LINEAR (LinearRegion::new, LinearRegion.FILE_SUFFIX, LinearRegion::getRegionFileName);
|
||||
|
||||
// we do this to improve performance, as calling values() creates a new array each time
|
||||
private final static RegionType[] VALUES = values();
|
||||
|
Loading…
Reference in New Issue
Block a user