Make regions generic to support different types of chunks

This commit is contained in:
Lukas Rieger (Blue) 2024-11-04 14:38:09 +01:00
parent 35fbcff370
commit 8b1c5ab3c2
No known key found for this signature in database
GPG Key ID: AA33883B1BBA03E6
8 changed files with 108 additions and 63 deletions

View File

@ -25,16 +25,16 @@
package de.bluecolored.bluemap.core.world;
@FunctionalInterface
public interface ChunkConsumer {
public interface ChunkConsumer<T> {
default boolean filter(int chunkX, int chunkZ, int lastModified) {
return true;
}
void accept(int chunkX, int chunkZ, Chunk chunk);
void accept(int chunkX, int chunkZ, T chunk);
@FunctionalInterface
interface ListOnly extends ChunkConsumer {
interface ListOnly<T> extends ChunkConsumer<T> {
void accept(int chunkX, int chunkZ, int lastModified);
@ -45,7 +45,7 @@ default boolean filter(int chunkX, int chunkZ, int lastModified) {
}
@Override
default void accept(int chunkX, int chunkZ, Chunk chunk) {
default void accept(int chunkX, int chunkZ, T chunk) {
throw new IllegalStateException("Should never be called.");
}

View File

@ -26,15 +26,15 @@
import java.io.IOException;
public interface Region {
public interface Region<T> {
/**
* Directly loads and returns the specified chunk.<br>
* (implementations should consider overriding this method for a faster implementation)
*/
default Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
class SingleChunkConsumer implements ChunkConsumer {
private Chunk foundChunk = Chunk.EMPTY_CHUNK;
default T loadChunk(int chunkX, int chunkZ) throws IOException {
class SingleChunkConsumer implements ChunkConsumer<T> {
private T foundChunk = emptyChunk();
@Override
public boolean filter(int x, int z, int lastModified) {
@ -42,7 +42,7 @@ public boolean filter(int x, int z, int lastModified) {
}
@Override
public void accept(int chunkX, int chunkZ, Chunk chunk) {
public void accept(int chunkX, int chunkZ, T chunk) {
this.foundChunk = chunk;
}
}
@ -54,11 +54,13 @@ public void accept(int chunkX, int chunkZ, Chunk chunk) {
/**
* Iterates over all chunks in this region and first calls {@link ChunkConsumer#filter(int, int, int)}.<br>
* And if (any only if) that method returned <code>true</code>, the chunk will be loaded and {@link ChunkConsumer#accept(int, int, Chunk)}
* And if (any only if) that method returned <code>true</code>, the chunk will be loaded and {@link ChunkConsumer#accept(int, int, T)}
* will be called with the loaded chunk.
* @param consumer the consumer choosing which chunks to load and accepting them
* @throws IOException if an IOException occurred trying to read the region
*/
void iterateAllChunks(ChunkConsumer consumer) throws IOException;
void iterateAllChunks(ChunkConsumer<T> consumer) throws IOException;
T emptyChunk();
}

View File

@ -0,0 +1,37 @@
/*
* 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;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import java.io.IOException;
public interface ChunkLoader<T> {
T load(byte[] data, int offset, int length, Compression compression) throws IOException;
T emptyChunk();
}

View File

@ -37,7 +37,7 @@
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.WatchService;
import de.bluecolored.bluemap.core.world.*;
import de.bluecolored.bluemap.core.world.mca.chunk.ChunkLoader;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunkLoader;
import de.bluecolored.bluemap.core.world.mca.data.DimensionTypeDeserializer;
import de.bluecolored.bluemap.core.world.mca.data.LevelData;
import de.bluecolored.bluemap.core.world.mca.region.RegionType;
@ -78,8 +78,8 @@ public class MCAWorld implements World {
private final Path dimensionFolder;
private final Path regionFolder;
private final ChunkLoader chunkLoader = new ChunkLoader(this);
private final LoadingCache<Vector2i, Region> regionCache = Caffeine.newBuilder()
private final MCAChunkLoader chunkLoader = new MCAChunkLoader(this);
private final LoadingCache<Vector2i, Region<Chunk>> regionCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.softValues()
.maximumSize(32)
@ -153,11 +153,11 @@ private Chunk getChunk(Vector2i pos) {
}
@Override
public Region getRegion(int x, int z) {
public Region<Chunk> getRegion(int x, int z) {
return getRegion(VECTOR_2_I_CACHE.get(x, z));
}
private Region getRegion(Vector2i pos) {
private Region<Chunk> getRegion(Vector2i pos) {
return regionCache.get(pos);
}
@ -191,7 +191,7 @@ public WatchService<Vector2i> createRegionWatchService() throws IOException {
@Override
public void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter) {
try {
getRegion(x, z).iterateAllChunks(new ChunkConsumer() {
getRegion(x, z).iterateAllChunks(new ChunkConsumer<>() {
@Override
public boolean filter(int chunkX, int chunkZ, int lastModified) {
Vector2i chunkPos = VECTOR_2_I_CACHE.get(chunkX, chunkZ);
@ -221,12 +221,12 @@ public void invalidateChunkCache(int x, int z) {
chunkCache.invalidate(VECTOR_2_I_CACHE.get(x, z));
}
private Region loadRegion(Vector2i regionPos) {
private Region<Chunk> loadRegion(Vector2i regionPos) {
return loadRegion(regionPos.getX(), regionPos.getY());
}
private Region loadRegion(int x, int z) {
return RegionType.loadRegion(this, getRegionFolder(), x, z);
private Region<Chunk> loadRegion(int x, int z) {
return RegionType.loadRegion(chunkLoader, getRegionFolder(), x, z);
}
private Chunk loadChunk(Vector2i chunkPos) {

View File

@ -25,6 +25,8 @@
package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.storage.compression.Compression;
import de.bluecolored.bluemap.core.world.Chunk;
import de.bluecolored.bluemap.core.world.mca.ChunkLoader;
import de.bluecolored.bluemap.core.world.mca.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import lombok.Getter;
@ -37,11 +39,11 @@
import java.util.List;
import java.util.function.BiFunction;
public class ChunkLoader {
public class MCAChunkLoader implements ChunkLoader<Chunk> {
private final MCAWorld world;
public ChunkLoader(MCAWorld world) {
public MCAChunkLoader(MCAWorld world) {
this.world = world;
}
@ -79,6 +81,11 @@ public MCAChunk load(byte[] data, int offset, int length, Compression compressio
return chunk;
}
@Override
public Chunk emptyChunk() {
return Chunk.EMPTY_CHUNK;
}
private @Nullable ChunkVersionLoader<?> findBestLoaderForVersion(int version) {
for (ChunkVersionLoader<?> loader : CHUNK_VERSION_LOADERS) {
if (loader.mightSupport(version)) return loader;

View File

@ -28,8 +28,7 @@
import de.bluecolored.bluemap.core.storage.compression.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 de.bluecolored.bluemap.core.world.mca.ChunkLoader;
import lombok.Getter;
import java.io.*;
@ -61,14 +60,14 @@
*/
@Getter
public class LinearRegion implements Region {
public class LinearRegion<T> implements Region<T> {
public static final String FILE_SUFFIX = ".linear";
public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.linear$");
private static final long MAGIC = 0xc3ff13183cca9d9aL;
private final MCAWorld world;
private final ChunkLoader<T> chunkLoader;
private final Path regionFile;
private final Vector2i regionPos;
@ -82,8 +81,8 @@ public class LinearRegion implements Region {
private long dataHash;
private byte[] compressedData;
public LinearRegion(MCAWorld world, Path regionFile) throws IllegalArgumentException {
this.world = world;
public LinearRegion(ChunkLoader<T> chunkLoader, Path regionFile) throws IllegalArgumentException {
this.chunkLoader = chunkLoader;
this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\.");
@ -93,12 +92,6 @@ public LinearRegion(MCAWorld world, Path regionFile) throws IllegalArgumentExcep
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;
@ -141,7 +134,7 @@ private synchronized void init() throws IOException {
}
@Override
public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
public void iterateAllChunks(ChunkConsumer<T> consumer) throws IOException {
if (!initialized) init();
int chunkStartX = regionPos.getX() * 32;
@ -177,7 +170,7 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
chunkDataBuffer = new byte[length];
dIn.readFully(chunkDataBuffer, 0, length);
MCAChunk chunk = world.getChunkLoader().load(chunkDataBuffer, 0, length, Compression.NONE);
T chunk = chunkLoader.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
@ -193,6 +186,11 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
}
}
@Override
public T emptyChunk() {
return chunkLoader.emptyChunk();
}
public static String getRegionFileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX;
}

View File

@ -29,7 +29,8 @@
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.ChunkLoader;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunkLoader;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunk;
import lombok.Getter;
@ -43,7 +44,7 @@
import java.util.regex.Pattern;
@Getter
public class MCARegion implements Region {
public class MCARegion<T> implements Region<T> {
public static final String FILE_SUFFIX = ".mca";
public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.mca$");
@ -57,12 +58,12 @@ public class MCARegion implements Region {
CHUNK_COMPRESSION_MAP[4] = Compression.LZ4;
}
private final MCAWorld world;
private final Path regionFile;
private final ChunkLoader<T> chunkLoader;
private final Vector2i regionPos;
public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentException {
this.world = world;
public MCARegion(ChunkLoader<T> chunkLoader, Path regionFile) throws IllegalArgumentException {
this.chunkLoader = chunkLoader;
this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\.");
@ -72,18 +73,12 @@ public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentExceptio
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;
public T loadChunk(int chunkX, int chunkZ) throws IOException {
if (Files.notExists(regionFile)) return chunkLoader.emptyChunk();
long fileLength = Files.size(regionFile);
if (fileLength == 0) return Chunk.EMPTY_CHUNK;
if (fileLength == 0) return chunkLoader.emptyChunk();
try (FileChannel channel = FileChannel.open(regionFile, StandardOpenOption.READ)) {
int xzChunk = (chunkZ & 0b11111) << 5 | (chunkX & 0b11111);
@ -98,7 +93,7 @@ public Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
offset *= 4096;
int size = (header[3] & 0xFF) * 4096;
if (size == 0) return Chunk.EMPTY_CHUNK;
if (size == 0) return chunkLoader.emptyChunk();
byte[] chunkDataBuffer = new byte[size];
@ -110,7 +105,7 @@ public Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
}
@Override
public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
public void iterateAllChunks(ChunkConsumer<T> consumer) throws IOException {
if (Files.notExists(regionFile)) return;
long fileLength = Files.size(regionFile);
@ -157,7 +152,7 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
channel.position(offset);
readFully(channel, chunkDataBuffer, 0, size);
MCAChunk chunk = loadChunk(chunkDataBuffer, size);
T chunk = loadChunk(chunkDataBuffer, size);
consumer.accept(chunkX, chunkZ, chunk);
}
}
@ -165,13 +160,18 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
}
}
private MCAChunk loadChunk(byte[] data, int size) throws IOException {
@Override
public T emptyChunk() {
return chunkLoader.emptyChunk();
}
private T loadChunk(byte[] data, int size) throws IOException {
int compressionTypeId = Byte.toUnsignedInt(data[4]);
Compression compression = CHUNK_COMPRESSION_MAP[compressionTypeId];
if (compression == null)
throw new IOException("Unknown chunk compression-id: " + compressionTypeId);
return world.getChunkLoader().load(data, 5, size - 5, compression);
return chunkLoader.load(data, 5, size - 5, compression);
}
public static String getRegionFileName(int regionX, int regionZ) {

View File

@ -29,7 +29,8 @@
import de.bluecolored.bluemap.core.util.Keyed;
import de.bluecolored.bluemap.core.util.Registry;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import de.bluecolored.bluemap.core.world.mca.ChunkLoader;
import de.bluecolored.bluemap.core.world.mca.chunk.MCAChunkLoader;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable;
@ -53,7 +54,7 @@ public interface RegionType extends Keyed {
/**
* Creates a new {@link Region} from the given world and region-file
*/
Region createRegion(MCAWorld world, Path regionFile);
<T> Region<T> createRegion(ChunkLoader<T> chunkLoader, Path regionFile);
/**
* Converts region coordinates into the region-file name.
@ -84,12 +85,12 @@ public interface RegionType extends Keyed {
return null;
}
static Region loadRegion(MCAWorld world, Path regionFolder, int regionX, int regionZ) {
static <T> Region<T> loadRegion(ChunkLoader<T> chunkLoader, Path regionFolder, int regionX, int regionZ) {
for (RegionType regionType : REGISTRY.values()) {
Path regionFile = regionFolder.resolve(regionType.getRegionFileName(regionX, regionZ));
if (Files.exists(regionFile)) return regionType.createRegion(world, regionFile);
if (Files.exists(regionFile)) return regionType.createRegion(chunkLoader, regionFile);
}
return DEFAULT.createRegion(world, regionFolder.resolve(DEFAULT.getRegionFileName(regionX, regionZ)));
return DEFAULT.createRegion(chunkLoader, regionFolder.resolve(DEFAULT.getRegionFileName(regionX, regionZ)));
}
@RequiredArgsConstructor
@ -100,8 +101,8 @@ class Impl implements RegionType {
private final RegionFileNameFunction regionFileNameFunction;
private final Pattern regionFileNamePattern;
public Region createRegion(MCAWorld world, Path regionFile) {
return this.regionFactory.create(world, regionFile);
public <T> Region<T> createRegion(ChunkLoader<T> chunkLoader, Path regionFile) {
return this.regionFactory.create(chunkLoader, regionFile);
}
public String getRegionFileName(int regionX, int regionZ) {
@ -122,7 +123,7 @@ public String getRegionFileName(int regionX, int regionZ) {
@FunctionalInterface
interface RegionFactory {
Region create(MCAWorld world, Path regionFile);
<T> Region<T> create(ChunkLoader<T> chunkLoader, Path regionFile);
}
@FunctionalInterface