Extract region/chunk-loading/caching from mcaworld into generic ChunkGrid class

This commit is contained in:
Lukas Rieger (Blue) 2025-01-19 22:05:17 +01:00
parent ef4d8e7989
commit 05e12c5a74
No known key found for this signature in database
GPG Key ID: AA33883B1BBA03E6
4 changed files with 183 additions and 111 deletions

View File

@ -0,0 +1,165 @@
package de.bluecolored.bluemap.core.world.mca;
import com.flowpowered.math.vector.Vector2i;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import de.bluecolored.bluemap.core.BlueMap;
import de.bluecolored.bluemap.core.logger.Logger;
import de.bluecolored.bluemap.core.util.Vector2iCache;
import de.bluecolored.bluemap.core.util.WatchService;
import de.bluecolored.bluemap.core.world.ChunkConsumer;
import de.bluecolored.bluemap.core.world.Region;
import de.bluecolored.bluemap.core.world.mca.region.RegionType;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Stream;
@RequiredArgsConstructor
public class ChunkGrid<T> {
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
private final ChunkLoader<T> chunkLoader;
private final Path regionFolder;
private final LoadingCache<Vector2i, Region<T>> regionCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.softValues()
.maximumSize(32)
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterAccess(1, TimeUnit.MINUTES)
.build(this::loadRegion);
private final LoadingCache<Vector2i, T> chunkCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.softValues()
.maximumSize(10240) // 10 regions worth of chunks
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterAccess(1, TimeUnit.MINUTES)
.build(this::loadChunk);
public T getChunk(int x, int z) {
return getChunk(VECTOR_2_I_CACHE.get(x, z));
}
private T getChunk(Vector2i pos) {
return chunkCache.get(pos);
}
public Region<T> getRegion(int x, int z) {
return getRegion(VECTOR_2_I_CACHE.get(x, z));
}
private Region<T> getRegion(Vector2i pos) {
return regionCache.get(pos);
}
public void iterateChunks(int minX, int minZ, int maxX, int maxZ, ChunkConsumer<T> chunkConsumer) {
}
public void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter) {
try {
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);
return chunkFilter.test(chunkPos);
}
@Override
public void accept(int chunkX, int chunkZ, T chunk) {
Vector2i chunkPos = VECTOR_2_I_CACHE.get(chunkX, chunkZ);
chunkCache.put(chunkPos, chunk);
}
});
} catch (IOException ex) {
Logger.global.logDebug("Unexpected exception trying to load preload region (x:" + x + ", z:" + z + "): " + ex);
}
}
public Collection<Vector2i> listRegions() {
if (!Files.exists(regionFolder)) return Collections.emptyList();
try (Stream<Path> stream = Files.list(regionFolder)) {
return stream
.map(file -> {
try {
if (Files.size(file) <= 0) return null;
return RegionType.regionForFileName(file.getFileName().toString());
} catch (IOException ex) {
Logger.global.logError("Failed to read region-file: " + file, ex);
return null;
}
})
.filter(Objects::nonNull)
.toList();
} catch (IOException ex) {
Logger.global.logError("Failed to list regions for folder: '" + regionFolder + "'", ex);
return List.of();
}
}
public WatchService<Vector2i> createRegionWatchService() throws IOException {
return new MCAWorldRegionWatchService(this.regionFolder);
}
public void invalidateChunkCache() {
regionCache.invalidateAll();
chunkCache.invalidateAll();
}
public void invalidateChunkCache(int x, int z) {
regionCache.invalidate(VECTOR_2_I_CACHE.get(x >> 5, z >> 5));
chunkCache.invalidate(VECTOR_2_I_CACHE.get(x, z));
}
private Region<T> loadRegion(Vector2i regionPos) {
return loadRegion(regionPos.getX(), regionPos.getY());
}
private Region<T> loadRegion(int x, int z) {
return RegionType.loadRegion(chunkLoader, regionFolder, x, z);
}
private T loadChunk(Vector2i chunkPos) {
return loadChunk(chunkPos.getX(), chunkPos.getY());
}
private T loadChunk(int x, int z) {
final int tries = 3;
final int tryInterval = 1000;
Exception loadException = null;
for (int i = 0; i < tries; i++) {
try {
return getRegion(x >> 5, z >> 5)
.loadChunk(x, z);
} catch (IOException | RuntimeException e) {
if (loadException != null && loadException != e)
e.addSuppressed(loadException);
loadException = e;
if (i + 1 < tries) {
try {
Thread.sleep(tryInterval);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
Logger.global.logDebug("Unexpected exception trying to load chunk (x:" + x + ", z:" + z + "): " + loadException);
return chunkLoader.erroredChunk();
}
}

View File

@ -34,4 +34,6 @@ public interface ChunkLoader<T> {
T emptyChunk();
T erroredChunk();
}

View File

@ -66,8 +66,6 @@ public class MCAWorld implements World {
private static final Grid CHUNK_GRID = new Grid(16);
private static final Grid REGION_GRID = new Grid(32).multiply(CHUNK_GRID);
private static final Vector2iCache VECTOR_2_I_CACHE = new Vector2iCache();
private final String id;
private final Path worldFolder;
private final Key dimension;
@ -77,23 +75,8 @@ public class MCAWorld implements World {
private final DimensionType dimensionType;
private final Vector3i spawnPoint;
private final Path dimensionFolder;
private final Path regionFolder;
private final MCAChunkLoader chunkLoader = new MCAChunkLoader(this);
private final LoadingCache<Vector2i, Region<Chunk>> regionCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.softValues()
.maximumSize(32)
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterAccess(1, TimeUnit.MINUTES)
.build(this::loadRegion);
private final LoadingCache<Vector2i, Chunk> chunkCache = Caffeine.newBuilder()
.executor(BlueMap.THREAD_POOL)
.softValues()
.maximumSize(10240) // 10 regions worth of chunks
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterAccess(1, TimeUnit.MINUTES)
.build(this::loadChunk);
private final ChunkGrid<Chunk> blockChunkGrid;
private MCAWorld(Path worldFolder, Key dimension, DataPack dataPack, LevelData levelData) {
this.id = World.id(worldFolder, dimension);
@ -121,7 +104,9 @@ private MCAWorld(Path worldFolder, Key dimension, DataPack dataPack, LevelData l
levelData.getData().getSpawnZ()
);
this.dimensionFolder = resolveDimensionFolder(worldFolder, dimension);
this.regionFolder = dimensionFolder.resolve("region");
this.blockChunkGrid = new ChunkGrid<>(new MCAChunkLoader(this), dimensionFolder.resolve("region"));
}
@Override
@ -146,80 +131,37 @@ public Chunk getChunkAtBlock(int x, int z) {
@Override
public Chunk getChunk(int x, int z) {
return getChunk(VECTOR_2_I_CACHE.get(x, z));
}
private Chunk getChunk(Vector2i pos) {
return chunkCache.get(pos);
return blockChunkGrid.getChunk(x, z);
}
@Override
public Region<Chunk> getRegion(int x, int z) {
return getRegion(VECTOR_2_I_CACHE.get(x, z));
}
private Region<Chunk> getRegion(Vector2i pos) {
return regionCache.get(pos);
return blockChunkGrid.getRegion(x, z);
}
@Override
public Collection<Vector2i> listRegions() {
if (!Files.exists(regionFolder)) return Collections.emptyList();
try (Stream<Path> stream = Files.list(regionFolder)) {
return stream
.map(file -> {
try {
if (Files.size(file) <= 0) return null;
return RegionType.regionForFileName(file.getFileName().toString());
} catch (IOException ex) {
Logger.global.logError("Failed to read region-file: " + file, ex);
return null;
}
})
.filter(Objects::nonNull)
.toList();
} catch (IOException ex) {
Logger.global.logError("Failed to list regions for world: '" + getId() + "'", ex);
return List.of();
}
return blockChunkGrid.listRegions();
}
@Override
public WatchService<Vector2i> createRegionWatchService() throws IOException {
return new MCAWorldRegionWatchService(this.regionFolder);
return blockChunkGrid.createRegionWatchService();
}
@Override
public void preloadRegionChunks(int x, int z, Predicate<Vector2i> chunkFilter) {
try {
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);
return chunkFilter.test(chunkPos);
}
@Override
public void accept(int chunkX, int chunkZ, Chunk chunk) {
Vector2i chunkPos = VECTOR_2_I_CACHE.get(chunkX, chunkZ);
chunkCache.put(chunkPos, chunk);
}
});
} catch (IOException ex) {
Logger.global.logDebug("Unexpected exception trying to load preload region (x:" + x + ", z:" + z + "): " + ex);
}
blockChunkGrid.preloadRegionChunks(x, z, chunkFilter);
}
@Override
public void invalidateChunkCache() {
regionCache.invalidateAll();
chunkCache.invalidateAll();
blockChunkGrid.invalidateChunkCache();
}
@Override
public void invalidateChunkCache(int x, int z) {
regionCache.invalidate(VECTOR_2_I_CACHE.get(x >> 5, z >> 5));
chunkCache.invalidate(VECTOR_2_I_CACHE.get(x, z));
blockChunkGrid.invalidateChunkCache(x, z);
}
@Override
@ -227,48 +169,6 @@ public void iterateEntities(int minX, int minZ, int maxX, int maxZ, Consumer<Ent
//TODO
}
private Region<Chunk> loadRegion(Vector2i regionPos) {
return loadRegion(regionPos.getX(), regionPos.getY());
}
private Region<Chunk> loadRegion(int x, int z) {
return RegionType.loadRegion(chunkLoader, getRegionFolder(), x, z);
}
private Chunk loadChunk(Vector2i chunkPos) {
return loadChunk(chunkPos.getX(), chunkPos.getY());
}
private Chunk loadChunk(int x, int z) {
final int tries = 3;
final int tryInterval = 1000;
Exception loadException = null;
for (int i = 0; i < tries; i++) {
try {
return getRegion(x >> 5, z >> 5)
.loadChunk(x, z);
} catch (IOException | RuntimeException e) {
if (loadException != null && loadException != e)
e.addSuppressed(loadException);
loadException = e;
if (i + 1 < tries) {
try {
Thread.sleep(tryInterval);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
break;
}
}
}
}
Logger.global.logDebug("Unexpected exception trying to load chunk (x:" + x + ", z:" + z + "): " + loadException);
return Chunk.ERRORED_CHUNK;
}
public static MCAWorld load(Path worldFolder, Key dimension, DataPack dataPack) throws IOException, InterruptedException {
// load level.dat

View File

@ -86,6 +86,11 @@ public Chunk emptyChunk() {
return Chunk.EMPTY_CHUNK;
}
@Override
public Chunk erroredChunk() {
return Chunk.ERRORED_CHUNK;
}
private @Nullable ChunkVersionLoader<?> findBestLoaderForVersion(int version) {
for (ChunkVersionLoader<?> loader : CHUNK_VERSION_LOADERS) {
if (loader.mightSupport(version)) return loader;