From 4a8812bbe8fab30b3acd99367e975b2c411b80b9 Mon Sep 17 00:00:00 2001 From: lilingfengdev Date: Sun, 14 Jan 2024 10:45:51 +0800 Subject: [PATCH 01/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8C=BA=E5=9F=9F?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../0045-Region-format-configuration.patch | 114 ++ .../0046-Add-Linear-region-format.patch | 1070 +++++++++++++++++ 2 files changed, 1184 insertions(+) create mode 100644 patches/server/0045-Region-format-configuration.patch create mode 100644 patches/server/0046-Add-Linear-region-format.patch diff --git a/patches/server/0045-Region-format-configuration.patch b/patches/server/0045-Region-format-configuration.patch new file mode 100644 index 00000000..2d2c6c9f --- /dev/null +++ b/patches/server/0045-Region-format-configuration.patch @@ -0,0 +1,114 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Triassic +Date: Fri, 22 Sep 2023 23:27:14 +0300 +Subject: [PATCH] Region format configuration + + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index f69976dcba060027c67c2e1b49fa28d3f28f66f0..0bf2f897b8bbb44e223c5c4f3d22130a01cc545c 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -887,7 +887,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 23 || regionFormatLinearCompressionLevel < 1) { ++ log(Level.SEVERE, "Linear region compression level should be between 1 and 22 in purpur.yml: " + regionFormatLinearCompressionLevel); ++ log(Level.SEVERE, "Falling back to compression level 1."); ++ regionFormatLinearCompressionLevel = 1; ++ } ++ linearCrashOnBrokenSymlink = getBoolean("region-format.linear.crash-on-broken-symlink", linearCrashOnBrokenSymlink); ++ } ++ // LinearPurpur end ++ + public boolean useBetterMending = false; + public double mendingMultiplier = 1.0; + public boolean alwaysTameInCreative = false; +diff --git a/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java b/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java +new file mode 100644 +index 0000000000000000000000000000000000000000..593d684da10368e8cb37628445b36a826719e79e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java +@@ -0,0 +1,16 @@ ++package org.purpurmc.purpur.region; ++ ++public enum RegionFileFormat { ++ ANVIL, ++ LINEAR, ++ INVALID; ++ ++ public static RegionFileFormat fromString(String format) { ++ for (RegionFileFormat rff : values()) { ++ if (rff.name().equalsIgnoreCase(format)) { ++ return rff; ++ } ++ } ++ return RegionFileFormat.INVALID; ++ } ++} diff --git a/patches/server/0046-Add-Linear-region-format.patch b/patches/server/0046-Add-Linear-region-format.patch new file mode 100644 index 00000000..c1859e1c --- /dev/null +++ b/patches/server/0046-Add-Linear-region-format.patch @@ -0,0 +1,1070 @@ +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(); ++ } ++} From 3ad29d08a5f5523fdd33dfd0f6a796b503d81794 Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Sun, 14 Jan 2024 11:07:30 +0800 Subject: [PATCH 02/16] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bd3acd36..48695bef 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ - **More customized** relying on features of [Purpur](https://github.com/PurpurMC/Purpur). - **Maintenance friendly**, integrating with [Sentry](https://sentry.io/welcome/) of [Pufferfish](https://github.com/pufferfish-gg/Pufferfish) to easy track all errors coming from your server in excruciating detail. - **Various optimization** blending from [other forks](https://github.com/Winds-Studio/Leaf#credits). + - **Better Region Format** Support for the Linear region file format from [LinearPurpur](https://github.com/StupidCraft/LinearPurpur) - ... ## Contact From 523bf20d36fa7128fd6e19d995998c40fc2223c7 Mon Sep 17 00:00:00 2001 From: lilingfengdev Date: Sun, 14 Jan 2024 11:16:42 +0800 Subject: [PATCH 03/16] Rename --- ...nfiguration.patch => 0010-1-Region-format-configuration.patch} | 0 ...-region-format.patch => 0010-2-Add-Linear-region-format.patch} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename patches/server/{0045-Region-format-configuration.patch => 0010-1-Region-format-configuration.patch} (100%) rename patches/server/{0046-Add-Linear-region-format.patch => 0010-2-Add-Linear-region-format.patch} (100%) diff --git a/patches/server/0045-Region-format-configuration.patch b/patches/server/0010-1-Region-format-configuration.patch similarity index 100% rename from patches/server/0045-Region-format-configuration.patch rename to patches/server/0010-1-Region-format-configuration.patch diff --git a/patches/server/0046-Add-Linear-region-format.patch b/patches/server/0010-2-Add-Linear-region-format.patch similarity index 100% rename from patches/server/0046-Add-Linear-region-format.patch rename to patches/server/0010-2-Add-Linear-region-format.patch From 719659e5b39f75254788baec4c69e2cfd4d2b4d9 Mon Sep 17 00:00:00 2001 From: DGun Otto Date: Mon, 15 Jan 2024 19:27:50 +0800 Subject: [PATCH 04/16] Linear Region --- patches/server/0045-AddLinearSupport.patch | 1176 ++++++++++++++++++++ 1 file changed, 1176 insertions(+) create mode 100644 patches/server/0045-AddLinearSupport.patch diff --git a/patches/server/0045-AddLinearSupport.patch b/patches/server/0045-AddLinearSupport.patch new file mode 100644 index 00000000..897ebb26 --- /dev/null +++ b/patches/server/0045-AddLinearSupport.patch @@ -0,0 +1,1176 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Your Name +Date: Sun, 14 Jan 2024 20:57:02 +0800 +Subject: [PATCH] AddLinearSupport + + +diff --git a/build.gradle.kts b/build.gradle.kts +index a6ac60d7d57e4b0662b0c30f7bc43ba84179d697..a4ed90ae8b704bac060409c0218543fcc9a3e15c 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -22,6 +22,8 @@ dependencies { + } + // Gale end - project setup + // Paper start ++ implementation("com.github.luben:zstd-jni:1.5.5-11") ++ implementation("org.lz4:lz4-java:1.8.0") + implementation("org.jline:jline-terminal-jansi:3.25.0") // Leaf - Bump Dependencies + implementation("com.github.Dreeam-qwq:TerminalConsoleAppender:360a0759") // Leaf - Use own TerminalConsoleAppender fork to fix some issues under latest version of jline/log4j + implementation("net.kyori:adventure-text-serializer-ansi:4.15.0") // Keep in sync with adventureVersion from Paper-API build file // Leaf - Bump Dependencies +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 c13df3a375f416273c6a26f5f77624c1f34a918c..e2c780e7d914e2cfd322fe07951aa54ab7ac8be4 100644 +--- a/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java ++++ b/src/main/java/io/papermc/paper/world/ThreadedWorldUpgrader.java +@@ -85,8 +85,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); + // Gale start - instantly continue on world upgrade finish +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index fb3bbe36c7bf67be65f0978ead2d4c778c32ba82..76022824dc9de7e64009383378f8ba75f70b09f3 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -870,7 +870,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 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(); +@@ -291,7 +291,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); +@@ -875,13 +875,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; +@@ -899,7 +899,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 22c9353264da69591a748773e5dd0ea49229f221..d23a9bd8e3b1588a9a162c95dc12aa5e68c44563 100644 +--- a/src/main/java/net/minecraft/server/level/ServerLevel.java ++++ b/src/main/java/net/minecraft/server/level/ServerLevel.java +@@ -425,8 +425,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 { +@@ -749,7 +749,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 5b91ccafbdc7582e50272953f3ab2b8c0f0bf9dd..5d554c64a7c278c12724c1ffbb45eeaefd6dceeb 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 db571f658f636cdda1dcdbaffa0c4da67fae11ad..c71446e1ae8d55a952e19ff8013a481dea1af03e 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 dad55726e798448b4e0e599bd5f8913c55059812..205e63477796359ce35b70b701e0438d8a8e45d5 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/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +index b12437a7e292fbf9aefc7a41b5bc1695733a11c5..c3f095236093c436d37c4730f5537d4fbc0ea6c0 100644 +--- a/src/main/java/org/purpurmc/purpur/PurpurConfig.java ++++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +@@ -241,6 +241,19 @@ public class PurpurConfig { + laggingThreshold = getDouble("settings.lagging-threshold", laggingThreshold); + } + ++ // LinearPurpur start - region format configuration ++ public static int linearFlushFrequency = 10; ++ public static int linearFlushThreads = 1; ++ private static void regionFormatSettings() { ++ linearFlushFrequency = getInt("region-format.linear.flush-frequency", linearFlushFrequency); ++ linearFlushThreads = getInt("region-format.linear.flush-max-threads", linearFlushThreads); ++ if (linearFlushThreads < 0) ++ linearFlushThreads = Math.max(Runtime.getRuntime().availableProcessors() + linearFlushThreads, 1); ++ else ++ linearFlushThreads = Math.max(linearFlushThreads, 1); ++ } ++ // LinearPurpur end ++ + public static boolean disableGiveCommandDrops = false; + private static void disableGiveCommandDrops() { + disableGiveCommandDrops = getBoolean("settings.disable-give-dropping", disableGiveCommandDrops); +diff --git a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +index 609446782958d03ae0d5bef99c1d8e5320dfa037..25e2b357633052e5296baf44beafb10aa746991a 100644 +--- a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java ++++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +@@ -27,6 +27,7 @@ import java.util.Map; + import java.util.function.Predicate; + import java.util.logging.Level; + import static org.purpurmc.purpur.PurpurConfig.log; ++import org.purpurmc.purpur.region.RegionFileFormat; + + @SuppressWarnings("unused") + public class PurpurWorldConfig { +@@ -118,6 +119,27 @@ public class PurpurWorldConfig { + arrowMovementResetsDespawnCounter = getBoolean("gameplay-mechanics.arrow.movement-resets-despawn-counter", arrowMovementResetsDespawnCounter); + } + ++ // LinearPurpur start - region format configuration ++ public RegionFileFormat regionFormatName = RegionFileFormat.ANVIL; ++ public boolean linearCrashOnBrokenSymlink = true; ++ public int regionFormatLinearCompressionLevel = 1; ++ private void regionFormatSettings() { ++ regionFormatName = RegionFileFormat.fromString(getString("region-format.format", regionFormatName.name())); ++ if (regionFormatName.equals(RegionFileFormat.INVALID)) { ++ log(Level.SEVERE, "Unknown region format in purpur.yml: " + regionFormatName); ++ log(Level.SEVERE, "Falling back to ANVIL region file format."); ++ regionFormatName = RegionFileFormat.ANVIL; ++ } ++ regionFormatLinearCompressionLevel = getInt("region-format.linear.compression-level", regionFormatLinearCompressionLevel); ++ if (regionFormatLinearCompressionLevel > 23 || regionFormatLinearCompressionLevel < 1) { ++ log(Level.SEVERE, "Linear region compression level should be between 1 and 22 in purpur.yml: " + regionFormatLinearCompressionLevel); ++ log(Level.SEVERE, "Falling back to compression level 1."); ++ regionFormatLinearCompressionLevel = 1; ++ } ++ linearCrashOnBrokenSymlink = getBoolean("region-format.linear.crash-on-broken-symlink", linearCrashOnBrokenSymlink); ++ } ++ // LinearPurpur end ++ + public boolean useBetterMending = false; + public double mendingMultiplier = 1.0; + public boolean alwaysTameInCreative = false; +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(); ++ } ++} +diff --git a/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java b/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java +new file mode 100644 +index 0000000000000000000000000000000000000000..593d684da10368e8cb37628445b36a826719e79e +--- /dev/null ++++ b/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java +@@ -0,0 +1,16 @@ ++package org.purpurmc.purpur.region; ++ ++public enum RegionFileFormat { ++ ANVIL, ++ LINEAR, ++ INVALID; ++ ++ public static RegionFileFormat fromString(String format) { ++ for (RegionFileFormat rff : values()) { ++ if (rff.name().equalsIgnoreCase(format)) { ++ return rff; ++ } ++ } ++ return RegionFileFormat.INVALID; ++ } ++} From e7bbd0ba3f78d484a6a4dc32176a4af9cebae73b Mon Sep 17 00:00:00 2001 From: DGun Otto Date: Mon, 15 Jan 2024 19:32:34 +0800 Subject: [PATCH 05/16] Update 0010-1-Region-format-configuration.patch --- .../0010-1-Region-format-configuration.patch | 113 ------------------ 1 file changed, 113 deletions(-) diff --git a/patches/server/0010-1-Region-format-configuration.patch b/patches/server/0010-1-Region-format-configuration.patch index 2d2c6c9f..8b137891 100644 --- a/patches/server/0010-1-Region-format-configuration.patch +++ b/patches/server/0010-1-Region-format-configuration.patch @@ -1,114 +1 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Triassic -Date: Fri, 22 Sep 2023 23:27:14 +0300 -Subject: [PATCH] Region format configuration - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index f69976dcba060027c67c2e1b49fa28d3f28f66f0..0bf2f897b8bbb44e223c5c4f3d22130a01cc545c 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -887,7 +887,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 23 || regionFormatLinearCompressionLevel < 1) { -+ log(Level.SEVERE, "Linear region compression level should be between 1 and 22 in purpur.yml: " + regionFormatLinearCompressionLevel); -+ log(Level.SEVERE, "Falling back to compression level 1."); -+ regionFormatLinearCompressionLevel = 1; -+ } -+ linearCrashOnBrokenSymlink = getBoolean("region-format.linear.crash-on-broken-symlink", linearCrashOnBrokenSymlink); -+ } -+ // LinearPurpur end -+ - public boolean useBetterMending = false; - public double mendingMultiplier = 1.0; - public boolean alwaysTameInCreative = false; -diff --git a/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java b/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java -new file mode 100644 -index 0000000000000000000000000000000000000000..593d684da10368e8cb37628445b36a826719e79e ---- /dev/null -+++ b/src/main/java/org/purpurmc/purpur/region/RegionFileFormat.java -@@ -0,0 +1,16 @@ -+package org.purpurmc.purpur.region; -+ -+public enum RegionFileFormat { -+ ANVIL, -+ LINEAR, -+ INVALID; -+ -+ public static RegionFileFormat fromString(String format) { -+ for (RegionFileFormat rff : values()) { -+ if (rff.name().equalsIgnoreCase(format)) { -+ return rff; -+ } -+ } -+ return RegionFileFormat.INVALID; -+ } -+} From f650b18eb52ad007b09a67af12a0a16318a89484 Mon Sep 17 00:00:00 2001 From: DGun Otto Date: Mon, 15 Jan 2024 19:32:57 +0800 Subject: [PATCH 06/16] 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(); -+ } -+} From fe286fb4e420bcc914462b784b6fdc72236e91de Mon Sep 17 00:00:00 2001 From: HaHaWTH Date: Mon, 15 Jan 2024 19:41:04 +0800 Subject: [PATCH 07/16] remove --- patches/server/0010-1-Region-format-configuration.patch | 1 - patches/server/0010-2-Add-Linear-region-format.patch | 1 - 2 files changed, 2 deletions(-) delete mode 100644 patches/server/0010-1-Region-format-configuration.patch delete mode 100644 patches/server/0010-2-Add-Linear-region-format.patch diff --git a/patches/server/0010-1-Region-format-configuration.patch b/patches/server/0010-1-Region-format-configuration.patch deleted file mode 100644 index 8b137891..00000000 --- a/patches/server/0010-1-Region-format-configuration.patch +++ /dev/null @@ -1 +0,0 @@ - diff --git a/patches/server/0010-2-Add-Linear-region-format.patch b/patches/server/0010-2-Add-Linear-region-format.patch deleted file mode 100644 index 8b137891..00000000 --- a/patches/server/0010-2-Add-Linear-region-format.patch +++ /dev/null @@ -1 +0,0 @@ - From fe49584f43cac284e9b4142f9048b1049ba0b220 Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:48:27 +0800 Subject: [PATCH 08/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- patches/server/0045-AddLinearSupport.patch | 18 ++--- ...-PandaSpigot-Configurable-knockback.patch} | 0 ...-of-futures-for-chunk-structure-gen.patch} | 0 ...aves-Disable-Moved-Wrongly-Threshold.patch | 70 +++++++++++++++++++ 4 files changed, 79 insertions(+), 9 deletions(-) rename patches/server/{0045-PandaSpigot-Configurable-knockback.patch => 0046-PandaSpigot-Configurable-knockback.patch} (100%) rename patches/server/{0046-Faster-sequencing-of-futures-for-chunk-structure-gen.patch => 0047-Faster-sequencing-of-futures-for-chunk-structure-gen.patch} (100%) create mode 100644 patches/server/0048-Leaves-Disable-Moved-Wrongly-Threshold.patch diff --git a/patches/server/0045-AddLinearSupport.patch b/patches/server/0045-AddLinearSupport.patch index 897ebb26..54586b67 100644 --- a/patches/server/0045-AddLinearSupport.patch +++ b/patches/server/0045-AddLinearSupport.patch @@ -5,7 +5,7 @@ Subject: [PATCH] AddLinearSupport diff --git a/build.gradle.kts b/build.gradle.kts -index a6ac60d7d57e4b0662b0c30f7bc43ba84179d697..a4ed90ae8b704bac060409c0218543fcc9a3e15c 100644 +index 0baaa2bcd05ea65c5fbb9ae646b3a9eeb12bac14..2f374fd4cb05d2c597d0dbd1a554cd6624f84863 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { @@ -121,10 +121,10 @@ index c13df3a375f416273c6a26f5f77624c1f34a918c..e2c780e7d914e2cfd322fe07951aa54a long expectedChunks = (long)regionFiles.length * (32L * 32L); // Gale start - instantly continue on world upgrade finish diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index fb3bbe36c7bf67be65f0978ead2d4c778c32ba82..76022824dc9de7e64009383378f8ba75f70b09f3 100644 +index 1b4e0c7f23186afd88c7a71e296a5ca958f1c1d8..d6bdcb21ec79a81c31eaab24a73b55d08351e19b 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -870,7 +870,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Wed, 17 Jan 2024 13:46:16 +0800 +Subject: [PATCH] Leaves Disable Moved Wrongly Threshold + + +diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 5736cb60316da784590ef03d5808376254aae20b..06c716715b29fa4d2481bb8716ac25e8ba3c4209 100644 +--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -247,7 +247,9 @@ import org.bukkit.inventory.EquipmentSlot; + import org.bukkit.inventory.InventoryView; + import org.bukkit.inventory.SmithingInventory; + // CraftBukkit end +- ++// Leaf start ++import org.dreeam.leaf.LeafConfig; ++// Leaf end + public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl implements ServerGamePacketListener, ServerPlayerConnection, TickablePacketListener { + + static final Logger LOGGER = LogUtils.getLogger(); +@@ -583,7 +585,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + } + // Paper end + +- if (d10 - d9 > Math.max(100.0D, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) { ++ if (!LeafConfig.disableMovedWronglyThreshold && d10 - d9 > Math.max(100.0D, Math.pow((double) (org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed), 2)) && !this.isSingleplayerOwner()) { // Leaves - disable can + // CraftBukkit end + ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved too quickly! {},{},{}", new Object[]{entity.getName().getString(), this.player.getName().getString(), d6, d7, d8}); + this.send(new ClientboundMoveVehiclePacket(entity)); +@@ -619,7 +621,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + d10 = d6 * d6 + d7 * d7 + d8 * d8; + boolean flag2 = false; + +- if (d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot ++ if (!LeafConfig.disableMovedWronglyThreshold && d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot // Leaves - disable can + flag2 = true; // Paper - diff on change, this should be moved wrongly + ServerGamePacketListenerImpl.LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", new Object[]{entity.getName().getString(), this.player.getName().getString(), Math.sqrt(d10)}); + } +@@ -1487,7 +1489,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY, + toX, toY, toZ, toYaw, toPitch, true); + if (!event.isAllowed()) { +- if (event.getLogWarning()) ++ if (!LeafConfig.disableMovedWronglyThreshold && event.getLogWarning()) // Leaves - disable can + ServerGamePacketListenerImpl.LOGGER.warn("{} moved too quickly! {},{},{}", new Object[]{this.player.getName().getString(), d6, d7, d8}); + this.teleport(this.player.getX(), this.player.getY(), this.player.getZ(), this.player.getYRot(), this.player.getXRot()); + return; +@@ -1557,7 +1559,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl + d10 = d6 * d6 + d7 * d7 + d8 * d8; + boolean flag2 = false; + +- if (!this.player.isChangingDimension() && d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot ++ if (!LeafConfig.disableMovedWronglyThreshold && !this.player.isChangingDimension() && d10 > org.spigotmc.SpigotConfig.movedWronglyThreshold && !this.player.isSleeping() && !this.player.gameMode.isCreative() && this.player.gameMode.getGameModeForPlayer() != GameType.SPECTATOR) { // Spigot // Leaves - disable can + // Paper start - Add fail move event + io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_WRONGLY, + toX, toY, toZ, toYaw, toPitch, true); +diff --git a/src/main/java/org/dreeam/leaf/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java +index c51b54bcb96d06197d6265055e2e9e44858cd224..156765eeee6ab46f18e34cf62dde1a7d878932cc 100644 +--- a/src/main/java/org/dreeam/leaf/LeafConfig.java ++++ b/src/main/java/org/dreeam/leaf/LeafConfig.java +@@ -313,4 +313,8 @@ public class LeafConfig { + extraHorizontal = getDouble("playerKnockback.extraHorizontal", extraHorizontal); + extraVertical = getDouble("playerKnockback.extraVertical", extraVertical); + } ++ public static boolean disableMovedWronglyThreshold = false; ++ private static void getDisableMovedWronglyThreshold() { ++ disableMovedWronglyThreshold = getBoolean("disableMovedWronglyThreshold",disableMovedWronglyThreshold,"Disable Moved Wrongly Threshold"); ++ } + } From 1cfdc3aa2956bd01484b5b5e31900d7e8c70c2f2 Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:38:35 +0800 Subject: [PATCH 09/16] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=80=89=E9=A1=B9?= =?UTF-8?q?=EF=BC=8C=E6=88=91=E6=98=AFjoker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...aves-Disable-Moved-Wrongly-Threshold.patch | 2 +- .../0049-Leaves-be-vanilla-end-gateway.patch | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 patches/server/0049-Leaves-be-vanilla-end-gateway.patch diff --git a/patches/server/0048-Leaves-Disable-Moved-Wrongly-Threshold.patch b/patches/server/0048-Leaves-Disable-Moved-Wrongly-Threshold.patch index 87490f14..111e9dfc 100644 --- a/patches/server/0048-Leaves-Disable-Moved-Wrongly-Threshold.patch +++ b/patches/server/0048-Leaves-Disable-Moved-Wrongly-Threshold.patch @@ -65,6 +65,6 @@ index c51b54bcb96d06197d6265055e2e9e44858cd224..156765eeee6ab46f18e34cf62dde1a7d } + public static boolean disableMovedWronglyThreshold = false; + private static void getDisableMovedWronglyThreshold() { -+ disableMovedWronglyThreshold = getBoolean("disableMovedWronglyThreshold",disableMovedWronglyThreshold,"Disable Moved Wrongly Threshold"); ++ disableMovedWronglyThreshold = getBoolean("disableMovedWronglyThreshold",disableMovedWronglyThreshold,:"Disable Moved Wrongly Threshold") + } } diff --git a/patches/server/0049-Leaves-be-vanilla-end-gateway.patch b/patches/server/0049-Leaves-be-vanilla-end-gateway.patch new file mode 100644 index 00000000..ddd87db6 --- /dev/null +++ b/patches/server/0049-Leaves-be-vanilla-end-gateway.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Wed, 17 Jan 2024 14:29:37 +0800 +Subject: [PATCH] Leaves be vanilla end gateway + + +diff --git a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index 5d9f25da1bd502b0047abc64ef7602968ee3cb20..2ca123ecd7cac2c7fdaafcc2c58c8c126adcf94a 100644 +--- a/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/src/main/java/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -38,6 +38,9 @@ import org.bukkit.craftbukkit.entity.CraftPlayer; + import org.bukkit.craftbukkit.util.CraftLocation; + import org.bukkit.event.player.PlayerTeleportEvent; + // CraftBukkit end ++// Leaf start ++import org.dreeam.leaf.LeafConfig; ++// Leaf end + + public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + +@@ -107,7 +110,7 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + if (!list.isEmpty()) { + // Paper start + for (Entity entity : list) { +- if (entity.canChangeDimensions()) { ++ if (LeafConfig.vanillaEndTeleport || entity.canChangeDimensions()) { // Leaf - be vanilla + TheEndGatewayBlockEntity.teleportEntity(world, pos, state, entity, blockEntity); + break; + } +diff --git a/src/main/java/org/dreeam/leaf/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java +index 156765eeee6ab46f18e34cf62dde1a7d878932cc..728082120af5a7afe20078b3fa6d80605f5ff6c7 100644 +--- a/src/main/java/org/dreeam/leaf/LeafConfig.java ++++ b/src/main/java/org/dreeam/leaf/LeafConfig.java +@@ -315,6 +315,10 @@ public class LeafConfig { + } + public static boolean disableMovedWronglyThreshold = false; + private static void getDisableMovedWronglyThreshold() { +- disableMovedWronglyThreshold = getBoolean("disableMovedWronglyThreshold",disableMovedWronglyThreshold,:"Disable Moved Wrongly Threshold") ++ disableMovedWronglyThreshold = getBoolean("disableMovedWronglyThreshold",disableMovedWronglyThreshold,"Disable Moved Wrongly Threshold"); ++ } ++ public static boolean vanillaEndTeleport = false; ++ private static void getVanillaTeleport() { ++ vanillaEndTeleport = getBoolean("vanillaEndTeleport",vanillaEndTeleport,"Vanilla End Gateway Teleport"); + } + } From 0be2cd0fc60d6ad57ff1e50675bafa303851861d Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:04:52 +0800 Subject: [PATCH 10/16] Add missing config --- ...Purpur-and-Pufferfish-missing-config.patch | 329 ++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 patches/server/0050-Add-Purpur-and-Pufferfish-missing-config.patch diff --git a/patches/server/0050-Add-Purpur-and-Pufferfish-missing-config.patch b/patches/server/0050-Add-Purpur-and-Pufferfish-missing-config.patch new file mode 100644 index 00000000..60eb4998 --- /dev/null +++ b/patches/server/0050-Add-Purpur-and-Pufferfish-missing-config.patch @@ -0,0 +1,329 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Wed, 17 Jan 2024 18:02:03 +0800 +Subject: [PATCH] Add Purpur and Pufferfish missing config + + +diff --git a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +index eb60d2d99155aae4a761051175fbbddf9ed5dad9..5444213b336bc08eb371673890ded04c1178fd90 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java ++++ b/src/main/java/net/minecraft/world/entity/animal/allay/Allay.java +@@ -150,6 +150,23 @@ public class Allay extends PathfinderMob implements InventoryCarrier, VibrationS + protected void registerGoals() { + this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + } ++ ++ // Plazma start ++ @Override ++ public boolean isSensitiveToWater() { ++ return level().purpurConfig.allayTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean isAlwaysExperienceDropper() { ++ return level().purpurConfig.allayAlwaysDropExp; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(level().purpurConfig.allayMaxHealth); ++ } ++ // Plazma end + // Purpur end + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java b/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java +index 11181c0429b720381e7752f8a0d5e89615bcce4b..30564115cea127cfff255cf203a187c6790040a8 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java ++++ b/src/main/java/net/minecraft/world/entity/animal/camel/Camel.java +@@ -95,6 +95,18 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Saddl + public int getPurpurBreedTime() { + return this.level().purpurConfig.camelBreedingTicks; + } ++ ++ // Plazma start ++ @Override ++ public boolean isSensitiveToWater() { ++ return level().purpurConfig.camelTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean isAlwaysExperienceDropper() { ++ return level().purpurConfig.camelAlwaysDropExp; ++ } ++ // Plazma end + // Purpur end + + @Override +@@ -155,10 +167,11 @@ public class Camel extends AbstractHorse implements PlayerRideableJumping, Saddl + protected float getStandingEyeHeight(Pose pose, EntityDimensions dimensions) { + return dimensions.height - 0.1F * this.getScale(); + } +- ++ private int behaviorTick = 0; // Leaf + @Override + protected void customServerAiStep() { + Brain brain = (Brain) this.getBrain(); // Paper - decompile fix ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Leaf - Add missing pufferfish configurations + brain.tick((ServerLevel)this.level(), this); + CamelAi.updateActivity(this); + super.customServerAiStep(); +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +index 80a7197835531fa8cd276d9d31860f2b5ad3089f..8fd223ef7a72fcd0c62bcfbb17b83f63ceb8b70f 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Frog.java +@@ -138,6 +138,23 @@ public class Frog extends Animal implements VariantHolder { + public float getJumpPower() { + return (getRider() != null && isControllable()) ? level().purpurConfig.frogRidableJumpHeight * this.getBlockJumpFactor() : super.getJumpPower(); + } ++ ++ // Plazma start ++ @Override ++ public boolean isSensitiveToWater() { ++ return level().purpurConfig.frogTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean isAlwaysExperienceDropper() { ++ return level().purpurConfig.frogAlwaysDropExp; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(level().purpurConfig.frogMaxHealth); ++ } ++ // Plazma end + // Purpur end + + public int getPurpurBreedTime() { +diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +index 1fbd034002fd5761011976510b9f9263ca0f5cac..a9d75fe602d0f946519582d2eaf2eb55db69e4b8 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -87,6 +87,23 @@ public class Tadpole extends AbstractFish { + protected void registerGoals() { + this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + } ++ ++ // Plazma start ++ @Override ++ public boolean isSensitiveToWater() { ++ return level().purpurConfig.tadpoleTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean isAlwaysExperienceDropper() { ++ return level().purpurConfig.tadpoleAlwaysDropExp; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(level().purpurConfig.tadpoleMaxHealth); ++ } ++ // Plazma end + // Purpur end + + @Override +diff --git a/src/main/java/net/minecraft/world/entity/animal/sniffer/Sniffer.java b/src/main/java/net/minecraft/world/entity/animal/sniffer/Sniffer.java +index b8a055f938ade211c07ca5a74bade19707568eb1..af5b46d198ff950c501c9f65ba5a0ca76d94f4a8 100644 +--- a/src/main/java/net/minecraft/world/entity/animal/sniffer/Sniffer.java ++++ b/src/main/java/net/minecraft/world/entity/animal/sniffer/Sniffer.java +@@ -106,6 +106,18 @@ public class Sniffer extends Animal { + public boolean isControllable() { + return level().purpurConfig.snifferControllable; + } ++ ++ // Plazma start ++ @Override ++ public boolean isSensitiveToWater() { ++ return level().purpurConfig.snifferTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean isAlwaysExperienceDropper() { ++ return level().purpurConfig.snifferAlwaysDropExp; ++ } ++ // Plazma end + // Purpur end + + @Override +@@ -514,9 +526,10 @@ public class Sniffer extends Animal { + protected Brain.Provider brainProvider() { + return Brain.provider(SnifferAi.MEMORY_TYPES, SnifferAi.SENSOR_TYPES); + } +- ++ private int behaviorTick; + @Override + protected void customServerAiStep() { ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Leaf - Add missing pufferfish configurations + this.getBrain().tick((ServerLevel) this.level(), this); + SnifferAi.updateActivity(this); + super.customServerAiStep(); +diff --git a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +index c23d192baf78890093b0e88d03735eef4569e75f..5cd66f51463d519fdfb33eb80864a7593a419193 100644 +--- a/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java ++++ b/src/main/java/net/minecraft/world/entity/monster/warden/Warden.java +@@ -147,6 +147,23 @@ public class Warden extends Monster implements VibrationSystem { + this.goalSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + this.targetSelector.addGoal(0, new org.purpurmc.purpur.entity.ai.HasRider(this)); // Purpur + } ++ ++ // Plazma start ++ @Override ++ public boolean isSensitiveToWater() { ++ return level().purpurConfig.wardenTakeDamageFromWater; ++ } ++ ++ @Override ++ public boolean isAlwaysExperienceDropper() { ++ return level().purpurConfig.wardenAlwaysDropExp; ++ } ++ ++ @Override ++ public void initAttributes() { ++ this.getAttribute(Attributes.MAX_HEALTH).setBaseValue(level().purpurConfig.wardenMaxHealth); ++ } ++ // Plazma end + // Purpur end + + @Override +@@ -302,7 +319,7 @@ public class Warden extends Monster implements VibrationSystem { + protected void customServerAiStep() { + ServerLevel worldserver = (ServerLevel) this.level(); + +- if (this.behaviorTick++ % this.activatedPriority == 0) // Pufferfish ++ if ((getRider() == null || !this.isControllable()) && this.behaviorTick++ % this.activatedPriority == 0) // Leaf + this.getBrain().tick(worldserver, this); + super.customServerAiStep(); + if ((this.tickCount + this.getId()) % 120 == 0) { +diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java b/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java +index bc3fe45d12ffc2069a03d1587b7623d31130565a..b621ec04e8bcca75ad8e8daf22e74197530b1c51 100644 +--- a/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java ++++ b/src/main/java/net/minecraft/world/entity/vehicle/ChestBoat.java +@@ -41,7 +41,7 @@ public class ChestBoat extends Boat implements HasCustomInventoryScreen, Contain + + public ChestBoat(EntityType type, Level world) { + super(type, world); +- this.itemStacks = NonNullList.withSize(27, ItemStack.EMPTY); ++ this.itemStacks = NonNullList.withSize(org.purpurmc.purpur.PurpurConfig.chestBoatRows * 9, ItemStack.EMPTY); // Leaf + } + + public ChestBoat(Level world, double d0, double d1, double d2) { +diff --git a/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +index c3f095236093c436d37c4730f5537d4fbc0ea6c0..d13bc7e913ebe816598f6104f928c90b320fee18 100644 +--- a/src/main/java/org/purpurmc/purpur/PurpurConfig.java ++++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java +@@ -336,6 +336,7 @@ public class PurpurConfig { + } + + public static int barrelRows = 3; ++ public static int chestBoatRows = 3; // Plazma + public static boolean enderChestSixRows = false; + public static boolean enderChestPermissionRows = false; + public static boolean cryingObsidianValidForPortalFrame = false; +@@ -376,6 +377,7 @@ public class PurpurConfig { + case 1 -> 9; + default -> 27; + }); ++ chestBoatRows = getInt("settings.blocks.chest_boat.rows", chestBoatRows); // Plazma + enderChestSixRows = getBoolean("settings.blocks.ender_chest.six-rows", enderChestSixRows); + org.bukkit.event.inventory.InventoryType.ENDER_CHEST.setDefaultSize(enderChestSixRows ? 54 : 27); + enderChestPermissionRows = getBoolean("settings.blocks.ender_chest.use-permissions-for-rows", enderChestPermissionRows); +diff --git a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +index 2b4408d398962fa4feab9eb248540c6d11037a94..3ede56b60a06b6ef808a73a38719968656f66f08 100644 +--- a/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java ++++ b/src/main/java/org/purpurmc/purpur/PurpurWorldConfig.java +@@ -1195,7 +1195,15 @@ public class PurpurWorldConfig { + public boolean allayRidableInWater = true; + public boolean allayControllable = true; + public List allayRespectNBT = new ArrayList<>(); ++ // Plazma start - Add missing purpur config options ++ public double allayMaxHealth = 20.0D; ++ public boolean allayTakeDamageFromWater = false; ++ public boolean allayAlwaysDropExp = false; + private void allaySettings() { ++ allayMaxHealth = getDouble("mobs.allay.max-health", allayMaxHealth); ++ allayTakeDamageFromWater = getBoolean("mobs.allay.take-damage-from-water", allayTakeDamageFromWater); ++ allayAlwaysDropExp = getBoolean("mobs.allay.always-drop-exp", allayAlwaysDropExp); ++ // Plazma end + allayRidable = getBoolean("mobs.allay.ridable", allayRidable); + allayRidableInWater = getBoolean("mobs.allay.ridable-in-water", allayRidableInWater); + allayControllable = getBoolean("mobs.allay.controllable", allayControllable); +@@ -1314,7 +1322,15 @@ public class PurpurWorldConfig { + public double camelMovementSpeedMin = 0.09D; + public double camelMovementSpeedMax = 0.09D; + public int camelBreedingTicks = 6000; ++ // Plazma start - Add missing purpur config options ++ //public boolean camelRidableInWater = false; ++ public boolean camelTakeDamageFromWater = false; ++ public boolean camelAlwaysDropExp = false; + private void camelSettings() { ++ //camelRidableInWater = getBoolean("mobs.camel.ridable-in-water", camelRidableInWater); ++ camelTakeDamageFromWater = getBoolean("mobs.camel.takes-damage-from-water", camelTakeDamageFromWater); ++ camelAlwaysDropExp = getBoolean("mobs.camel.always-drop-exp", camelAlwaysDropExp); ++ // Plazma end + camelRidableInWater = getBoolean("mobs.camel.ridable-in-water", camelRidableInWater); + camelMaxHealthMin = getDouble("mobs.camel.attributes.max_health.min", camelMaxHealthMin); + camelMaxHealthMax = getDouble("mobs.camel.attributes.max_health.max", camelMaxHealthMax); +@@ -1744,7 +1760,15 @@ public class PurpurWorldConfig { + public boolean frogControllable = true; + public float frogRidableJumpHeight = 0.65F; + public int frogBreedingTicks = 6000; ++ // Plazma start - Add missing purpur config options ++ public double frogMaxHealth = 10.0D; ++ public boolean frogTakeDamageFromWater = false; ++ public boolean frogAlwaysDropExp = false; + private void frogSettings() { ++ frogMaxHealth = getDouble("mobs.frog.attributes.max_health", frogMaxHealth); ++ frogTakeDamageFromWater = getBoolean("mobs.frog.takes-damage-from-water", frogTakeDamageFromWater); ++ frogAlwaysDropExp = getBoolean("mobs.frog.always-drop-exp", frogAlwaysDropExp); ++ // Plazma end + frogRidable = getBoolean("mobs.frog.ridable", frogRidable); + frogRidableInWater = getBoolean("mobs.frog.ridable-in-water", frogRidableInWater); + frogControllable = getBoolean("mobs.frog.controllable", frogControllable); +@@ -2690,7 +2714,13 @@ public class PurpurWorldConfig { + public boolean snifferControllable = true; + public double snifferMaxHealth = 14.0D; + public int snifferBreedingTicks = 6000; ++ // Plazma start - Add missing purpur config options ++ public boolean snifferTakeDamageFromWater = false; ++ public boolean snifferAlwaysDropExp = false; + private void snifferSettings() { ++ snifferTakeDamageFromWater = getBoolean("mobs.sniffer.takes-damage-from-water", snifferTakeDamageFromWater); ++ snifferAlwaysDropExp = getBoolean("mobs.sniffer.always-drop-exp", snifferAlwaysDropExp); ++ // Plazma end + snifferRidable = getBoolean("mobs.sniffer.ridable", snifferRidable); + snifferRidableInWater = getBoolean("mobs.sniffer.ridable-in-water", snifferRidableInWater); + snifferControllable = getBoolean("mobs.sniffer.controllable", snifferControllable); +@@ -2789,7 +2819,15 @@ public class PurpurWorldConfig { + public boolean tadpoleRidable = false; + public boolean tadpoleRidableInWater = true; + public boolean tadpoleControllable = true; ++ // Plazma start - Add missing purpur config options ++ public double tadpoleMaxHealth = 10.0D; ++ public boolean tadpoleTakeDamageFromWater = false; ++ public boolean tadpoleAlwaysDropExp = false; + private void tadpoleSettings() { ++ tadpoleMaxHealth = getDouble("mobs.tadpole.attributes.max_health", tadpoleMaxHealth); ++ tadpoleTakeDamageFromWater = getBoolean("mobs.tadpole.takes-damage-from-water", tadpoleTakeDamageFromWater); ++ tadpoleAlwaysDropExp = getBoolean("mobs.tadpole.always-drop-exp", tadpoleAlwaysDropExp); ++ // Plazma end + tadpoleRidable = getBoolean("mobs.tadpole.ridable", tadpoleRidable); + tadpoleRidableInWater = getBoolean("mobs.tadpole.ridable-in-water", tadpoleRidableInWater); + tadpoleControllable = getBoolean("mobs.tadpole.controllable", tadpoleControllable); +@@ -2999,7 +3037,15 @@ public class PurpurWorldConfig { + public boolean wardenRidable = false; + public boolean wardenRidableInWater = true; + public boolean wardenControllable = true; ++ // Plazma start - Add missing purpur config options ++ public double wardenMaxHealth = 500.0D; ++ public boolean wardenTakeDamageFromWater = false; ++ public boolean wardenAlwaysDropExp = false; + private void wardenSettings() { ++ wardenMaxHealth = getDouble("mobs.warden.attributes.max_health", wardenMaxHealth); ++ wardenTakeDamageFromWater = getBoolean("mobs.warden.takes-damage-from-water", wardenTakeDamageFromWater); ++ wardenAlwaysDropExp = getBoolean("mobs.warden.always-drop-exp", wardenAlwaysDropExp); ++ // Plazma end + wardenRidable = getBoolean("mobs.warden.ridable", wardenRidable); + wardenRidableInWater = getBoolean("mobs.warden.ridable-in-water", wardenRidableInWater); + wardenControllable = getBoolean("mobs.warden.controllable", wardenControllable); From 9cc4b124f7185c47e6fe40d27c5b474c8245d682 Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:17:07 +0800 Subject: [PATCH 11/16] Skip-event-if-no-listeners --- .../0051-Skip-event-if-no-listeners.patch | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 patches/server/0051-Skip-event-if-no-listeners.patch diff --git a/patches/server/0051-Skip-event-if-no-listeners.patch b/patches/server/0051-Skip-event-if-no-listeners.patch new file mode 100644 index 00000000..42a9b06a --- /dev/null +++ b/patches/server/0051-Skip-event-if-no-listeners.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Wed, 17 Jan 2024 18:16:38 +0800 +Subject: [PATCH] Skip event if no listeners + + +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +index 06dfd0b27ac0006a2be07f54a0702519a691c6ec..90b40040b2dc0e8a481515a9b139cf1277578545 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +@@ -35,6 +35,10 @@ class PaperEventManager { + + // SimplePluginManager + public void callEvent(@NotNull Event event) { ++ // Leaf start - Skip event if no listeners ++ RegisteredListener[] listeners = event.getHandlers().getRegisteredListeners(); ++ if (listeners.length == 0) return; ++ // Leaf end + // KTP start - Optimise spigot event bus + if (event.asynchronous() != net.kyori.adventure.util.TriState.NOT_SET) { + final boolean onPrimaryThread = this.server.isPrimaryThread(); +@@ -47,9 +51,6 @@ class PaperEventManager { + // KTP stop - Optimise spigot event bus + } + +- HandlerList handlers = event.getHandlers(); +- RegisteredListener[] listeners = handlers.getRegisteredListeners(); +- + for (RegisteredListener registration : listeners) { + if (!registration.getPlugin().isEnabled()) { + continue; From ddc10546b32090f53ea5b4ce2db459894be5637d Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 18:59:14 +0800 Subject: [PATCH 12/16] Optimize-canSee-checks --- .../server/0052-Optimize-canSee-checks.patch | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 patches/server/0052-Optimize-canSee-checks.patch diff --git a/patches/server/0052-Optimize-canSee-checks.patch b/patches/server/0052-Optimize-canSee-checks.patch new file mode 100644 index 00000000..5cf4dacc --- /dev/null +++ b/patches/server/0052-Optimize-canSee-checks.patch @@ -0,0 +1,56 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Wed, 17 Jan 2024 18:58:47 +0800 +Subject: [PATCH] Optimize canSee checks + + +diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java +index 38ddd6684535f9a4e6e9fb432282e0b6be0ede4c..673f0b1f8ad35559d050cb73f6593dc466d6ac0c 100644 +--- a/src/main/java/net/minecraft/server/level/ChunkMap.java ++++ b/src/main/java/net/minecraft/server/level/ChunkMap.java +@@ -1409,7 +1409,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + // Paper end - check Y + + // CraftBukkit start - respect vanish API +- if (flag && !player.getBukkitEntity().canSee(this.entity.getBukkitEntity())) { // Paper - only consider hits ++ if (flag && !player.getBukkitEntity().canSeeChunkMapUpdatePlayer(this.entity.getBukkitEntity())) { // Paper - only consider hits // Polpot - Optimize canSee checks + flag = false; + } + // CraftBukkit end +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 468ac1525c97043b53b16440fec6bb784e1e9c25..86a4a62dfb9c81078e630885f43d7bd24826e58a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -188,7 +188,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + private boolean hasPlayedBefore = false; + private final ConversationTracker conversationTracker = new ConversationTracker(); + private final Set channels = new HashSet(); +- private final Map>> invertedVisibilityEntities = new HashMap<>(); ++ private final Map>> invertedVisibilityEntities = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Polpot - optimize canSee checks + private final Set unlistedEntities = new HashSet<>(); // Paper + private static final WeakHashMap> pluginWeakReferences = new WeakHashMap<>(); + private int hash = 0; +@@ -2166,7 +2166,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + // Leaf - TODO + @Override + public boolean canSee(org.bukkit.entity.Entity entity) { +- return this.equals(entity) || entity.isVisibleByDefault() ^ this.invertedVisibilityEntities.containsKey(entity.getUniqueId()); // SPIGOT-7312: Can always see self ++ return this.equals(entity) || entity.isVisibleByDefault() ^ (!invertedVisibilityEntities.isEmpty() && this.invertedVisibilityEntities.containsKey(entity.getUniqueId())); // SPIGOT-7312: Can always see self // Polpot - optimize canSee checks + } + + public boolean canSee(UUID uuid) { +@@ -2178,7 +2178,14 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + return (entity != null) ? this.canSee(entity) : false; // If we can't find it, we can't see it + } + ++ // Polpot - optimize canSee checks ++ // The check in ChunkMap#updatePlayer already rejects if it is the same entity, so we don't need to check it twice, especially because CraftPlayer's equals check is a bit expensive ++ public boolean canSeeChunkMapUpdatePlayer(org.bukkit.entity.Entity entity) { ++ return entity.isVisibleByDefault() ^ (!invertedVisibilityEntities.isEmpty() && this.invertedVisibilityEntities.containsKey(entity.getUniqueId())); // SPIGOT-7312: Can always see self // SparklyPaper - optimize canSee checks ++ } ++ // Polpot end + // Paper start ++ + @Override + public boolean isListed(Player other) { + return !this.unlistedEntities.contains(other.getUniqueId()); From f04568eece2da71ffa1ea1d9ac3d5ec26028a9fa Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:41:23 +0800 Subject: [PATCH 13/16] Rewrite-framed-map-tracker-ticking --- ...r-Rewrite-framed-map-tracker-ticking.patch | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 patches/server/0053-SparklyPaper-Rewrite-framed-map-tracker-ticking.patch diff --git a/patches/server/0053-SparklyPaper-Rewrite-framed-map-tracker-ticking.patch b/patches/server/0053-SparklyPaper-Rewrite-framed-map-tracker-ticking.patch new file mode 100644 index 00000000..b91da0b4 --- /dev/null +++ b/patches/server/0053-SparklyPaper-Rewrite-framed-map-tracker-ticking.patch @@ -0,0 +1,286 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Wed, 17 Jan 2024 19:40:17 +0800 +Subject: [PATCH] SparklyPaper Rewrite-framed-map-tracker-ticking + + +diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java +index 068dd1194016b65227ec44747b065860a48e7f1d..26a68dd4e7373291c645053990e63eb27ac41dbf 100644 +--- a/src/main/java/net/minecraft/server/level/ServerEntity.java ++++ b/src/main/java/net/minecraft/server/level/ServerEntity.java +@@ -115,23 +115,35 @@ public class ServerEntity { + ItemFrame entityitemframe = (ItemFrame) entity; + + if (true || this.tickCount % 10 == 0) { // CraftBukkit - Moved below, should always enter this block +- ItemStack itemstack = entityitemframe.getItem(); ++ //ItemStack itemstack = entityitemframe.getItem(); // SparklyPaper + +- if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 && itemstack.getItem() instanceof MapItem) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // Paper - Make item frame map cursor update interval configurable ++ if (this.level.paperConfig().maps.itemFrameCursorUpdateInterval > 0 && this.tickCount % this.level.paperConfig().maps.itemFrameCursorUpdateInterval == 0 /*&&itemstack.getItem() instanceof MapItem*/) { // CraftBukkit - Moved this.tickCounter % 10 logic here so item frames do not enter the other blocks // SparklyPaper - Make item frame map cursor update interval configurable + Integer integer = entityitemframe.cachedMapId; // Paper + MapItemSavedData worldmap = MapItem.getSavedData(integer, this.level); + + if (worldmap != null) { +- Iterator iterator = this.trackedPlayers.iterator(); // CraftBukkit ++ // SparklyPaper start - re-use the same update packet when possible ++ if (!worldmap.hasContextualRenderer) { ++ // Pass in a "random" player when a non-contextual plugin renderer is added to make sure its called ++ final Packet updatePacket = worldmap.framedUpdatePacket(integer, worldmap.hasPluginRenderer ? com.google.common.collect.Iterables.getFirst(this.trackedPlayers, null).getPlayer() : null); ++ if (updatePacket != null) { ++ for (ServerPlayerConnection connection : this.trackedPlayers) { ++ connection.send(updatePacket); ++ } ++ } ++ } else { ++ // SparklyPaper end ++ Iterator iterator = this.trackedPlayers.iterator(); // CraftBukkit + +- while (iterator.hasNext()) { +- ServerPlayer entityplayer = iterator.next().getPlayer(); // CraftBukkit ++ while (iterator.hasNext()) { ++ ServerPlayer entityplayer = iterator.next().getPlayer(); // CraftBukkit + +- worldmap.tickCarriedBy(entityplayer, itemstack); +- Packet packet = worldmap.getUpdatePacket(integer, entityplayer); ++ //worldmap.tickCarriedBy(entityplayer, itemstack); // SparklyPaper ++ Packet packet = worldmap.framedUpdatePacket(integer, entityplayer); // SparklyPaper + +- if (packet != null) { +- entityplayer.connection.send(packet); ++ if (packet != null) { ++ entityplayer.connection.send(packet); ++ } + } + } + } +@@ -367,7 +379,19 @@ public class ServerEntity { + sender.accept(new ClientboundSetEntityLinkPacket(entityinsentient, entityinsentient.getLeashHolder())); + } + } ++ // SparklyPaper start ++ if (this.entity instanceof ItemFrame frame && frame.cachedMapId != null) { ++ MapItemSavedData mapData = MapItem.getSavedData(frame.cachedMapId, this.level); ++ ++ if (mapData != null) { ++ mapData.addFrameDecoration(frame); + ++ final Packet mapPacket = mapData.fullUpdatePacket(frame.cachedMapId, mapData.hasPluginRenderer ? player : null); ++ if (mapPacket != null) ++ sender.accept((Packet) mapPacket); ++ } ++ } ++ // SparklyPaper end + } + + private void sendDirtyEntityData() { +diff --git a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +index d728dc8a9b5fa2de0a824aaf132ee15db090b02e..37d83ae140ee07dc448a18d0061fc091785da063 100644 +--- a/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java ++++ b/src/main/java/net/minecraft/world/entity/decoration/ItemFrame.java +@@ -489,6 +489,16 @@ public class ItemFrame extends HangingEntity { + } + this.setItem(ItemStack.fromBukkitCopy(event.getItemStack())); + // Paper end ++ // SparklyPaper start - add decoration and mark everything dirty for other players who are already tracking this frame ++ final ItemStack item = this.getItem(); ++ if (item.is(Items.FILLED_MAP)) { ++ final MapItemSavedData data = MapItem.getSavedData(item, this.level()); ++ if (data != null) { ++ data.addFrameDecoration(this); ++ data.markAllDirty(); ++ } ++ } ++ // SparklyPaper end + this.gameEvent(GameEvent.BLOCK_CHANGE, player); + if (!player.getAbilities().instabuild) { + itemstack.shrink(1); +diff --git a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index 804c342783baccdc12e8ca49a362770e31596f6a..a0b760388407469adbd9569edfccadcb1b3d37a1 100644 +--- a/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/src/main/java/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -67,6 +67,16 @@ public class MapItemSavedData extends SavedData { + private final Map frameMarkers = Maps.newHashMap(); + private int trackedDecorationCount; + private org.bukkit.craftbukkit.map.RenderData vanillaRender = new org.bukkit.craftbukkit.map.RenderData(); // Paper ++ // SparklyPaper start - shared between all players tracking this map inside an item frame ++ public boolean dirtyColorData; ++ public int minDirtyX; ++ public int minDirtyY; ++ public int maxDirtyX; ++ public int maxDirtyY; ++ public boolean dirtyFrameDecorations; ++ public boolean hasPluginRenderer; ++ public boolean hasContextualRenderer; ++ // SparklyPaper end + public boolean isExplorerMap; // Purpur + + // CraftBukkit start +@@ -333,6 +343,7 @@ public class MapItemSavedData extends SavedData { + } + + this.setDecorationsDirty(); ++ if (mapicon != null && mapicon.renderOnFrame()) this.dirtyFrameDecorations = true; // SparklyPaper + } + + public static void addTargetDecoration(ItemStack stack, BlockPos pos, String id, MapDecoration.Type type) { +@@ -428,6 +439,7 @@ public class MapItemSavedData extends SavedData { + } + + this.setDecorationsDirty(); ++ if (type.isRenderedOnFrame() || (mapicon1 != null && mapicon.type().isRenderedOnFrame())) this.dirtyFrameDecorations = true; // SparklyPaper + } + + } +@@ -441,6 +453,20 @@ public class MapItemSavedData extends SavedData { + + public void setColorsDirty(int x, int z) { + this.setDirty(); ++ // SparklyPaper start ++ if (this.dirtyColorData) { ++ this.minDirtyX = Math.min(this.minDirtyX, x); ++ this.minDirtyY = Math.min(this.minDirtyY, z); ++ this.maxDirtyX = Math.max(this.maxDirtyX, x); ++ this.maxDirtyY = Math.max(this.maxDirtyY, z); ++ } else { ++ this.dirtyColorData = true; ++ this.minDirtyX = x; ++ this.minDirtyY = z; ++ this.maxDirtyX = x; ++ this.maxDirtyY = z; ++ } ++ // SparklyPaper end + Iterator iterator = this.carriedBy.iterator(); + + while (iterator.hasNext()) { +@@ -523,6 +549,7 @@ public class MapItemSavedData extends SavedData { + public void removedFromFrame(BlockPos pos, int id) { + this.removeDecoration("frame-" + id); + this.frameMarkers.remove(MapFrame.frameId(pos)); ++ this.dirtyFrameDecorations = true; // SparklyPaper + } + + public boolean updateColor(int x, int z, byte color) { +@@ -579,7 +606,90 @@ public class MapItemSavedData extends SavedData { + public boolean isTrackedCountOverLimit(int iconCount) { + return this.trackedDecorationCount >= iconCount; + } ++ // SparklyPaper start ++ public final @Nullable Packet framedUpdatePacket(int id, @Nullable Player player) { ++ return createUpdatePacket(id, player, false); ++ } ++ ++ public final @Nullable Packet fullUpdatePacket(int id, @Nullable Player player) { ++ return createUpdatePacket(id, player, true); ++ } ++ ++ public final @Nullable Packet createUpdatePacket(int id, @Nullable Player player, boolean full) { ++ if (!dirtyColorData && !dirtyFrameDecorations && (player == null || server.getCurrentTick() % 5 != 0) && !full) { ++ return null; ++ } ++ ++ final org.bukkit.craftbukkit.map.RenderData render = player != null ? this.mapView.render((org.bukkit.craftbukkit.entity.CraftPlayer) player.getBukkitEntity()) : this.vanillaRender; ++ ++ final MapPatch patch; ++ if (full) { ++ patch = createPatch(render.buffer, 0, 0, 127, 127); ++ } else if (dirtyColorData) { ++ dirtyColorData = false; ++ patch = createPatch(render.buffer, this.minDirtyX, this.minDirtyY, this.maxDirtyX, this.maxDirtyY); ++ } else { ++ patch = null; ++ } ++ ++ Collection decorations = null; ++ if (dirtyFrameDecorations || full || hasPluginRenderer) { ++ dirtyFrameDecorations = false; ++ decorations = new java.util.ArrayList<>(); ++ ++ if (player == null) { ++ for (MapDecoration decoration : this.decorations.values()) { ++ if (decoration.renderOnFrame()) { ++ decorations.add(decoration); ++ } ++ } ++ } + ++ for (final org.bukkit.map.MapCursor cursor : render.cursors) { ++ if (cursor.isVisible()) { ++ decorations.add(new MapDecoration(MapDecoration.Type.byIcon(cursor.getRawType()), cursor.getX(), cursor.getY(), cursor.getDirection(), PaperAdventure.asVanilla(cursor.caption()))); // Paper - Adventure ++ } ++ } ++ } ++ ++ return new ClientboundMapItemDataPacket(id, this.scale, this.locked, decorations, patch); ++ } ++ ++ private MapPatch createPatch(byte[] buffer, int minDirtyX, int minDirtyY, int maxDirtyX, int maxDirtyY) { ++ int i = minDirtyX; ++ int j = minDirtyY; ++ int k = maxDirtyX + 1 - minDirtyX; ++ int l = maxDirtyY + 1 - minDirtyY; ++ byte[] abyte = new byte[k * l]; ++ ++ for (int i1 = 0; i1 < k; ++i1) { ++ for (int j1 = 0; j1 < l; ++j1) { ++ abyte[i1 + j1 * k] = buffer[i + i1 + (j + j1) * 128]; ++ } ++ } ++ ++ return new MapItemSavedData.MapPatch(i, j, k, l, abyte); ++ } ++ ++ public void addFrameDecoration(net.minecraft.world.entity.decoration.ItemFrame frame) { ++ if (this.trackedDecorationCount >= frame.level().paperConfig().maps.itemFrameCursorLimit || this.frameMarkers.containsKey(MapFrame.frameId(frame.getPos()))) { ++ return; ++ } ++ ++ MapFrame mapFrame = new MapFrame(frame.getPos(), frame.getDirection().get2DDataValue() * 90, frame.getId()); ++ this.addDecoration(MapDecoration.Type.FRAME, frame.level(), "frame-" + frame.getId(), frame.getPos().getX(), frame.getPos().getZ(), mapFrame.getRotation(), (Component) null); ++ this.frameMarkers.put(mapFrame.getId(), mapFrame); ++ } ++ ++ public void markAllDirty() { ++ this.dirtyColorData = true; ++ this.minDirtyX = 0; ++ this.minDirtyY = 0; ++ this.maxDirtyX = 127; ++ this.maxDirtyY = 127; ++ this.dirtyFrameDecorations = true; ++ } ++ // SparklyPaper end + public class HoldingPlayer { + + // Paper start +diff --git a/src/main/java/org/bukkit/craftbukkit/map/CraftMapView.java b/src/main/java/org/bukkit/craftbukkit/map/CraftMapView.java +index c3266c43a073cb7d7eff10d1a1b15f0a2265b859..ff3b0f8ebea13f52a1d86085e32489db86969dd1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/map/CraftMapView.java ++++ b/src/main/java/org/bukkit/craftbukkit/map/CraftMapView.java +@@ -108,6 +108,10 @@ public final class CraftMapView implements MapView { + this.renderers.add(renderer); + this.canvases.put(renderer, new HashMap()); + renderer.initialize(this); ++ // SparklyPaper start ++ this.worldMap.hasPluginRenderer |= !(renderer instanceof CraftMapRenderer); ++ this.worldMap.hasContextualRenderer |= renderer.isContextual(); ++ // SparklyPaper end + } + } + +@@ -123,6 +127,17 @@ public final class CraftMapView implements MapView { + } + } + this.canvases.remove(renderer); ++ // SparklyPaper start ++ this.worldMap.hasPluginRenderer = !(this.renderers.size() == 1 && this.renderers.get(0) instanceof CraftMapRenderer); ++ if (renderer.isContextual()) { ++ // Re-check all renderers ++ boolean contextualFound = false; ++ for (final MapRenderer mapRenderer : this.renderers) { ++ contextualFound |= mapRenderer.isContextual(); ++ } ++ this.worldMap.hasContextualRenderer = contextualFound; ++ } ++ // SparklyPaper end + return true; + } else { + return false; From e2710aec1b49f0503106e4816adc45bf5242af27 Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:58:44 +0800 Subject: [PATCH 14/16] Skip-MapItem-update-if-the-map-does-not-have-the-Cra --- ...p-MapItem-update-if-the-map-does-not.patch | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 patches/server/0054-SparklyPaper-Skip-MapItem-update-if-the-map-does-not.patch diff --git a/patches/server/0054-SparklyPaper-Skip-MapItem-update-if-the-map-does-not.patch b/patches/server/0054-SparklyPaper-Skip-MapItem-update-if-the-map-does-not.patch new file mode 100644 index 00000000..43be7ef5 --- /dev/null +++ b/patches/server/0054-SparklyPaper-Skip-MapItem-update-if-the-map-does-not.patch @@ -0,0 +1,51 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Wed, 17 Jan 2024 19:57:53 +0800 +Subject: [PATCH] + SparklyPaper-Skip-MapItem-update-if-the-map-does-not-have-the-Cra + + +diff --git a/src/main/java/net/minecraft/world/item/MapItem.java b/src/main/java/net/minecraft/world/item/MapItem.java +index 6cfd169c2c32b644d70907358c2d4a2087c00a68..62aa1717eee90032c2ddf848424e903cc952a913 100644 +--- a/src/main/java/net/minecraft/world/item/MapItem.java ++++ b/src/main/java/net/minecraft/world/item/MapItem.java +@@ -32,6 +32,9 @@ import net.minecraft.world.level.levelgen.Heightmap; + import net.minecraft.world.level.material.FluidState; + import net.minecraft.world.level.material.MapColor; + import net.minecraft.world.level.saveddata.maps.MapItemSavedData; ++// Leaf Start ++import org.dreeam.leaf.LeafConfig; ++// Leaf end + + public class MapItem extends ComplexItem { + +@@ -314,6 +317,7 @@ public class MapItem extends ComplexItem { + } + + @Override ++ + public void inventoryTick(ItemStack stack, Level world, Entity entity, int slot, boolean selected) { + if (!world.isClientSide) { + MapItemSavedData worldmap = MapItem.getSavedData(stack, world); +@@ -325,7 +329,7 @@ public class MapItem extends ComplexItem { + worldmap.tickCarriedBy(entityhuman, stack); + } + +- if (!worldmap.locked && (selected || entity instanceof Player && ((Player) entity).getOffhandItem() == stack)) { ++ if (!worldmap.locked && (!LeafConfig.skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer || worldmap.mapView.getRenderers().stream().anyMatch(mapRenderer -> mapRenderer.getClass() == org.bukkit.craftbukkit.map.CraftMapRenderer.class)) && (selected || entity instanceof Player && ((Player) entity).getOffhandItem() == stack)) { // SparklyPaper - don't update maps if they don't have the CraftMapRenderer in the render list + this.update(world, entity, worldmap); + } + +diff --git a/src/main/java/org/dreeam/leaf/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java +index 728082120af5a7afe20078b3fa6d80605f5ff6c7..db12eb7e81307cc3fd4fe0602697e32e71289f7e 100644 +--- a/src/main/java/org/dreeam/leaf/LeafConfig.java ++++ b/src/main/java/org/dreeam/leaf/LeafConfig.java +@@ -321,4 +321,8 @@ public class LeafConfig { + private static void getVanillaTeleport() { + vanillaEndTeleport = getBoolean("vanillaEndTeleport",vanillaEndTeleport,"Vanilla End Gateway Teleport"); + } ++ public static boolean skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer=true; ++ private static void getSkipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer() { ++ skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer = getBoolean("skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer",skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer); ++ } + } From 7ba5b5ee9e5ecd11db3a060f320655edb4fba0ec Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:21:22 +0800 Subject: [PATCH 15/16] Sparkly and Polpot optimize --- ...ordinate-key-used-for-nearby-players.patch | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 patches/server/0055-Polpot-Cache-coordinate-key-used-for-nearby-players.patch diff --git a/patches/server/0055-Polpot-Cache-coordinate-key-used-for-nearby-players.patch b/patches/server/0055-Polpot-Cache-coordinate-key-used-for-nearby-players.patch new file mode 100644 index 00000000..88dec429 --- /dev/null +++ b/patches/server/0055-Polpot-Cache-coordinate-key-used-for-nearby-players.patch @@ -0,0 +1,72 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Thu, 18 Jan 2024 13:14:57 +0800 +Subject: [PATCH] Polpot Cache-coordinate-key-used-for-nearby-players + + +diff --git a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java +index 17ba07cbd4792f63d88ce29d00da280f30c4abff..98ae712c78d500e8c463aa671909a931a001407e 100644 +--- a/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java ++++ b/src/main/java/io/papermc/paper/util/player/NearbyPlayers.java +@@ -106,6 +106,13 @@ public final class NearbyPlayers { + return chunk == null ? null : chunk.players[type.ordinal()]; + } + ++ // Polpot start - cache coordinate key used for nearby players ++ public ReferenceList getPlayers(final long nearbyPlayersCoordinateKey, final NearbyMapType type) { ++ final TrackedChunk chunk = this.byChunk.get(nearbyPlayersCoordinateKey); ++ return chunk == null ? null : chunk.players[type.ordinal()]; ++ } ++ // Polpot end ++ + public ReferenceList getPlayersByChunk(final int chunkX, final int chunkZ, final NearbyMapType type) { + final TrackedChunk chunk = this.byChunk.get(CoordinateUtils.getChunkKey(chunkX, chunkZ)); + +diff --git a/src/main/java/net/minecraft/server/Eula.java b/src/main/java/net/minecraft/server/Eula.java +index b9403a3eafc1b30b33ac654f253d0849cacadd03..a6190f14cb9463ee75e8890f191a7ec88e59b37f 100644 +--- a/src/main/java/net/minecraft/server/Eula.java ++++ b/src/main/java/net/minecraft/server/Eula.java +@@ -47,7 +47,7 @@ public class Eula { + return var3; + } catch (Exception var6) { + if (file == this.file) { // Gale - YAPFA - global EULA file +- LOGGER.warn("Failed to load {}", (Object)this.file); ++ LOGGER.warn("Failed to load {}", this.file); // Leaf - remove convert + this.saveDefaults(); + } // Gale - YAPFA - global EULA file + return false; +diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +index 02b517288ef0794ccfd46802bf76fa56af21c524..e7fe8e2a7f5cb0c36679142f3743467efbed9c31 100644 +--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java ++++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java +@@ -602,7 +602,7 @@ public class ServerChunkCache extends ChunkSource { + + // Paper start - optimise chunk tick iteration + com.destroystokyo.paper.util.maplist.ReferenceList playersNearby +- = nearbyPlayers.getPlayers(chunkcoordintpair, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE); ++ = nearbyPlayers.getPlayers(chunk1.nearbyPlayersCoordinateKey, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE); // nearbyPlayers.getPlayers(chunkcoordintpair, io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.SPAWN_RANGE); // Polpot - cache coordinate key used for nearby players + if (playersNearby == null) { + continue; + } +diff --git a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +index f7e5e016a7028a9196e689e950805b0d5b31fe38..29d5848dec547245a1bafc5674b62b54ff7cd287 100644 +--- a/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/src/main/java/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -62,7 +62,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + protected final ShortList[] postProcessing; + protected volatile boolean unsaved; + private volatile boolean isLightCorrect; +- protected final ChunkPos chunkPos; public final long coordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key ++ protected final ChunkPos chunkPos; public final long coordinateKey; public final long nearbyPlayersCoordinateKey; public final int locX; public final int locZ; // Paper - cache coordinate key // Polpot - cache coordinate key used for nearby players + private long inhabitedTime; + /** @deprecated */ + @Nullable +@@ -136,7 +136,7 @@ public abstract class ChunkAccess implements BlockGetter, BiomeManager.NoiseBiom + } + // Paper end - rewrite light engine + this.locX = pos.x; this.locZ = pos.z; // Paper - reduce need for field lookups +- this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); // Paper - cache long key ++ this.chunkPos = pos; this.coordinateKey = ChunkPos.asLong(locX, locZ); this.nearbyPlayersCoordinateKey = io.papermc.paper.util.CoordinateUtils.getChunkKey(locX, locZ); // Paper - cache long key // Polpot - cache coordinate key used for nearby players + this.upgradeData = upgradeData; + this.levelHeightAccessor = heightLimitView; + this.sections = new LevelChunkSection[heightLimitView.getSectionsCount()]; From 475b0d9fe1cb689e3609db5eda9dd5648d36786d Mon Sep 17 00:00:00 2001 From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:30:27 +0800 Subject: [PATCH 16/16] Polpot make make egg and snowball can knockback player --- ...-egg-and-snowball-can-knockback-play.patch | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 patches/server/0056-Polpot-make-make-egg-and-snowball-can-knockback-play.patch diff --git a/patches/server/0056-Polpot-make-make-egg-and-snowball-can-knockback-play.patch b/patches/server/0056-Polpot-make-make-egg-and-snowball-can-knockback-play.patch new file mode 100644 index 00000000..2d5d5d44 --- /dev/null +++ b/patches/server/0056-Polpot-make-make-egg-and-snowball-can-knockback-play.patch @@ -0,0 +1,83 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: lilingfengdev <145678359+lilingfengdev@users.noreply.github.com> +Date: Thu, 18 Jan 2024 13:30:02 +0800 +Subject: [PATCH] Polpot make make egg and snowball can knockback player + + +diff --git a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +index 440d3d72d8b2dac14f83a83caa5ae9dbf3e979b6..26f1f1c588095d791b24b678c054cccfbbbf0706 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java +@@ -3,6 +3,7 @@ package net.minecraft.world.entity.projectile; + import net.minecraft.core.particles.ItemParticleOption; + import net.minecraft.core.particles.ParticleOptions; + import net.minecraft.core.particles.ParticleTypes; ++import net.minecraft.server.level.ServerPlayer; + import net.minecraft.world.entity.Entity; + import net.minecraft.world.entity.EntityType; + import net.minecraft.world.entity.LivingEntity; +@@ -13,6 +14,9 @@ import net.minecraft.world.item.Items; + import net.minecraft.world.level.Level; + import net.minecraft.world.phys.EntityHitResult; + import net.minecraft.world.phys.HitResult; ++// Leaf start ++import org.dreeam.leaf.LeafConfig; ++// Leaf end + + public class Snowball extends ThrowableItemProjectile { + public Snowball(EntityType type, Level world) { +@@ -55,6 +59,12 @@ public class Snowball extends ThrowableItemProjectile { + Entity entity = entityHitResult.getEntity(); + int i = entity.level().purpurConfig.snowballDamage >= 0 ? entity.level().purpurConfig.snowballDamage : entity instanceof Blaze ? 3 : 0; // Purpur + entity.hurt(this.damageSources().thrown(this, this.getOwner()), (float)i); ++ // Leaf - Polpot start - make snowball can knockback player ++ if (LeafConfig.snowballAndEggCanKnockback && entity instanceof ServerPlayer) { ++ entity.hurt(this.damageSources().thrown(this, this.getOwner()), 0.0000001F); ++ ((ServerPlayer) entity).knockback(0.4000000059604645D, this.getX() - entity.getX(), this.getZ() - entity.getZ(), this); ++ } ++ // Leaf - Polpot end + } + + // Purpur start - borrowed and modified code from ThrownPotion#onHitBlock and ThrownPotion#dowseFire +diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java +index b64ecadae45c2126b92963ac8d118dde76126ddd..8daa0d7ee597f94901948a732df5d5e9e1f670ad 100644 +--- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java ++++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java +@@ -15,6 +15,9 @@ import org.bukkit.entity.EntityType; + import org.bukkit.entity.Player; + import org.bukkit.event.player.PlayerEggThrowEvent; + // CraftBukkit end ++// Leaf start ++import org.dreeam.leaf.LeafConfig; ++// Leaf end + + public class ThrownEgg extends ThrowableItemProjectile { + +@@ -45,7 +48,14 @@ public class ThrownEgg extends ThrowableItemProjectile { + @Override + protected void onHitEntity(EntityHitResult entityHitResult) { + super.onHitEntity(entityHitResult); ++ Entity entity = entityHitResult.getEntity(); // Polpot - make egg can knockback player + entityHitResult.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); ++ // Leaf - Polpot start - make egg can knockback player ++ if (LeafConfig.snowballAndEggCanKnockback && entity instanceof ServerPlayer) { ++ entity.hurt(this.damageSources().thrown(this, this.getOwner()), 0.0000001F); ++ ((ServerPlayer) entity).knockback(0.4000000059604645D, this.getX() - entity.getX(), this.getZ() - entity.getZ(), this); ++ } ++ // Leaf - Polpot end - make egg can knockback player + } + + @Override +diff --git a/src/main/java/org/dreeam/leaf/LeafConfig.java b/src/main/java/org/dreeam/leaf/LeafConfig.java +index db12eb7e81307cc3fd4fe0602697e32e71289f7e..0439423d1c8b3a9f063595f6df542ce54aa646ab 100644 +--- a/src/main/java/org/dreeam/leaf/LeafConfig.java ++++ b/src/main/java/org/dreeam/leaf/LeafConfig.java +@@ -325,4 +325,8 @@ public class LeafConfig { + private static void getSkipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer() { + skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer = getBoolean("skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer",skipMapItemDataUpdatesIfMapDoesNotHaveCraftMapRenderer); + } ++ public static boolean snowballAndEggCanKnockback = false; ++ private static void SnowballAndEggCanKnockback() { ++ snowballAndEggCanKnockback = getBoolean("settings.snowball-egg-knockback-players", snowballAndEggCanKnockback,"Make snowball and egg can knock back player"); ++ } + }