From f650b18eb52ad007b09a67af12a0a16318a89484 Mon Sep 17 00:00:00 2001 From: DGun Otto Date: Mon, 15 Jan 2024 19:32:57 +0800 Subject: [PATCH] Update 0010-2-Add-Linear-region-format.patch --- .../0010-2-Add-Linear-region-format.patch | 1069 ----------------- 1 file changed, 1069 deletions(-) diff --git a/patches/server/0010-2-Add-Linear-region-format.patch b/patches/server/0010-2-Add-Linear-region-format.patch index c1859e1c..8b137891 100644 --- a/patches/server/0010-2-Add-Linear-region-format.patch +++ b/patches/server/0010-2-Add-Linear-region-format.patch @@ -1,1070 +1 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Triassic -Date: Sat, 23 Sep 2023 01:18:14 +0300 -Subject: [PATCH] Add Linear region format - -diff --git a/build.gradle.kts b/build.gradle.kts -index 81ec1a0a7dac7f74f6b9709ae8b284a12c7e6433..3f6f9e9386d7c5d0f4a4715fb5ac9293784c1c75 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -34,6 +34,10 @@ dependencies { - alsoShade(log4jPlugins.output) - implementation("io.netty:netty-codec-haproxy:4.1.97.Final") // Paper - Add support for proxy protocol - // Paper end -+ // LinearPurpur start - Linear format -+ implementation("com.github.luben:zstd-jni:1.5.5-11") -+ implementation("org.lz4:lz4-java:1.8.0") -+ // LinearPurpur end - implementation("org.apache.logging.log4j:log4j-iostreams:2.19.0") // Paper - remove exclusion - implementation("org.ow2.asm:asm:9.5") - implementation("org.ow2.asm:asm-commons:9.5") // Paper - ASM event executor generation -diff --git a/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java -index f2c27e0ac65be4b75c1d86ef6fd45fdb538d96ac..bcd35b189bf5ec44733161df952ef6957f7fbe79 100644 ---- a/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java -+++ b/src/main/java/com/destroystokyo/paper/io/PaperFileIOThread.java -@@ -314,8 +314,8 @@ public final class PaperFileIOThread extends QueueExecutorThread { - public abstract void writeData(final int x, final int z, final CompoundTag compound) throws IOException; - public abstract CompoundTag readData(final int x, final int z) throws IOException; - -- public abstract T computeForRegionFile(final int chunkX, final int chunkZ, final Function function); -- public abstract T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function); -+ public abstract T computeForRegionFile(final int chunkX, final int chunkZ, final Function function); // LinearPurpur -+ public abstract T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function); // LinearPurpur - - public static final class InProgressWrite { - public long writeCounter; -diff --git a/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java -index 8a11e10b01fa012b2f98b1c193c53251e848f909..17b6199a8b7184cf5b4b59d88ed8bdcae88bf0bf 100644 ---- a/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java -+++ b/src/main/java/io/papermc/paper/chunk/system/io/RegionFileIOThread.java -@@ -811,7 +811,7 @@ public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { - final ChunkDataController taskController) { - final ChunkPos chunkPos = new ChunkPos(chunkX, chunkZ); - if (intendingToBlock) { -- return taskController.computeForRegionFile(chunkX, chunkZ, true, (final RegionFile file) -> { -+ return taskController.computeForRegionFile(chunkX, chunkZ, true, (final org.purpurmc.purpur.region.AbstractRegionFile file) -> { // LinearPurpur - if (file == null) { // null if no regionfile exists - return Boolean.FALSE; - } -@@ -824,7 +824,7 @@ public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { - return Boolean.FALSE; - } // else: it either exists or is not known, fall back to checking the loaded region file - -- return taskController.computeForRegionFileIfLoaded(chunkX, chunkZ, (final RegionFile file) -> { -+ return taskController.computeForRegionFileIfLoaded(chunkX, chunkZ, (final org.purpurmc.purpur.region.AbstractRegionFile file) -> { // LinearPurpur - if (file == null) { // null if not loaded - // not sure at this point, let the I/O thread figure it out - return Boolean.TRUE; -@@ -1126,9 +1126,9 @@ public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { - return this.getCache().doesRegionFileNotExistNoIO(new ChunkPos(chunkX, chunkZ)); - } - -- public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) { -+ public T computeForRegionFile(final int chunkX, final int chunkZ, final boolean existingOnly, final Function function) { // LinearPurpur - final RegionFileStorage cache = this.getCache(); -- final RegionFile regionFile; -+ final org.purpurmc.purpur.region.AbstractRegionFile regionFile; // LinearPurpur - synchronized (cache) { - try { - regionFile = cache.getRegionFile(new ChunkPos(chunkX, chunkZ), existingOnly, true); -@@ -1141,19 +1141,19 @@ public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { - return function.apply(regionFile); - } finally { - if (regionFile != null) { -- regionFile.fileLock.unlock(); -+ regionFile.getFileLock().unlock(); // LinearPurpur - } - } - } - -- public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) { -+ public T computeForRegionFileIfLoaded(final int chunkX, final int chunkZ, final Function function) { // LinearPurpur - final RegionFileStorage cache = this.getCache(); -- final RegionFile regionFile; -+ final org.purpurmc.purpur.region.AbstractRegionFile regionFile; // LinearPurpur - - synchronized (cache) { - regionFile = cache.getRegionFileIfLoaded(new ChunkPos(chunkX, chunkZ)); - if (regionFile != null) { -- regionFile.fileLock.lock(); -+ regionFile.getFileLock().lock(); // LinearPurpur - } - } - -@@ -1161,7 +1161,7 @@ public final class RegionFileIOThread extends PrioritisedQueueExecutorThread { - return function.apply(regionFile); - } finally { - if (regionFile != null) { -- regionFile.fileLock.unlock(); -+ regionFile.getFileLock().unlock(); // LinearPurpur - } - } - } -diff --git a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java -index 513833c2ea23df5b079d157bc5cb89d5c9754c0b..5c0718146152db7655d10ed0a9477b1b49e552f9 100644 ---- a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java -+++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java -@@ -84,8 +84,15 @@ public class ThreadedWorldUpgrader { - LOGGER.info("Found " + regionFiles.length + " regionfiles to convert"); - LOGGER.info("Starting conversion now for world " + this.worldName); - -+ // LinearPurpur start -+ org.purpurmc.purpur.region.RegionFileFormat formatName = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.regionFormatName; -+ int linearCompression = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.regionFormatLinearCompressionLevel; -+ boolean linearCrashOnBrokenSymlink = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.linearCrashOnBrokenSymlink; -+ LOGGER.info("Using format " + formatName + " (" + linearCompression + ")"); -+ // LinearPurpur end -+ - final WorldInfo info = new WorldInfo(() -> worldPersistentData, -- new ChunkStorage(regionFolder.toPath(), this.dataFixer, false), this.removeCaches, this.dimensionType, this.generatorKey); -+ new ChunkStorage(formatName, linearCompression, linearCrashOnBrokenSymlink, regionFolder.toPath(), this.dataFixer, false), this.removeCaches, this.dimensionType, this.generatorKey); // LinearPurpur - - long expectedChunks = (long)regionFiles.length * (32L * 32L); - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 2631c05a3855c71eff3f8cf8d13920430f929a13..c7c633ae31b8a7d6e7926a3068d53518d2330ac9 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -247,7 +247,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - // Paper end - optimise chunk tick iteration - - public ChunkMap(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor executor, BlockableEventLoop mainThreadExecutor, LightChunkGetter chunkProvider, ChunkGenerator chunkGenerator, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory, int viewDistance, boolean dsync) { -- super(session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); -+ super(world.getLevel().purpurConfig.regionFormatName, world.getLevel().purpurConfig.regionFormatLinearCompressionLevel, world.getLevel().purpurConfig.linearCrashOnBrokenSymlink, session.getDimensionPath(world.dimension()).resolve("region"), dataFixer, dsync); // LinearPurpur - // Paper - rewrite chunk system - this.tickingGenerated = new AtomicInteger(); - this.playerMap = new PlayerMap(); -@@ -292,7 +292,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), null, null); // Paper - rewrite chunk system - this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); - this.overworldDataStorage = persistentStateManagerFactory; -- this.poiManager = new PoiManager(path.resolve("poi"), dataFixer, dsync, iregistrycustom, world); -+ this.poiManager = new PoiManager(this.level.purpurConfig.regionFormatName, this.level.purpurConfig.regionFormatLinearCompressionLevel, this.level.purpurConfig.linearCrashOnBrokenSymlink, path.resolve("poi"), dataFixer, dsync, iregistrycustom, world); // LinearPurpur - this.setServerViewDistance(viewDistance); - // Paper start - this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new); -@@ -886,13 +886,13 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - - // Paper start - chunk status cache "api" - public ChunkStatus getChunkStatusOnDiskIfCached(ChunkPos chunkPos) { -- net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos); -+ org.purpurmc.purpur.region.AbstractRegionFile regionFile = regionFileCache.getRegionFileIfLoaded(chunkPos); // LinearPurpur - - return regionFile == null ? null : regionFile.getStatusIfCached(chunkPos.x, chunkPos.z); - } - - public ChunkStatus getChunkStatusOnDisk(ChunkPos chunkPos) throws IOException { -- net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true); -+ org.purpurmc.purpur.region.AbstractRegionFile regionFile = regionFileCache.getRegionFile(chunkPos, true); // LinearPurpur - - if (regionFile == null || !regionFileCache.chunkExists(chunkPos)) { - return null; -@@ -910,7 +910,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void updateChunkStatusOnDisk(ChunkPos chunkPos, @Nullable CompoundTag compound) throws IOException { -- net.minecraft.world.level.chunk.storage.RegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false); -+ org.purpurmc.purpur.region.AbstractRegionFile regionFile = regionFileCache.getRegionFile(chunkPos, false); // LinearPurpur - - regionFile.setStatus(chunkPos.x, chunkPos.z, ChunkSerializer.getStatus(compound)); - } -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 90000d74cfb009e2cbd7336ae11077d67ef67f7b..c630f62bf98f92fd4a5d91da20d2274f8af1b6fd 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -426,8 +426,8 @@ public class ServerLevel extends Level implements WorldGenLevel { - - private static final class EntityRegionFileStorage extends net.minecraft.world.level.chunk.storage.RegionFileStorage { - -- public EntityRegionFileStorage(Path directory, boolean dsync) { -- super(directory, dsync); -+ public EntityRegionFileStorage(org.purpurmc.purpur.region.RegionFileFormat format, int linearCompression, boolean linearCrashOnBrokenSymlink, Path directory, boolean dsync) { // LinearPurpur -+ super(format, linearCompression, linearCrashOnBrokenSymlink, directory, dsync); // LinearPurpur - } - - protected void write(ChunkPos pos, net.minecraft.nbt.CompoundTag nbt) throws IOException { -@@ -753,7 +753,7 @@ public class ServerLevel extends Level implements WorldGenLevel { - // CraftBukkit end - boolean flag2 = minecraftserver.forceSynchronousWrites(); - DataFixer datafixer = minecraftserver.getFixerUpper(); -- this.entityStorage = new EntityRegionFileStorage(convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system //EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver); -+ this.entityStorage = new EntityRegionFileStorage(this.getLevel().purpurConfig.regionFormatName, this.getLevel().purpurConfig.regionFormatLinearCompressionLevel, this.getLevel().purpurConfig.linearCrashOnBrokenSymlink, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), flag2); // Paper - rewrite chunk system //EntityPersistentStorage entitypersistentstorage = new EntityStorage(this, convertable_conversionsession.getDimensionPath(resourcekey).resolve("entities"), datafixer, flag2, minecraftserver); // LinearPurpur - - // this.entityManager = new PersistentEntitySectionManager<>(Entity.class, new ServerLevel.EntityCallbacks(), entitypersistentstorage, this.entitySliceManager); // Paper // Paper - rewrite chunk system - StructureTemplateManager structuretemplatemanager = minecraftserver.getStructureManager(); -diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -index f2a7cb6ebed7a4b4019a09af2a025f624f6fe9c9..61c3730a1448e89c59983a1e92507592f61de964 100644 ---- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -@@ -61,7 +61,7 @@ public class WorldUpgrader { - private volatile int skipped; - private final Reference2FloatMap> progressMap = Reference2FloatMaps.synchronize(new Reference2FloatOpenHashMap()); - private volatile Component status = Component.translatable("optimizeWorld.stage.counting"); -- public static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mca$"); -+ public static Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.(linear | mca)$"); // LinearPurpur - private final DimensionDataStorage overworldDataStorage; - - public WorldUpgrader(LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, Registry dimensionOptionsRegistry, boolean eraseCache) { -@@ -116,7 +116,13 @@ public class WorldUpgrader { - ResourceKey resourcekey1 = (ResourceKey) iterator1.next(); - Path path = this.levelStorage.getDimensionPath(resourcekey1); - -- builder1.put(resourcekey1, new ChunkStorage(path.resolve("region"), this.dataFixer, true)); -+ // LinearPurpur start -+ String worldName = this.levelStorage.getLevelId(); -+ org.purpurmc.purpur.region.RegionFileFormat formatName = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.regionFormatName; -+ int linearCompression = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.regionFormatLinearCompressionLevel; -+ boolean linearCrashOnBrokenSymlink = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.linearCrashOnBrokenSymlink; -+ builder1.put(resourcekey1, new ChunkStorage(formatName, linearCompression, linearCrashOnBrokenSymlink, path.resolve("region"), this.dataFixer, true)); -+ // LinearPurpur end - } - - ImmutableMap, ChunkStorage> immutablemap1 = builder1.build(); -@@ -235,7 +241,7 @@ public class WorldUpgrader { - File file = this.levelStorage.getDimensionPath(world).toFile(); - File file1 = new File(file, "region"); - File[] afile = file1.listFiles((file2, s) -> { -- return s.endsWith(".mca"); -+ return s.endsWith(".mca") || s.endsWith(".linear"); // LinearPurpur - }); - - if (afile == null) { -@@ -254,7 +260,11 @@ public class WorldUpgrader { - int l = Integer.parseInt(matcher.group(2)) << 5; - - try { -- RegionFile regionfile = new RegionFile(file2.toPath(), file1.toPath(), true); -+ // LinearPurpur start -+ String worldName = this.levelStorage.getLevelId(); -+ int linearCompression = ((org.bukkit.craftbukkit.CraftWorld) org.bukkit.Bukkit.getWorld(worldName)).getHandle().purpurConfig.regionFormatLinearCompressionLevel; -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = org.purpurmc.purpur.region.AbstractRegionFileFactory.getAbstractRegionFile(linearCompression, file2.toPath(), file1.toPath(), true); -+ // LinearPurpur end - - try { - for (int i1 = 0; i1 < 32; ++i1) { -diff --git a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -index 12a7aaeaa8b4b788b620b1985591c3b93253ccd5..af639cc29999a49f4f2d494dc82f99577c6d9d1a 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -+++ b/src/main/java/net/minecraft/world/entity/ai/village/poi/PoiManager.java -@@ -57,8 +57,8 @@ public class PoiManager extends SectionStorage { - // Paper end - rewrite chunk system - - -- public PoiManager(Path path, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world) { -- super(path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, registryManager, world); -+ public PoiManager(org.purpurmc.purpur.region.RegionFileFormat formatName, int linearCompression, boolean linearCrashOnBrokenSymlink, Path path, DataFixer dataFixer, boolean dsync, RegistryAccess registryManager, LevelHeightAccessor world) { // LinearPurpur -+ super(formatName, linearCompression, linearCrashOnBrokenSymlink, path, PoiSection::codec, PoiSection::new, dataFixer, DataFixTypes.POI_CHUNK, dsync, registryManager, world); // LinearPurpur - this.world = (net.minecraft.server.level.ServerLevel)world; // Paper - rewrite chunk system - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -index 8ebecb588058da174b0e0e19e54fcddfeeca1422..473484d8187a3f2224943cd0cbbdbc28334edb61 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java -@@ -37,11 +37,11 @@ public class ChunkStorage implements AutoCloseable { - public final RegionFileStorage regionFileCache; - // Paper end - async chunk loading - -- public ChunkStorage(Path directory, DataFixer dataFixer, boolean dsync) { -+ public ChunkStorage(org.purpurmc.purpur.region.RegionFileFormat format, int linearCompression, boolean linearCrashOnBrokenSymlink, Path directory, DataFixer dataFixer, boolean dsync) { // LinearPurpur - this.fixerUpper = dataFixer; - // Paper start - async chunk io - // remove IO worker -- this.regionFileCache = new RegionFileStorage(directory, dsync, true); // Paper - nuke IOWorker // Paper -+ this.regionFileCache = new RegionFileStorage(format, linearCompression, linearCrashOnBrokenSymlink, directory, dsync, true); // Paper - nuke IOWorker // Paper // LinearPurpur - // Paper end - async chunk io - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index 9248769e6d357f6eec68945fd7700e79b2942c41..31bdeb271088f83429d97e2d28610bce930be702 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -26,7 +26,7 @@ import net.minecraft.nbt.NbtIo; // Paper - import net.minecraft.world.level.ChunkPos; - import org.slf4j.Logger; - --public class RegionFile implements AutoCloseable { -+public class RegionFile implements AutoCloseable, org.purpurmc.purpur.region.AbstractRegionFile { // LinearPurpur - - private static final Logger LOGGER = LogUtils.getLogger(); - private static final int SECTOR_BYTES = 4096; -@@ -50,6 +50,16 @@ public class RegionFile implements AutoCloseable { - public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(); // Paper - public final Path regionFile; // Paper - -+ // LinearPurpur start - Abstract getters -+ public Path getRegionFile() { -+ return this.regionFile; -+ } -+ -+ public java.util.concurrent.locks.ReentrantLock getFileLock() { -+ return this.fileLock; -+ } -+ // LinearPurpur end -+ - // Paper start - try to recover from RegionFile header corruption - private static long roundToSectors(long bytes) { - long sectors = bytes >>> 12; // 4096 = 2^12 -@@ -128,7 +138,7 @@ public class RegionFile implements AutoCloseable { - } - - // note: only call for CHUNK regionfiles -- boolean recalculateHeader() throws IOException { -+ public boolean recalculateHeader() throws IOException { // LinearPurpur - if (!this.canRecalcHeader) { - return false; - } -@@ -954,10 +964,10 @@ public class RegionFile implements AutoCloseable { - private static int getChunkIndex(int x, int z) { - return (x & 31) + (z & 31) * 32; - } -- synchronized boolean isOversized(int x, int z) { -+ public synchronized boolean isOversized(int x, int z) { // LinearPurpur - return this.oversized[getChunkIndex(x, z)] == 1; - } -- synchronized void setOversized(int x, int z, boolean oversized) throws IOException { -+ public synchronized void setOversized(int x, int z, boolean oversized) throws IOException { // LinearPurpur - final int offset = getChunkIndex(x, z); - boolean previous = this.oversized[offset] == 1; - this.oversized[offset] = (byte) (oversized ? 1 : 0); -@@ -996,7 +1006,7 @@ public class RegionFile implements AutoCloseable { - return this.regionFile.getParent().resolve(this.regionFile.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt"); - } - -- synchronized CompoundTag getOversizedData(int x, int z) throws IOException { -+ public synchronized CompoundTag getOversizedData(int x, int z) throws IOException { // LinearPurpur - Path file = getOversizedFile(x, z); - try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) { - return NbtIo.read((java.io.DataInput) out); -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index 6eaeb2db0da59611501f2b1a63b5b48816a0ba48..ba10c7ffb08f806d8d9d0291276a7321421651fc 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -19,11 +19,17 @@ import net.minecraft.world.level.ChunkPos; - - public class RegionFileStorage implements AutoCloseable { - -+ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // LinearPurpur - public static final String ANVIL_EXTENSION = ".mca"; - private static final int MAX_CACHE_SIZE = 256; -- public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap(); -+ public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap(); // LinearPurpur - private final Path folder; - private final boolean sync; -+ // LinearPurpur start - Per world chunk format -+ public final org.purpurmc.purpur.region.RegionFileFormat format; -+ public final int linearCompression; -+ public final boolean linearCrashOnBrokenSymlink; -+ // LinearPurpur end - private final boolean isChunkData; // Paper - - // Paper start - cache regionfile does not exist state -@@ -55,11 +61,16 @@ public class RegionFileStorage implements AutoCloseable { - } - // Paper end - cache regionfile does not exist state - -- protected RegionFileStorage(Path directory, boolean dsync) { // Paper - protected constructor -+ protected RegionFileStorage(org.purpurmc.purpur.region.RegionFileFormat format, int linearCompression, boolean linearCrashOnBrokenSymlink, Path directory, boolean dsync) { // Paper - protected constructor // LinearPurpur - // Paper start - add isChunkData param -- this(directory, dsync, false); -+ this(format, linearCompression, linearCrashOnBrokenSymlink, directory, dsync, false); - } -- RegionFileStorage(Path directory, boolean dsync, boolean isChunkData) { -+ RegionFileStorage(org.purpurmc.purpur.region.RegionFileFormat format, int linearCompression, boolean linearCrashOnBrokenSymlink, Path directory, boolean dsync, boolean isChunkData) { // LinearPurpur -+ // LinearPurpur start -+ this.format = format; -+ this.linearCompression = linearCompression; -+ this.linearCrashOnBrokenSymlink = linearCrashOnBrokenSymlink; -+ // LinearPurpur end - this.isChunkData = isChunkData; - // Paper end - add isChunkData param - this.folder = directory; -@@ -70,7 +81,7 @@ public class RegionFileStorage implements AutoCloseable { - @Nullable - public static ChunkPos getRegionFileCoordinates(Path file) { - String fileName = file.getFileName().toString(); -- if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { -+ if (!fileName.startsWith("r.") || !fileName.endsWith(".mca") || !fileName.endsWith(".linear")) { // LinearPurpur - return null; - } - -@@ -90,29 +101,43 @@ public class RegionFileStorage implements AutoCloseable { - } - } - -- public synchronized RegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) { -+ public synchronized org.purpurmc.purpur.region.AbstractRegionFile getRegionFileIfLoaded(ChunkPos chunkcoordintpair) { // LinearPurpur - return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ())); - } - - public synchronized boolean chunkExists(ChunkPos pos) throws IOException { -- RegionFile regionfile = getRegionFile(pos, true); -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = getRegionFile(pos, true); // LinearPurpur - - return regionfile != null ? regionfile.hasChunk(pos) : false; - } - -- public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit -+ // LinearPurpur start -+ private void guardAgainstBrokenSymlinks(Path path) throws IOException { -+ if (!linearCrashOnBrokenSymlink) return; -+ if (!this.format.equals("LINEAR")) return; -+ if (!java.nio.file.Files.isSymbolicLink(path)) return; -+ Path link = java.nio.file.Files.readSymbolicLink(path); -+ if (!java.nio.file.Files.exists(link) || !java.nio.file.Files.isReadable(link)) { -+ LOGGER.error("Linear region file {} is a broken symbolic link, crashing to prevent data loss", path); -+ net.minecraft.server.MinecraftServer.getServer().halt(false); -+ throw new IOException("Linear region file " + path + " is a broken symbolic link, crashing to prevent data loss"); -+ } -+ } -+ // LinearPurpur end -+ -+ public synchronized org.purpurmc.purpur.region.AbstractRegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // LinearPurpur - return this.getRegionFile(chunkcoordintpair, existingOnly, false); - } -- public synchronized RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException { -+ public synchronized org.purpurmc.purpur.region.AbstractRegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly, boolean lock) throws IOException { // LinearPurpur - // Paper end - long i = ChunkPos.asLong(chunkcoordintpair.getRegionX(), chunkcoordintpair.getRegionZ()); final long regionPos = i; // Paper - OBFHELPER -- RegionFile regionfile = (RegionFile) this.regionCache.getAndMoveToFirst(i); -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = this.regionCache.getAndMoveToFirst(i); // LinearPurpur - - if (regionfile != null) { - // Paper start - if (lock) { - // must be in this synchronized block -- regionfile.fileLock.lock(); -+ regionfile.getFileLock().lock(); // LinearPurpur - } - // Paper end - return regionfile; -@@ -123,28 +148,45 @@ public class RegionFileStorage implements AutoCloseable { - } - // Paper end - cache regionfile does not exist state - if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - configurable -- ((RegionFile) this.regionCache.removeLast()).close(); -+ this.regionCache.removeLast().close(); // LinearPurpur - } - - // Paper - only create directory if not existing only - moved down - Path path = this.folder; - int j = chunkcoordintpair.getRegionX(); -- Path path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Paper - diff on change -- if (existingOnly && !java.nio.file.Files.exists(path1)) { // Paper start - cache regionfile does not exist state -- this.markNonExisting(regionPos); -- return null; // CraftBukkit -+ // LinearPurpur start - Polyglot -+ Path path1; -+ if (existingOnly) { -+ Path anvil = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); -+ Path linear = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + ".linear"); -+ guardAgainstBrokenSymlinks(linear); -+ if (java.nio.file.Files.exists(anvil)) path1 = anvil; -+ else if (java.nio.file.Files.exists(linear)) path1 = linear; -+ else { -+ this.markNonExisting(regionPos); -+ return null; -+ } -+ // LinearPurpur end - } else { -+ // LinearPurpur start - Polyglot -+ String extension = switch (this.format) { -+ case LINEAR -> "linear"; -+ default -> "mca"; -+ }; -+ path1 = path.resolve("r." + j + "." + chunkcoordintpair.getRegionZ() + "." + extension); -+ // LinearPurpur end -+ guardAgainstBrokenSymlinks(path1); // LinearPurpur - Crash on broken symlink - this.createRegionFile(regionPos); - } - // Paper end - cache regionfile does not exist state - FileUtil.createDirectoriesSafe(this.folder); // Paper - only create directory if not existing only - moved from above -- RegionFile regionfile1 = new RegionFile(path1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header - -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile1 = org.purpurmc.purpur.region.AbstractRegionFileFactory.getAbstractRegionFile(this.linearCompression, path1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header // LinearPurpur - this.regionCache.putAndMoveToFirst(i, regionfile1); - // Paper start - if (lock) { - // must be in this synchronized block -- regionfile1.fileLock.lock(); -+ regionfile1.getFileLock().lock(); // LinearPurpur - } - // Paper end - return regionfile1; -@@ -172,7 +214,7 @@ public class RegionFileStorage implements AutoCloseable { - } - - -- private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { -+ private static CompoundTag readOversizedChunk(org.purpurmc.purpur.region.AbstractRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // LinearPurpur - synchronized (regionfile) { - try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { - CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z); -@@ -219,14 +261,14 @@ public class RegionFileStorage implements AutoCloseable { - @Nullable - public CompoundTag read(ChunkPos pos) throws IOException { - // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing -- RegionFile regionfile = this.getRegionFile(pos, true, true); // Paper -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = this.getRegionFile(pos, true, true); // Paper // LinearPurpur - if (regionfile == null) { - return null; - } - // Paper start - Add regionfile parameter - return this.read(pos, regionfile); - } -- public CompoundTag read(ChunkPos pos, RegionFile regionfile) throws IOException { -+ public CompoundTag read(ChunkPos pos, org.purpurmc.purpur.region.AbstractRegionFile regionfile) throws IOException { // LinearPurpur - // We add the regionfile parameter to avoid the potential deadlock (on fileLock) if we went back to obtain a regionfile - // if we decide to re-read - // Paper end -@@ -236,7 +278,7 @@ public class RegionFileStorage implements AutoCloseable { - - // Paper start - if (regionfile.isOversized(pos.x, pos.z)) { -- printOversizedLog("Loading Oversized Chunk!", regionfile.regionFile, pos.x, pos.z); -+ printOversizedLog("Loading Oversized Chunk!", regionfile.getRegionFile(), pos.x, pos.z); // LinearPurpur - return readOversizedChunk(regionfile, pos); - } - // Paper end -@@ -250,12 +292,12 @@ public class RegionFileStorage implements AutoCloseable { - if (this.isChunkData) { - ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound); - if (!chunkPos.equals(pos)) { -- net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.regionFile.toAbsolutePath()); -+ net.minecraft.server.MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + chunkPos + " instead! Attempting regionfile recalculation for regionfile " + regionfile.getRegionFile().toAbsolutePath()); // LinearPurpur - if (regionfile.recalculateHeader()) { -- regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. -+ regionfile.getFileLock().lock(); // otherwise we will unlock twice and only lock once. // LinearPurpur - return this.read(pos, regionfile); - } -- net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.regionFile.toAbsolutePath()); -+ net.minecraft.server.MinecraftServer.LOGGER.error("Can't recalculate regionfile header, regenerating chunk " + pos + " for " + regionfile.getRegionFile().toAbsolutePath()); // LinearPurpur - return null; - } - } -@@ -289,13 +331,13 @@ public class RegionFileStorage implements AutoCloseable { - - return nbttagcompound; - } finally { // Paper start -- regionfile.fileLock.unlock(); -+ regionfile.getFileLock().unlock(); // LinearPurpur - } // Paper end - } - - public void scanChunk(ChunkPos chunkPos, StreamTagVisitor scanner) throws IOException { - // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing -- RegionFile regionfile = this.getRegionFile(chunkPos, true); -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = this.getRegionFile(chunkPos, true); // LinearPurpur - if (regionfile == null) { - return; - } -@@ -325,7 +367,7 @@ public class RegionFileStorage implements AutoCloseable { - } - - protected void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { -- RegionFile regionfile = this.getRegionFile(pos, nbt == null, true); // CraftBukkit // Paper // Paper start - rewrite chunk system -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = this.getRegionFile(pos, nbt == null, true); // CraftBukkit // Paper // Paper start - rewrite chunk system // LinearPurpur - if (nbt == null && regionfile == null) { - return; - } -@@ -375,7 +417,7 @@ public class RegionFileStorage implements AutoCloseable { - } - // Paper end - } finally { // Paper start -- regionfile.fileLock.unlock(); -+ regionfile.getFileLock().unlock(); // LinearPurpur - } // Paper end - } - -@@ -384,7 +426,7 @@ public class RegionFileStorage implements AutoCloseable { - ObjectIterator objectiterator = this.regionCache.values().iterator(); - - while (objectiterator.hasNext()) { -- RegionFile regionfile = (RegionFile) objectiterator.next(); -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = (org.purpurmc.purpur.region.AbstractRegionFile) objectiterator.next(); // LinearPurpur - - try { - regionfile.close(); -@@ -400,7 +442,7 @@ public class RegionFileStorage implements AutoCloseable { - ObjectIterator objectiterator = this.regionCache.values().iterator(); - - while (objectiterator.hasNext()) { -- RegionFile regionfile = (RegionFile) objectiterator.next(); -+ org.purpurmc.purpur.region.AbstractRegionFile regionfile = (org.purpurmc.purpur.region.AbstractRegionFile) objectiterator.next(); // LinearPurpur - - regionfile.flush(); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -index 4aac1979cf57300825a999c876fcf24d3170e68e..79a389b9a139f6838adf32d3b5d4d7ecdd87bcc3 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/SectionStorage.java -@@ -47,8 +47,8 @@ public class SectionStorage extends RegionFileStorage implements AutoCloseabl - public final RegistryAccess registryAccess; // Paper - rewrite chunk system - protected final LevelHeightAccessor levelHeightAccessor; - -- public SectionStorage(Path path, Function> codecFactory, Function factory, DataFixer dataFixer, DataFixTypes dataFixTypes, boolean dsync, RegistryAccess dynamicRegistryManager, LevelHeightAccessor world) { -- super(path, dsync); // Paper - remove mojang I/O thread -+ public SectionStorage(org.purpurmc.purpur.region.RegionFileFormat format, int linearCompression, boolean linearCrashOnBrokenSymlink, Path path, Function> codecFactory, Function factory, DataFixer dataFixer, DataFixTypes dataFixTypes, boolean dsync, RegistryAccess dynamicRegistryManager, LevelHeightAccessor world) { // LinearPurpur -+ super(format, linearCompression, linearCrashOnBrokenSymlink, path, dsync); // Paper - remove mojang I/O thread // LinearPurpur - this.codec = codecFactory; - this.factory = factory; - this.fixerUpper = dataFixer; -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index f6c6cd92e1eff044abefa6ca74477d361f4434ec..ffc77e81999ab80906ff299eef04c5c5abd2ec1b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -567,7 +567,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - return true; - } - -- net.minecraft.world.level.chunk.storage.RegionFile file; -+ org.purpurmc.purpur.region.AbstractRegionFile file; // LinearPurpur - try { - file = world.getChunkSource().chunkMap.regionFileCache.getRegionFile(chunkPos, false); - } catch (java.io.IOException ex) { -diff --git a/src/main/java/org/purpurmc/purpur/region/AbstractRegionFile.java b/src/main/java/org/purpurmc/purpur/region/AbstractRegionFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e6ab1ca9a699c3f57855f2acc8d08b8a005bce2f ---- /dev/null -+++ b/src/main/java/org/purpurmc/purpur/region/AbstractRegionFile.java -@@ -0,0 +1,31 @@ -+package org.purpurmc.purpur.region; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkStatus; -+ -+import java.io.DataInputStream; -+import java.io.DataOutputStream; -+import java.io.IOException; -+import java.nio.file.Path; -+import java.util.concurrent.locks.ReentrantLock; -+ -+public interface AbstractRegionFile { -+ void flush() throws IOException; -+ void clear(ChunkPos pos) throws IOException; -+ void close() throws IOException; -+ void setStatus(int x, int z, ChunkStatus status); -+ void setOversized(int x, int z, boolean b) throws IOException; -+ -+ boolean hasChunk(ChunkPos pos); -+ boolean doesChunkExist(ChunkPos pos) throws Exception; -+ boolean isOversized(int x, int z); -+ boolean recalculateHeader() throws IOException; -+ -+ DataOutputStream getChunkDataOutputStream(ChunkPos pos) throws IOException; -+ DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException; -+ CompoundTag getOversizedData(int x, int z) throws IOException; -+ ChunkStatus getStatusIfCached(int x, int z); -+ ReentrantLock getFileLock(); -+ Path getRegionFile(); -+} -diff --git a/src/main/java/org/purpurmc/purpur/region/AbstractRegionFileFactory.java b/src/main/java/org/purpurmc/purpur/region/AbstractRegionFileFactory.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c88ff6fda185a8489cbefa51a7b09ccba4f11461 ---- /dev/null -+++ b/src/main/java/org/purpurmc/purpur/region/AbstractRegionFileFactory.java -@@ -0,0 +1,29 @@ -+package org.purpurmc.purpur.region; -+ -+import net.minecraft.world.level.chunk.storage.RegionFile; -+import net.minecraft.world.level.chunk.storage.RegionFileVersion; -+ -+import java.io.IOException; -+import java.nio.file.Path; -+ -+public class AbstractRegionFileFactory { -+ public static AbstractRegionFile getAbstractRegionFile(int linearCompression, Path file, Path directory, boolean dsync) throws IOException { -+ return getAbstractRegionFile(linearCompression, file, directory, RegionFileVersion.VERSION_DEFLATE, dsync); -+ } -+ -+ public static AbstractRegionFile getAbstractRegionFile(int linearCompression, Path file, Path directory, boolean dsync, boolean canRecalcHeader) throws IOException { -+ return getAbstractRegionFile(linearCompression, file, directory, RegionFileVersion.VERSION_DEFLATE, dsync, canRecalcHeader); -+ } -+ -+ public static AbstractRegionFile getAbstractRegionFile(int linearCompression, Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException { -+ return getAbstractRegionFile(linearCompression, file, directory, outputChunkStreamVersion, dsync, false); -+ } -+ -+ public static AbstractRegionFile getAbstractRegionFile(int linearCompression, Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync, boolean canRecalcHeader) throws IOException { -+ if (file.toString().endsWith(".linear")) { -+ return new LinearRegionFile(file, linearCompression); -+ } else { -+ return new RegionFile(file, directory, outputChunkStreamVersion, dsync, canRecalcHeader); -+ } -+ } -+} -diff --git a/src/main/java/org/purpurmc/purpur/region/LinearRegionFile.java b/src/main/java/org/purpurmc/purpur/region/LinearRegionFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..731a90436cae2e615c228c07f042fa112b95a8d2 ---- /dev/null -+++ b/src/main/java/org/purpurmc/purpur/region/LinearRegionFile.java -@@ -0,0 +1,316 @@ -+package org.purpurmc.purpur.region; -+ -+import com.github.luben.zstd.ZstdInputStream; -+import com.github.luben.zstd.ZstdOutputStream; -+import com.mojang.logging.LogUtils; -+import net.jpountz.lz4.LZ4Compressor; -+import net.jpountz.lz4.LZ4Factory; -+import net.jpountz.lz4.LZ4FastDecompressor; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.chunk.ChunkStatus; -+import org.slf4j.Logger; -+ -+import javax.annotation.Nullable; -+import java.io.*; -+import java.nio.ByteBuffer; -+import java.nio.file.Files; -+import java.nio.file.Path; -+import java.nio.file.StandardCopyOption; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.List; -+import java.util.concurrent.atomic.AtomicBoolean; -+import java.util.concurrent.locks.ReentrantLock; -+ -+public class LinearRegionFile implements AbstractRegionFile, AutoCloseable { -+ private static final long SUPERBLOCK = -4323716122432332390L; -+ private static final byte VERSION = 2; -+ private static final int HEADER_SIZE = 32; -+ private static final int FOOTER_SIZE = 8; -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ private static final List SUPPORTED_VERSIONS = Arrays.asList((byte) 1, (byte) 2); -+ private static final LinearRegionFileFlusher linearRegionFileFlusher = new LinearRegionFileFlusher(); -+ -+ private final byte[][] buffer = new byte[1024][]; -+ private final int[] bufferUncompressedSize = new int[1024]; -+ -+ private final int[] chunkTimestamps = new int[1024]; -+ private final ChunkStatus[] statuses = new ChunkStatus[1024]; -+ -+ private final LZ4Compressor compressor; -+ private final LZ4FastDecompressor decompressor; -+ -+ public final ReentrantLock fileLock = new ReentrantLock(true); -+ private final int compressionLevel; -+ -+ private AtomicBoolean markedToSave = new AtomicBoolean(false); -+ public boolean closed = false; -+ public Path path; -+ -+ -+ public LinearRegionFile(Path file, int compression) throws IOException { -+ this.path = file; -+ this.compressionLevel = compression; -+ this.compressor = LZ4Factory.fastestInstance().fastCompressor(); -+ this.decompressor = LZ4Factory.fastestInstance().fastDecompressor(); -+ -+ File regionFile = new File(this.path.toString()); -+ -+ Arrays.fill(this.bufferUncompressedSize, 0); -+ -+ if (!regionFile.canRead()) return; -+ -+ try (FileInputStream fileStream = new FileInputStream(regionFile); -+ DataInputStream rawDataStream = new DataInputStream(fileStream)) { -+ -+ long superBlock = rawDataStream.readLong(); -+ if (superBlock != SUPERBLOCK) -+ throw new RuntimeException("Invalid superblock: " + superBlock + " in " + file); -+ -+ byte version = rawDataStream.readByte(); -+ if (!SUPPORTED_VERSIONS.contains(version)) -+ throw new RuntimeException("Invalid version: " + version + " in " + file); -+ -+ // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused. -+ rawDataStream.skipBytes(11); -+ -+ int dataCount = rawDataStream.readInt(); -+ long fileLength = file.toFile().length(); -+ if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE) -+ throw new IOException("Invalid file length: " + this.path + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE)); -+ -+ rawDataStream.skipBytes(8); // Skip data hash (Long): Unused. -+ -+ byte[] rawCompressed = new byte[dataCount]; -+ rawDataStream.readFully(rawCompressed, 0, dataCount); -+ -+ superBlock = rawDataStream.readLong(); -+ if (superBlock != SUPERBLOCK) -+ throw new IOException("Footer superblock invalid " + this.path); -+ -+ try (DataInputStream dataStream = new DataInputStream(new ZstdInputStream(new ByteArrayInputStream(rawCompressed)))) { -+ -+ int[] starts = new int[1024]; -+ for (int i = 0; i < 1024; i++) { -+ starts[i] = dataStream.readInt(); -+ dataStream.skipBytes(4); // Skip timestamps (Int): Unused. -+ } -+ -+ for (int i = 0; i < 1024; i++) { -+ if (starts[i] > 0) { -+ int size = starts[i]; -+ byte[] b = new byte[size]; -+ dataStream.readFully(b, 0, size); -+ -+ int maxCompressedLength = this.compressor.maxCompressedLength(size); -+ byte[] compressed = new byte[maxCompressedLength]; -+ int compressedLength = this.compressor.compress(b, 0, size, compressed, 0, maxCompressedLength); -+ b = new byte[compressedLength]; -+ System.arraycopy(compressed, 0, b, 0, compressedLength); -+ -+ this.buffer[i] = b; -+ this.bufferUncompressedSize[i] = size; -+ } -+ } -+ } -+ } -+ } -+ -+ public Path getRegionFile() { -+ return this.path; -+ } -+ -+ public ReentrantLock getFileLock() { -+ return this.fileLock; -+ } -+ -+ public void flush() throws IOException { -+ if (isMarkedToSave()) flushWrapper(); // sync -+ } -+ -+ private void markToSave() { -+ linearRegionFileFlusher.scheduleSave(this); -+ markedToSave.set(true); -+ } -+ -+ public boolean isMarkedToSave() { -+ return markedToSave.getAndSet(false); -+ } -+ -+ public void flushWrapper() { -+ try { -+ save(); -+ } catch (IOException e) { -+ LOGGER.error("Failed to flush region file " + path.toAbsolutePath(), e); -+ } -+ } -+ -+ public boolean doesChunkExist(ChunkPos pos) throws Exception { -+ throw new Exception("doesChunkExist is a stub"); -+ } -+ -+ private synchronized void save() throws IOException { -+ long timestamp = getTimestamp(); -+ short chunkCount = 0; -+ -+ File tempFile = new File(path.toString() + ".tmp"); -+ -+ try (FileOutputStream fileStream = new FileOutputStream(tempFile); -+ ByteArrayOutputStream zstdByteArray = new ByteArrayOutputStream(); -+ ZstdOutputStream zstdStream = new ZstdOutputStream(zstdByteArray, this.compressionLevel); -+ DataOutputStream zstdDataStream = new DataOutputStream(zstdStream); -+ DataOutputStream dataStream = new DataOutputStream(fileStream)) { -+ -+ dataStream.writeLong(SUPERBLOCK); -+ dataStream.writeByte(VERSION); -+ dataStream.writeLong(timestamp); -+ dataStream.writeByte(this.compressionLevel); -+ -+ ArrayList byteBuffers = new ArrayList<>(); -+ for (int i = 0; i < 1024; i++) { -+ if (this.bufferUncompressedSize[i] != 0) { -+ chunkCount += 1; -+ byte[] content = new byte[bufferUncompressedSize[i]]; -+ this.decompressor.decompress(buffer[i], 0, content, 0, bufferUncompressedSize[i]); -+ -+ byteBuffers.add(content); -+ } else byteBuffers.add(null); -+ } -+ for (int i = 0; i < 1024; i++) { -+ zstdDataStream.writeInt(this.bufferUncompressedSize[i]); // Write uncompressed size -+ zstdDataStream.writeInt(this.chunkTimestamps[i]); // Write timestamp -+ } -+ for (int i = 0; i < 1024; i++) { -+ if (byteBuffers.get(i) != null) -+ zstdDataStream.write(byteBuffers.get(i), 0, byteBuffers.get(i).length); -+ } -+ zstdDataStream.close(); -+ -+ dataStream.writeShort(chunkCount); -+ -+ byte[] compressed = zstdByteArray.toByteArray(); -+ -+ dataStream.writeInt(compressed.length); -+ dataStream.writeLong(0); -+ -+ dataStream.write(compressed, 0, compressed.length); -+ dataStream.writeLong(SUPERBLOCK); -+ -+ dataStream.flush(); -+ fileStream.getFD().sync(); -+ fileStream.getChannel().force(true); // Ensure atomicity on Btrfs -+ } -+ Files.move(tempFile.toPath(), this.path, StandardCopyOption.REPLACE_EXISTING); -+ } -+ -+ -+ public void setStatus(int x, int z, ChunkStatus status) { -+ this.statuses[getChunkIndex(x, z)] = status; -+ } -+ -+ public synchronized void write(ChunkPos pos, ByteBuffer buffer) { -+ try { -+ byte[] b = toByteArray(new ByteArrayInputStream(buffer.array())); -+ int uncompressedSize = b.length; -+ -+ int maxCompressedLength = this.compressor.maxCompressedLength(b.length); -+ byte[] compressed = new byte[maxCompressedLength]; -+ int compressedLength = this.compressor.compress(b, 0, b.length, compressed, 0, maxCompressedLength); -+ b = new byte[compressedLength]; -+ System.arraycopy(compressed, 0, b, 0, compressedLength); -+ -+ int index = getChunkIndex(pos.x, pos.z); -+ this.buffer[index] = b; -+ this.chunkTimestamps[index] = getTimestamp(); -+ this.bufferUncompressedSize[getChunkIndex(pos.x, pos.z)] = uncompressedSize; -+ } catch (IOException e) { -+ LOGGER.error("Chunk write IOException " + e + " " + this.path); -+ } -+ markToSave(); -+ } -+ -+ public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { -+ return new DataOutputStream(new BufferedOutputStream(new ChunkBuffer(pos))); -+ } -+ -+ private class ChunkBuffer extends ByteArrayOutputStream { -+ private final ChunkPos pos; -+ -+ public ChunkBuffer(ChunkPos chunkcoordintpair) { -+ super(); -+ this.pos = chunkcoordintpair; -+ } -+ -+ public void close() throws IOException { -+ ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); -+ LinearRegionFile.this.write(this.pos, bytebuffer); -+ } -+ } -+ -+ private byte[] toByteArray(InputStream in) throws IOException { -+ ByteArrayOutputStream out = new ByteArrayOutputStream(); -+ byte[] tempBuffer = new byte[4096]; -+ -+ int length; -+ while ((length = in.read(tempBuffer)) >= 0) { -+ out.write(tempBuffer, 0, length); -+ } -+ -+ return out.toByteArray(); -+ } -+ -+ @Nullable -+ public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) { -+ if(this.bufferUncompressedSize[getChunkIndex(pos.x, pos.z)] != 0) { -+ byte[] content = new byte[bufferUncompressedSize[getChunkIndex(pos.x, pos.z)]]; -+ this.decompressor.decompress(this.buffer[getChunkIndex(pos.x, pos.z)], 0, content, 0, bufferUncompressedSize[getChunkIndex(pos.x, pos.z)]); -+ return new DataInputStream(new ByteArrayInputStream(content)); -+ } -+ return null; -+ } -+ -+ public ChunkStatus getStatusIfCached(int x, int z) { -+ return this.statuses[getChunkIndex(x, z)]; -+ } -+ -+ public void clear(ChunkPos pos) { -+ int i = getChunkIndex(pos.x, pos.z); -+ this.buffer[i] = null; -+ this.bufferUncompressedSize[i] = 0; -+ this.chunkTimestamps[i] = getTimestamp(); -+ markToSave(); -+ } -+ -+ public boolean hasChunk(ChunkPos pos) { -+ return this.bufferUncompressedSize[getChunkIndex(pos.x, pos.z)] > 0; -+ } -+ -+ public void close() throws IOException { -+ if (closed) return; -+ closed = true; -+ flush(); // sync -+ } -+ -+ private static int getChunkIndex(int x, int z) { -+ return (x & 31) + ((z & 31) << 5); -+ } -+ -+ private static int getTimestamp() { -+ return (int) (System.currentTimeMillis() / 1000L); -+ } -+ -+ public boolean recalculateHeader() { -+ return false; -+ } -+ -+ public void setOversized(int x, int z, boolean something) {} -+ -+ public CompoundTag getOversizedData(int x, int z) throws IOException { -+ throw new IOException("getOversizedData is a stub " + this.path); -+ } -+ -+ public boolean isOversized(int x, int z) { -+ return false; -+ } -+} -diff --git a/src/main/java/org/purpurmc/purpur/region/LinearRegionFileFlusher.java b/src/main/java/org/purpurmc/purpur/region/LinearRegionFileFlusher.java -new file mode 100644 -index 0000000000000000000000000000000000000000..60f8ed586676609b9be7626eebf865aaaee92ac2 ---- /dev/null -+++ b/src/main/java/org/purpurmc/purpur/region/LinearRegionFileFlusher.java -@@ -0,0 +1,45 @@ -+package org.purpurmc.purpur.region; -+ -+import com.google.common.util.concurrent.ThreadFactoryBuilder; -+import java.util.Queue; -+import java.util.concurrent.*; -+import org.purpurmc.purpur.PurpurConfig; -+import org.bukkit.Bukkit; -+ -+public class LinearRegionFileFlusher { -+ private final Queue savingQueue = new LinkedBlockingQueue<>(); -+ private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( -+ new ThreadFactoryBuilder() -+ .setNameFormat("linear-flush-scheduler") -+ .build() -+ ); -+ private final ExecutorService executor = Executors.newFixedThreadPool( -+ PurpurConfig.linearFlushThreads, -+ new ThreadFactoryBuilder() -+ .setNameFormat("linear-flusher-%d") -+ .build() -+ ); -+ -+ public LinearRegionFileFlusher() { -+ Bukkit.getLogger().info("Using " + PurpurConfig.linearFlushThreads + " threads for linear region flushing."); -+ scheduler.scheduleAtFixedRate(this::pollAndFlush, 0L, PurpurConfig.linearFlushFrequency, TimeUnit.SECONDS); -+ } -+ -+ public void scheduleSave(LinearRegionFile regionFile) { -+ if (savingQueue.contains(regionFile)) return; -+ savingQueue.add(regionFile); -+ } -+ -+ private void pollAndFlush() { -+ while (!savingQueue.isEmpty()) { -+ LinearRegionFile regionFile = savingQueue.poll(); -+ if (!regionFile.closed && regionFile.isMarkedToSave()) -+ executor.execute(regionFile::flushWrapper); -+ } -+ } -+ -+ public void shutdown() { -+ executor.shutdown(); -+ scheduler.shutdown(); -+ } -+}