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

View File

@ -26,15 +26,15 @@
import java.io.IOException; import java.io.IOException;
public interface Region { public interface Region<T> {
/** /**
* Directly loads and returns the specified chunk.<br> * Directly loads and returns the specified chunk.<br>
* (implementations should consider overriding this method for a faster implementation) * (implementations should consider overriding this method for a faster implementation)
*/ */
default Chunk loadChunk(int chunkX, int chunkZ) throws IOException { default T loadChunk(int chunkX, int chunkZ) throws IOException {
class SingleChunkConsumer implements ChunkConsumer { class SingleChunkConsumer implements ChunkConsumer<T> {
private Chunk foundChunk = Chunk.EMPTY_CHUNK; private T foundChunk = emptyChunk();
@Override @Override
public boolean filter(int x, int z, int lastModified) { public boolean filter(int x, int z, int lastModified) {
@ -42,7 +42,7 @@ public boolean filter(int x, int z, int lastModified) {
} }
@Override @Override
public void accept(int chunkX, int chunkZ, Chunk chunk) { public void accept(int chunkX, int chunkZ, T chunk) {
this.foundChunk = 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> * 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. * will be called with the loaded chunk.
* @param consumer the consumer choosing which chunks to load and accepting them * @param consumer the consumer choosing which chunks to load and accepting them
* @throws IOException if an IOException occurred trying to read the region * @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.Vector2iCache;
import de.bluecolored.bluemap.core.util.WatchService; import de.bluecolored.bluemap.core.util.WatchService;
import de.bluecolored.bluemap.core.world.*; 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.DimensionTypeDeserializer;
import de.bluecolored.bluemap.core.world.mca.data.LevelData; import de.bluecolored.bluemap.core.world.mca.data.LevelData;
import de.bluecolored.bluemap.core.world.mca.region.RegionType; 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 dimensionFolder;
private final Path regionFolder; private final Path regionFolder;
private final ChunkLoader chunkLoader = new ChunkLoader(this); private final MCAChunkLoader chunkLoader = new MCAChunkLoader(this);
private final LoadingCache<Vector2i, Region> regionCache = Caffeine.newBuilder() private final LoadingCache<Vector2i, Region<Chunk>> regionCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL) .executor(BlueMap.THREAD_POOL)
.softValues() .softValues()
.maximumSize(32) .maximumSize(32)
@ -153,11 +153,11 @@ private Chunk getChunk(Vector2i pos) {
} }
@Override @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)); return getRegion(VECTOR_2_I_CACHE.get(x, z));
} }
private Region getRegion(Vector2i pos) { private Region<Chunk> getRegion(Vector2i pos) {
return regionCache.get(pos); return regionCache.get(pos);
} }
@ -191,7 +191,7 @@ public WatchService<Vector2i> createRegionWatchService() throws IOException {
@Override @Override
public void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter) { public void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter) {
try { try {
getRegion(x, z).iterateAllChunks(new ChunkConsumer() { getRegion(x, z).iterateAllChunks(new ChunkConsumer<>() {
@Override @Override
public boolean filter(int chunkX, int chunkZ, int lastModified) { public boolean filter(int chunkX, int chunkZ, int lastModified) {
Vector2i chunkPos = VECTOR_2_I_CACHE.get(chunkX, chunkZ); 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)); 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()); return loadRegion(regionPos.getX(), regionPos.getY());
} }
private Region loadRegion(int x, int z) { private Region<Chunk> loadRegion(int x, int z) {
return RegionType.loadRegion(this, getRegionFolder(), x, z); return RegionType.loadRegion(chunkLoader, getRegionFolder(), x, z);
} }
private Chunk loadChunk(Vector2i chunkPos) { private Chunk loadChunk(Vector2i chunkPos) {

View File

@ -25,6 +25,8 @@
package de.bluecolored.bluemap.core.world.mca.chunk; package de.bluecolored.bluemap.core.world.mca.chunk;
import de.bluecolored.bluemap.core.storage.compression.Compression; 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.MCAUtil;
import de.bluecolored.bluemap.core.world.mca.MCAWorld; import de.bluecolored.bluemap.core.world.mca.MCAWorld;
import lombok.Getter; import lombok.Getter;
@ -37,11 +39,11 @@
import java.util.List; import java.util.List;
import java.util.function.BiFunction; import java.util.function.BiFunction;
public class ChunkLoader { public class MCAChunkLoader implements ChunkLoader<Chunk> {
private final MCAWorld world; private final MCAWorld world;
public ChunkLoader(MCAWorld world) { public MCAChunkLoader(MCAWorld world) {
this.world = world; this.world = world;
} }
@ -79,6 +81,11 @@ public MCAChunk load(byte[] data, int offset, int length, Compression compressio
return chunk; return chunk;
} }
@Override
public Chunk emptyChunk() {
return Chunk.EMPTY_CHUNK;
}
private @Nullable ChunkVersionLoader<?> findBestLoaderForVersion(int version) { private @Nullable ChunkVersionLoader<?> findBestLoaderForVersion(int version) {
for (ChunkVersionLoader<?> loader : CHUNK_VERSION_LOADERS) { for (ChunkVersionLoader<?> loader : CHUNK_VERSION_LOADERS) {
if (loader.mightSupport(version)) return loader; 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.storage.compression.Compression;
import de.bluecolored.bluemap.core.world.ChunkConsumer; import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region; 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.MCAChunk;
import lombok.Getter; import lombok.Getter;
import java.io.*; import java.io.*;
@ -61,14 +60,14 @@
*/ */
@Getter @Getter
public class LinearRegion implements Region { public class LinearRegion<T> implements Region<T> {
public static final String FILE_SUFFIX = ".linear"; public static final String FILE_SUFFIX = ".linear";
public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.linear$"); public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.linear$");
private static final long MAGIC = 0xc3ff13183cca9d9aL; private static final long MAGIC = 0xc3ff13183cca9d9aL;
private final MCAWorld world; private final ChunkLoader<T> chunkLoader;
private final Path regionFile; private final Path regionFile;
private final Vector2i regionPos; private final Vector2i regionPos;
@ -82,8 +81,8 @@ public class LinearRegion implements Region {
private long dataHash; private long dataHash;
private byte[] compressedData; private byte[] compressedData;
public LinearRegion(MCAWorld world, Path regionFile) throws IllegalArgumentException { public LinearRegion(ChunkLoader<T> chunkLoader, Path regionFile) throws IllegalArgumentException {
this.world = world; this.chunkLoader = chunkLoader;
this.regionFile = regionFile; this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\."); String[] filenameParts = regionFile.getFileName().toString().split("\\.");
@ -93,12 +92,6 @@ public LinearRegion(MCAWorld world, Path regionFile) throws IllegalArgumentExcep
this.regionPos = new Vector2i(rX, rZ); 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 { private synchronized void init() throws IOException {
if (initialized) return; if (initialized) return;
@ -141,7 +134,7 @@ private synchronized void init() throws IOException {
} }
@Override @Override
public void iterateAllChunks(ChunkConsumer consumer) throws IOException { public void iterateAllChunks(ChunkConsumer<T> consumer) throws IOException {
if (!initialized) init(); if (!initialized) init();
int chunkStartX = regionPos.getX() * 32; int chunkStartX = regionPos.getX() * 32;
@ -177,7 +170,7 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
chunkDataBuffer = new byte[length]; chunkDataBuffer = new byte[length];
dIn.readFully(chunkDataBuffer, 0, 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); consumer.accept(chunkX, chunkZ, chunk);
} else { } else {
// skip before reading the next chunk, but only if there is a next chunk // 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) { public static String getRegionFileName(int regionX, int regionZ) {
return "r." + regionX + "." + regionZ + FILE_SUFFIX; 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.Chunk;
import de.bluecolored.bluemap.core.world.ChunkConsumer; import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region; 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 de.bluecolored.bluemap.core.world.mca.chunk.MCAChunk;
import lombok.Getter; import lombok.Getter;
@ -43,7 +44,7 @@
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Getter @Getter
public class MCARegion implements Region { public class MCARegion<T> implements Region<T> {
public static final String FILE_SUFFIX = ".mca"; public static final String FILE_SUFFIX = ".mca";
public static final Pattern FILE_PATTERN = Pattern.compile("^r\\.(-?\\d+)\\.(-?\\d+)\\.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; CHUNK_COMPRESSION_MAP[4] = Compression.LZ4;
} }
private final MCAWorld world;
private final Path regionFile; private final Path regionFile;
private final ChunkLoader<T> chunkLoader;
private final Vector2i regionPos; private final Vector2i regionPos;
public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentException { public MCARegion(ChunkLoader<T> chunkLoader, Path regionFile) throws IllegalArgumentException {
this.world = world; this.chunkLoader = chunkLoader;
this.regionFile = regionFile; this.regionFile = regionFile;
String[] filenameParts = regionFile.getFileName().toString().split("\\."); String[] filenameParts = regionFile.getFileName().toString().split("\\.");
@ -72,18 +73,12 @@ public MCARegion(MCAWorld world, Path regionFile) throws IllegalArgumentExceptio
this.regionPos = new Vector2i(rX, rZ); 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 @Override
public Chunk loadChunk(int chunkX, int chunkZ) throws IOException { public T loadChunk(int chunkX, int chunkZ) throws IOException {
if (Files.notExists(regionFile)) return Chunk.EMPTY_CHUNK; if (Files.notExists(regionFile)) return chunkLoader.emptyChunk();
long fileLength = Files.size(regionFile); 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)) { try (FileChannel channel = FileChannel.open(regionFile, StandardOpenOption.READ)) {
int xzChunk = (chunkZ & 0b11111) << 5 | (chunkX & 0b11111); int xzChunk = (chunkZ & 0b11111) << 5 | (chunkX & 0b11111);
@ -98,7 +93,7 @@ public Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
offset *= 4096; offset *= 4096;
int size = (header[3] & 0xFF) * 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]; byte[] chunkDataBuffer = new byte[size];
@ -110,7 +105,7 @@ public Chunk loadChunk(int chunkX, int chunkZ) throws IOException {
} }
@Override @Override
public void iterateAllChunks(ChunkConsumer consumer) throws IOException { public void iterateAllChunks(ChunkConsumer<T> consumer) throws IOException {
if (Files.notExists(regionFile)) return; if (Files.notExists(regionFile)) return;
long fileLength = Files.size(regionFile); long fileLength = Files.size(regionFile);
@ -157,7 +152,7 @@ public void iterateAllChunks(ChunkConsumer consumer) throws IOException {
channel.position(offset); channel.position(offset);
readFully(channel, chunkDataBuffer, 0, size); readFully(channel, chunkDataBuffer, 0, size);
MCAChunk chunk = loadChunk(chunkDataBuffer, size); T chunk = loadChunk(chunkDataBuffer, size);
consumer.accept(chunkX, chunkZ, chunk); 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]); int compressionTypeId = Byte.toUnsignedInt(data[4]);
Compression compression = CHUNK_COMPRESSION_MAP[compressionTypeId]; Compression compression = CHUNK_COMPRESSION_MAP[compressionTypeId];
if (compression == null) if (compression == null)
throw new IOException("Unknown chunk compression-id: " + compressionTypeId); 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) { 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.Keyed;
import de.bluecolored.bluemap.core.util.Registry; import de.bluecolored.bluemap.core.util.Registry;
import de.bluecolored.bluemap.core.world.Region; 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.Getter;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.Nullable; 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 * 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. * Converts region coordinates into the region-file name.
@ -84,12 +85,12 @@ public interface RegionType extends Keyed {
return null; 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()) { for (RegionType regionType : REGISTRY.values()) {
Path regionFile = regionFolder.resolve(regionType.getRegionFileName(regionX, regionZ)); 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 @RequiredArgsConstructor
@ -100,8 +101,8 @@ class Impl implements RegionType {
private final RegionFileNameFunction regionFileNameFunction; private final RegionFileNameFunction regionFileNameFunction;
private final Pattern regionFileNamePattern; private final Pattern regionFileNamePattern;
public Region createRegion(MCAWorld world, Path regionFile) { public <T> Region<T> createRegion(ChunkLoader<T> chunkLoader, Path regionFile) {
return this.regionFactory.create(world, regionFile); return this.regionFactory.create(chunkLoader, regionFile);
} }
public String getRegionFileName(int regionX, int regionZ) { public String getRegionFileName(int regionX, int regionZ) {
@ -122,7 +123,7 @@ public String getRegionFileName(int regionX, int regionZ) {
@FunctionalInterface @FunctionalInterface
interface RegionFactory { interface RegionFactory {
Region create(MCAWorld world, Path regionFile); <T> Region<T> create(ChunkLoader<T> chunkLoader, Path regionFile);
} }
@FunctionalInterface @FunctionalInterface