diff --git a/divinemc-server/build.gradle.kts.patch b/divinemc-server/build.gradle.kts.patch index 4a6b74d..b1fc836 100644 --- a/divinemc-server/build.gradle.kts.patch +++ b/divinemc-server/build.gradle.kts.patch @@ -48,7 +48,7 @@ } } -@@ -142,10 +_,22 @@ +@@ -142,10 +_,23 @@ } dependencies { @@ -62,6 +62,7 @@ + } + implementation("net.objecthunter:exp4j:0.4.8") + implementation("org.agrona:agrona:2.0.1") ++ implementation("net.openhft:zero-allocation-hashing:0.16") + implementation("com.github.luben:zstd-jni:1.5.6-9") + implementation("org.lz4:lz4-java:1.8.0") + // DivineMC end diff --git a/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch b/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch index 7510812..ed5ce7b 100644 --- a/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch +++ b/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch @@ -1293,7 +1293,7 @@ index 6540b2d6a1062d883811ce240c49d30d1925b291..8055b8552b40160732953b15876dda79 } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 3c3e8e58cd2761ab2f0652e63f944a5c9a95dca8..3980e0dfb08a357384ea33670fb282a26a598f6e 100644 +index eb6286d34a68bf6eb57877a9cfc2be09615c7e83..f4b1f45f1dc86bd077860f088cdd6da78ecc6020 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -77,8 +77,6 @@ import net.minecraft.util.ProgressListener; @@ -1437,7 +1437,7 @@ index 3c3e8e58cd2761ab2f0652e63f944a5c9a95dca8..3980e0dfb08a357384ea33670fb282a2 } @VisibleForTesting -@@ -1330,17 +1296,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1356,17 +1322,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } // Paper end - log detailed entity tick information entity.setOldPosAndRot(); @@ -1455,7 +1455,7 @@ index 3c3e8e58cd2761ab2f0652e63f944a5c9a95dca8..3980e0dfb08a357384ea33670fb282a2 for (Entity entity1 : entity.getPassengers()) { this.tickPassenger(entity, entity1, isActive); // Paper - EAR 2 -@@ -1361,9 +1323,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1387,9 +1349,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe passengerEntity.setOldPosAndRot(); passengerEntity.tickCount++; passengerEntity.totalEntityAge++; // Paper - age-like counter for all entities @@ -1465,7 +1465,7 @@ index 3c3e8e58cd2761ab2f0652e63f944a5c9a95dca8..3980e0dfb08a357384ea33670fb282a2 // Paper start - EAR 2 if (isActive) { passengerEntity.rideTick(); -@@ -1375,7 +1334,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1401,7 +1360,6 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe ridingEntity.positionRider(passengerEntity); } // Paper end - EAR 2 diff --git a/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch b/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch index 77fdb97..2f524d7 100644 --- a/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch +++ b/divinemc-server/minecraft-patches/features/0006-Multithreaded-Tracker.patch @@ -208,10 +208,10 @@ index 0fb253aa55a24b56b17f524b3261c5b75c7d7e59..b6053158f5d9b6ad325ea075ab7c60f9 attributesToSync.clear(); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 9afd448ede87c9192dc576f66e08676a68b34d98..d4b44e0fdf113e66c45cc86221df20fe2d3bb9f0 100644 +index c229d69ce3d007e4cb57e559611b3fca7a03562f..ba5ca5213fafd60b2257409f334a7c6b28fe918a 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -2471,7 +2471,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2497,7 +2497,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @Override public LevelEntityGetter getEntities() { @@ -220,7 +220,7 @@ index 9afd448ede87c9192dc576f66e08676a68b34d98..d4b44e0fdf113e66c45cc86221df20fe return this.moonrise$getEntityLookup(); // Paper - rewrite chunk system } -@@ -2698,7 +2698,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2724,7 +2724,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } map.carriedByPlayers.remove(player); diff --git a/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch b/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch index 9284877..7d2a317 100644 --- a/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch +++ b/divinemc-server/minecraft-patches/features/0008-Parallel-world-ticking.patch @@ -435,7 +435,7 @@ index 0d889730641fd88d4da0c9116226a4dae385e846..f7a061ad623fa909389c60c1d5b4be84 try { org.purpurmc.purpur.PurpurConfig.init((java.io.File) options.valueOf("purpur-settings")); diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index d4b44e0fdf113e66c45cc86221df20fe2d3bb9f0..50ea7f4a0428cbc49fb13a31b1fa643e00ef9434 100644 +index ba5ca5213fafd60b2257409f334a7c6b28fe918a..52ba052fc1ff2a35786570c282a7de4e9dff99f5 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -182,7 +182,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @@ -463,7 +463,7 @@ index d4b44e0fdf113e66c45cc86221df20fe2d3bb9f0..50ea7f4a0428cbc49fb13a31b1fa643e this.preciseTime = this.serverLevelData.getDayTime(); // Purpur - Configurable daylight cycle } -@@ -1255,12 +1257,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1281,12 +1283,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (fluidState.is(fluid)) { fluidState.tick(this, pos, blockState); } @@ -481,7 +481,7 @@ index d4b44e0fdf113e66c45cc86221df20fe2d3bb9f0..50ea7f4a0428cbc49fb13a31b1fa643e } private void tickBlock(BlockPos pos, Block block) { -@@ -1268,12 +1270,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1294,12 +1296,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe if (blockState.is(block)) { blockState.tick(this, pos, this.random); } @@ -499,7 +499,7 @@ index d4b44e0fdf113e66c45cc86221df20fe2d3bb9f0..50ea7f4a0428cbc49fb13a31b1fa643e } // Paper start - log detailed entity tick information -@@ -1522,6 +1524,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1548,6 +1550,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } private void addPlayer(ServerPlayer player) { @@ -507,7 +507,7 @@ index d4b44e0fdf113e66c45cc86221df20fe2d3bb9f0..50ea7f4a0428cbc49fb13a31b1fa643e Entity entity = this.getEntities().get(player.getUUID()); if (entity != null) { LOGGER.warn("Force-added player with duplicate UUID {}", player.getUUID()); -@@ -1534,7 +1537,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -1560,7 +1563,13 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // CraftBukkit start private boolean addEntity(Entity entity, org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason spawnReason) { diff --git a/divinemc-server/minecraft-patches/features/0010-Chunk-System-Optimizations.patch b/divinemc-server/minecraft-patches/features/0010-Chunk-System-Optimizations.patch index 2002671..e27e2fb 100644 --- a/divinemc-server/minecraft-patches/features/0010-Chunk-System-Optimizations.patch +++ b/divinemc-server/minecraft-patches/features/0010-Chunk-System-Optimizations.patch @@ -1092,7 +1092,7 @@ index ab30af9cd58ff7310e05be87b08f42bacf69e11e..ae0e36d198ad8243920c8e8a55c0be49 } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 50ea7f4a0428cbc49fb13a31b1fa643e00ef9434..a70007d324e2169d1b2a1a11046500196b8b5660 100644 +index 52ba052fc1ff2a35786570c282a7de4e9dff99f5..9298bdca9a653622f3625190e875e7b6c5e40023 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -181,6 +181,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe @@ -1199,7 +1199,7 @@ index 50ea7f4a0428cbc49fb13a31b1fa643e00ef9434..a70007d324e2169d1b2a1a1104650019 } // Paper end - optimise random ticking -@@ -2531,30 +2533,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +@@ -2557,30 +2559,25 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe private boolean isPositionTickingWithEntitiesLoaded(long chunkPos) { // Paper start - rewrite chunk system diff --git a/divinemc-server/minecraft-patches/features/0039-Linear-region-file-format.patch b/divinemc-server/minecraft-patches/features/0039-Linear-region-file-format.patch index 93e5faa..bb7a6fd 100644 --- a/divinemc-server/minecraft-patches/features/0039-Linear-region-file-format.patch +++ b/divinemc-server/minecraft-patches/features/0039-Linear-region-file-format.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Linear region file format diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java -index a814512fcfb85312474ae2c2c21443843bf57831..c21e04c1d43797db221e4712fcc987a44eaa983c 100644 +index a814512fcfb85312474ae2c2c21443843bf57831..fdccc27c528b01b16a72e614ffd96523aa6f50d2 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java @@ -8,9 +8,9 @@ public interface ChunkSystemRegionFileStorage { @@ -13,15 +13,15 @@ index a814512fcfb85312474ae2c2c21443843bf57831..c21e04c1d43797db221e4712fcc987a4 public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ); - public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); -+ public org.stupidcraft.linearpaper.region.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // DivineMC - Linear region file format ++ public org.bxteam.divinemc.region.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // DivineMC - Linear region file format - public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; -+ public org.stupidcraft.linearpaper.region.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // DivineMC - Linear region file format ++ public org.bxteam.divinemc.region.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // DivineMC - Linear region file format public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( final int chunkX, final int chunkZ, final CompoundTag compound diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java -index 98fbc5c8044bd945d64569f13412a6e7e49a4e7f..57ae23fdd5a95e7670eb4dfaca0d290edfc1d25c 100644 +index 98fbc5c8044bd945d64569f13412a6e7e49a4e7f..34682217252cb98a70511a8cb25f077ec9f872b8 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java @@ -1260,7 +1260,7 @@ public final class MoonriseRegionFileIO { @@ -29,7 +29,7 @@ index 98fbc5c8044bd945d64569f13412a6e7e49a4e7f..57ae23fdd5a95e7670eb4dfaca0d290e // Paper start - flush regionfiles on save if (this.world.paperConfig().chunks.flushRegionsOnSave) { - final RegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); -+ final org.stupidcraft.linearpaper.region.IRegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // DivineMC - Linear region file format ++ final org.bxteam.divinemc.region.IRegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // DivineMC - Linear region file format if (regionFile != null) { regionFile.flush(); } // else: evicted from cache, which should have called flush @@ -38,12 +38,12 @@ index 98fbc5c8044bd945d64569f13412a6e7e49a4e7f..57ae23fdd5a95e7670eb4dfaca0d290e public static interface IORunnable { - public void run(final RegionFile regionFile) throws IOException; -+ public void run(final org.stupidcraft.linearpaper.region.IRegionFile regionFile) throws IOException; // DivineMC - Linear region file format ++ public void run(final org.bxteam.divinemc.region.IRegionFile regionFile) throws IOException; // DivineMC - Linear region file format } } diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java -index 51c126735ace8fdde89ad97b5cab62f244212db0..c466ce5c669e4b5ed835b13584a5ae5afe08fa11 100644 +index 51c126735ace8fdde89ad97b5cab62f244212db0..8713d00d767c9225a0823d2fdbb0b479005738d7 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java @@ -8,5 +8,5 @@ public interface ChunkSystemChunkBuffer { @@ -51,10 +51,10 @@ index 51c126735ace8fdde89ad97b5cab62f244212db0..c466ce5c669e4b5ed835b13584a5ae5a public void moonrise$setWriteOnClose(final boolean value); - public void moonrise$write(final RegionFile regionFile) throws IOException; -+ public void moonrise$write(final org.stupidcraft.linearpaper.region.IRegionFile regionFile) throws IOException; // DivineMC - Linear region file format ++ public void moonrise$write(final org.bxteam.divinemc.region.IRegionFile regionFile) throws IOException; // DivineMC - Linear region file format } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 9d6fa3bced0ba5ab3443bdf4ce306598a8e4a31f..34bc5d54de26608e64170bd902baa87da137dab6 100644 +index 26a4a279122bb03a83dd4a34e780d6a4567ba3a8..51b79f614417f231951e9ba05b29ff0044e081e7 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -947,10 +947,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop list1 = Lists.newArrayList(); - try (RegionFile regionFile = new RegionFile(regionStorageInfo, file.toPath(), path, true)) { -+ try (org.stupidcraft.linearpaper.region.IRegionFile regionFile = org.stupidcraft.linearpaper.region.IRegionFileFactory.getAbstractRegionFile(regionStorageInfo, file.toPath(), path, true)) { // DivineMC - Linear region file format ++ try (org.bxteam.divinemc.region.IRegionFile regionFile = org.bxteam.divinemc.region.RegionFileFactory.getAbstractRegionFile(regionStorageInfo, file.toPath(), path, true)) { // DivineMC - Linear region file format for (int i2 = 0; i2 < 32; i2++) { for (int i3 = 0; i3 < 32; i3++) { ChunkPos chunkPos = new ChunkPos(i2 + i, i3 + i1); @@ -106,7 +106,7 @@ index e0bcda2ddea0d6633445a7440fbf0d18e50a7653..2ab8e8d9db5ea390a1334c28b97ef6d0 protected abstract boolean tryProcessOnePosition(T chunkStorage, ChunkPos chunkPos, ResourceKey dimension); - private void onFileFinished(RegionFile regionFile) { -+ private void onFileFinished(org.stupidcraft.linearpaper.region.IRegionFile regionFile) { // DivineMC - Linear region file format ++ private void onFileFinished(org.bxteam.divinemc.region.IRegionFile regionFile) { // DivineMC - Linear region file format if (WorldUpgrader.this.recreateRegionFiles) { if (this.previousWriteFuture != null) { this.previousWriteFuture.join(); @@ -115,12 +115,12 @@ index e0bcda2ddea0d6633445a7440fbf0d18e50a7653..2ab8e8d9db5ea390a1334c28b97ef6d0 } - record FileToUpgrade(RegionFile file, List chunksToUpgrade) { -+ record FileToUpgrade(org.stupidcraft.linearpaper.region.IRegionFile file, List chunksToUpgrade) { // DivineMC - Linear region file format ++ record FileToUpgrade(org.bxteam.divinemc.region.IRegionFile file, List chunksToUpgrade) { // DivineMC - Linear region file format } class PoiUpgrader extends WorldUpgrader.SimpleRegionStorageUpgrader { diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java -index c72494e757a9dc50e053dbc873f7b30e83d5cb8c..5d29e3046485dcdda9356f6bfe6f6463e9b9bef4 100644 +index c72494e757a9dc50e053dbc873f7b30e83d5cb8c..2988084cf2d72557dd304dcb904d09441e79863d 100644 --- a/net/minecraft/world/level/chunk/storage/RegionFile.java +++ b/net/minecraft/world/level/chunk/storage/RegionFile.java @@ -22,7 +22,7 @@ import net.minecraft.util.profiling.jfr.JvmProfiler; @@ -128,7 +128,7 @@ index c72494e757a9dc50e053dbc873f7b30e83d5cb8c..5d29e3046485dcdda9356f6bfe6f6463 import org.slf4j.Logger; -public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system -+public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile, org.stupidcraft.linearpaper.region.IRegionFile { // Paper - rewrite chunk system // DivineMC - Linear region file format ++public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile, org.bxteam.divinemc.region.IRegionFile { // Paper - rewrite chunk system // DivineMC - Linear region file format private static final Logger LOGGER = LogUtils.getLogger(); public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails private static final int SECTOR_BYTES = 4096; @@ -137,12 +137,12 @@ index c72494e757a9dc50e053dbc873f7b30e83d5cb8c..5d29e3046485dcdda9356f6bfe6f6463 @Override - public final void moonrise$write(final RegionFile regionFile) throws IOException { -+ public final void moonrise$write(final org.stupidcraft.linearpaper.region.IRegionFile regionFile) throws IOException { // DivineMC - Linear region file format ++ public final void moonrise$write(final org.bxteam.divinemc.region.IRegionFile regionFile) throws IOException { // DivineMC - Linear region file format regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); } // Paper end - rewrite chunk system diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b4196cf25588 100644 +index 16cd10ab8de69ca3d29c84cf93715645322fd72a..9fd987078625bee796b381108ae7dbfb1f14d256 100644 --- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -18,7 +18,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise @@ -150,7 +150,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 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<>(); // DivineMC - Linear region file format ++ public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); // DivineMC - Linear region file format private final RegionStorageInfo info; private final Path folder; private final boolean sync; @@ -168,7 +168,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(); private static String getRegionFileName(final int chunkX, final int chunkZ) { + // DivineMC start - Linear region file format -+ if (org.bxteam.divinemc.DivineConfig.regionFormatTypeName == org.stupidcraft.linearpaper.region.EnumRegionFileExtension.LINEAR) { ++ if (org.bxteam.divinemc.DivineConfig.regionFormatTypeName == org.bxteam.divinemc.region.RegionFileFormat.LINEAR) { + return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".linear"; + } + // DivineMC end - Linear region file format @@ -181,17 +181,17 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 @Override - public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { -+ public synchronized final org.stupidcraft.linearpaper.region.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // DivineMC - Linear region file format ++ public synchronized final org.bxteam.divinemc.region.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // DivineMC - Linear region file format return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); } @Override - public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { -+ public synchronized final org.stupidcraft.linearpaper.region.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // DivineMC - Linear region file format ++ public synchronized final org.bxteam.divinemc.region.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // DivineMC - Linear region file format final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - RegionFile ret = this.regionCache.getAndMoveToFirst(key); -+ org.stupidcraft.linearpaper.region.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // DivineMC - Linear region file format ++ org.bxteam.divinemc.region.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // DivineMC - Linear region file format if (ret != null) { return ret; } @@ -200,7 +200,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 FileUtil.createDirectoriesSafe(this.folder); - ret = new RegionFile(this.info, regionPath, this.folder, this.sync); -+ ret = org.stupidcraft.linearpaper.region.IRegionFileFactory.getAbstractRegionFile(this.info, regionPath, this.folder, this.sync); // DivineMC - Linear region file format ++ ret = org.bxteam.divinemc.region.RegionFileFactory.getAbstractRegionFile(this.info, regionPath, this.folder, this.sync); // DivineMC - Linear region file format this.regionCache.putAndMoveToFirst(key, ret); @@ -209,7 +209,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 final ChunkPos pos = new ChunkPos(chunkX, chunkZ); - final RegionFile regionFile = this.getRegionFile(pos); -+ final org.stupidcraft.linearpaper.region.IRegionFile regionFile = this.getRegionFile(pos); // DivineMC - Linear region file format ++ final org.bxteam.divinemc.region.IRegionFile regionFile = this.getRegionFile(pos); // DivineMC - Linear region file format // note: not required to keep regionfile loaded after this call, as the write param takes a regionfile as input // (and, the regionfile parameter is unused for writing until the write call) @@ -223,7 +223,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 final ChunkPos pos = new ChunkPos(chunkX, chunkZ); if (writeData.result() == ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE) { - final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); -+ final org.stupidcraft.linearpaper.region.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // DivineMC - Linear region file format ++ final org.bxteam.divinemc.region.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // DivineMC - Linear region file format if (regionFile != null) { regionFile.clear(pos); } // else: didn't exist @@ -232,7 +232,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 final int chunkX, final int chunkZ ) throws IOException { - final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); -+ final org.stupidcraft.linearpaper.region.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // DivineMC - Linear region file format ++ final org.bxteam.divinemc.region.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // DivineMC - Linear region file format final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); @@ -241,7 +241,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 // Paper end - rewrite chunk system // Paper start - rewrite chunk system - public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { -+ public org.stupidcraft.linearpaper.region.IRegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { // DivineMC - Linear region file format ++ public org.bxteam.divinemc.region.IRegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { // DivineMC - Linear region file format return this.getRegionFile(chunkcoordintpair, false); } // Paper end - rewrite chunk system @@ -250,7 +250,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 } - @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit -+ @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private org.stupidcraft.linearpaper.region.IRegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit // DivineMC - Linear region file format ++ @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private org.bxteam.divinemc.region.IRegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit // DivineMC - Linear region file format // Paper start - rewrite chunk system if (existingOnly) { return this.moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z); @@ -259,7 +259,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 final long key = ChunkPos.asLong(chunkPos.x >> REGION_SHIFT, chunkPos.z >> REGION_SHIFT); - RegionFile ret = this.regionCache.getAndMoveToFirst(key); -+ org.stupidcraft.linearpaper.region.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // DivineMC - Linear region file format ++ org.bxteam.divinemc.region.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // DivineMC - Linear region file format if (ret != null) { return ret; } @@ -268,7 +268,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 FileUtil.createDirectoriesSafe(this.folder); - ret = new RegionFile(this.info, regionPath, this.folder, this.sync); -+ ret = org.stupidcraft.linearpaper.region.IRegionFileFactory.getAbstractRegionFile(this.info, regionPath, this.folder, this.sync); // DivineMC - Linear region file format ++ ret = org.bxteam.divinemc.region.RegionFileFactory.getAbstractRegionFile(this.info, regionPath, this.folder, this.sync); // DivineMC - Linear region file format this.regionCache.putAndMoveToFirst(key, ret); @@ -277,7 +277,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 } - private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { -+ private static CompoundTag readOversizedChunk(org.stupidcraft.linearpaper.region.IRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // DivineMC - Linear region file format ++ private static CompoundTag readOversizedChunk(org.bxteam.divinemc.region.IRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // DivineMC - Linear region file format synchronized (regionfile) { try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z); @@ -286,7 +286,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 public CompoundTag read(ChunkPos chunkPos) 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.stupidcraft.linearpaper.region.IRegionFile regionFile = this.getRegionFile(chunkPos, true); // DivineMC - Linear region file format ++ org.bxteam.divinemc.region.IRegionFile regionFile = this.getRegionFile(chunkPos, true); // DivineMC - Linear region file format if (regionFile == null) { return null; } @@ -295,7 +295,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 public void scanChunk(ChunkPos chunkPos, StreamTagVisitor visitor) 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.stupidcraft.linearpaper.region.IRegionFile regionFile = this.getRegionFile(chunkPos, true); // DivineMC - Linear region file format ++ org.bxteam.divinemc.region.IRegionFile regionFile = this.getRegionFile(chunkPos, true); // DivineMC - Linear region file format if (regionFile == null) { return; } @@ -304,7 +304,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 public void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException { // Paper - rewrite chunk system - public - RegionFile regionFile = this.getRegionFile(chunkPos, chunkData == null); // CraftBukkit // Paper - rewrite chunk system -+ org.stupidcraft.linearpaper.region.IRegionFile regionFile = this.getRegionFile(chunkPos, chunkData == null); // CraftBukkit // Paper - rewrite chunk system // DivineMC - Linear region file format ++ org.bxteam.divinemc.region.IRegionFile regionFile = this.getRegionFile(chunkPos, chunkData == null); // CraftBukkit // Paper - rewrite chunk system // DivineMC - Linear region file format // Paper start - rewrite chunk system if (regionFile == null) { // if the RegionFile doesn't exist, no point in deleting from it @@ -313,7 +313,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 synchronized (this) { final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); - for (final RegionFile regionFile : this.regionCache.values()) { -+ for (final org.stupidcraft.linearpaper.region.IRegionFile regionFile : this.regionCache.values()) { // DivineMC - Linear region file format ++ for (final org.bxteam.divinemc.region.IRegionFile regionFile : this.regionCache.values()) { // DivineMC - Linear region file format try { regionFile.close(); } catch (final IOException ex) { @@ -322,7 +322,7 @@ index 16cd10ab8de69ca3d29c84cf93715645322fd72a..1061d1480af0d4947d0f7f1b0028b419 synchronized (this) { final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); - for (final RegionFile regionFile : this.regionCache.values()) { -+ for (final org.stupidcraft.linearpaper.region.IRegionFile regionFile : this.regionCache.values()) { // DivineMC - Linear region file format ++ for (final org.bxteam.divinemc.region.IRegionFile regionFile : this.regionCache.values()) { // DivineMC - Linear region file format try { regionFile.flush(); } catch (final IOException ex) { diff --git a/divinemc-server/purpur-patches/features/0002-Add-missing-purpur-config-options.patch b/divinemc-server/purpur-patches/features/0002-Add-missing-purpur-config-options.patch index d2bafb0..edebe96 100644 --- a/divinemc-server/purpur-patches/features/0002-Add-missing-purpur-config-options.patch +++ b/divinemc-server/purpur-patches/features/0002-Add-missing-purpur-config-options.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Add missing purpur config options diff --git a/src/main/java/org/purpurmc/purpur/PurpurConfig.java b/src/main/java/org/purpurmc/purpur/PurpurConfig.java -index 168361632d4af27c2144864ade539ce7ea217217..48248e1a7eb9f63220dd619c1868dcca3150c345 100644 +index 554ae05a1a7f7dbe91455cb14b1d9a02f3b7d288..2c32f98e4f3ac2705ae6e95a8e8965b4b9d9a0d5 100644 --- a/src/main/java/org/purpurmc/purpur/PurpurConfig.java +++ b/src/main/java/org/purpurmc/purpur/PurpurConfig.java @@ -327,6 +327,7 @@ public class PurpurConfig { @@ -16,7 +16,7 @@ index 168361632d4af27c2144864ade539ce7ea217217..48248e1a7eb9f63220dd619c1868dcca public static boolean enderChestSixRows = false; public static boolean enderChestPermissionRows = false; public static boolean cryingObsidianValidForPortalFrame = false; -@@ -369,6 +370,7 @@ public class PurpurConfig { +@@ -370,6 +371,7 @@ public class PurpurConfig { case 1 -> 9; default -> 27; }); diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java index 18d7ec9..d4ad79c 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/DivineConfig.java @@ -10,13 +10,14 @@ import org.apache.logging.log4j.Logger; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.MemoryConfiguration; import org.bxteam.divinemc.entity.pathfinding.PathfindTaskRejectPolicy; +import org.bxteam.divinemc.region.LinearImplementation; import org.bxteam.divinemc.server.chunk.ChunkSystemAlgorithms; import org.bxteam.divinemc.server.chunk.ChunkTaskPriority; import org.jetbrains.annotations.Nullable; import org.simpleyaml.configuration.comments.CommentType; import org.simpleyaml.configuration.file.YamlFile; import org.simpleyaml.exceptions.InvalidConfigurationException; -import org.stupidcraft.linearpaper.region.EnumRegionFileExtension; +import org.bxteam.divinemc.region.RegionFileFormat; import java.io.File; import java.io.IOException; @@ -526,32 +527,50 @@ public class DivineConfig { if (asyncEntityTrackerQueueSize <= 0) asyncEntityTrackerQueueSize = asyncEntityTrackerMaxThreads * 384; } - public static EnumRegionFileExtension regionFormatTypeName = EnumRegionFileExtension.MCA; + public static RegionFileFormat regionFormatTypeName = RegionFileFormat.ANVIL; + public static LinearImplementation linearImplementation = LinearImplementation.V2; + public static int linearFlushMaxThreads = 4; + public static int linearFlushDelay = 100; + public static boolean linearUseVirtualThread = false; public static int linearCompressionLevel = 1; - public static int linearFlushFrequency = 5; private static void linearRegionFormat() { - regionFormatTypeName = EnumRegionFileExtension.fromName(getString("settings.linear-region-format.type", regionFormatTypeName.name(), + regionFormatTypeName = RegionFileFormat.fromName(getString("settings.linear-region-format.type", regionFormatTypeName.name(), "The type of region file format to use for storing chunk data.", "Valid values:", " - LINEAR: Linear region file format", - " - MCA: Anvil region file format (default)")); + " - ANVIL: Anvil region file format (default)")); + linearImplementation = LinearImplementation.valueOf(getString("settings.linear-region-format.implementation", linearImplementation.name(), + "The implementation of the linear region file format to use.", + "Valid values:", + " - V1: Basic and default linear implementation", + " - V2: Introduces a grid-based compression scheme for better data management and flexibility (default)")); + + linearFlushMaxThreads = getInt("settings.linear-region-format.flush-max-threads", linearFlushMaxThreads, + "The maximum number of threads to use for flushing linear region files.", + "If this value is less than or equal to 0, it will be set to the number of available processors + this value."); + linearFlushDelay = getInt("settings.linear-region-format.flush-delay", linearFlushDelay, + "The delay in milliseconds to wait before flushing next files."); + linearUseVirtualThread = getBoolean("settings.linear-region-format.use-virtual-thread", linearUseVirtualThread, + "Whether to use virtual threads for flushing."); linearCompressionLevel = getInt("settings.linear-region-format.compression-level", linearCompressionLevel, "The compression level to use for the linear region file format."); - linearFlushFrequency = getInt("settings.linear-region-format.flush-frequency", linearFlushFrequency, - "The frequency in seconds to flush the linear region file format."); setComment("settings.linear-region-format", - "The linear region file format is a custom region file format that is designed to be more efficient than the MCA format.", + "The linear region file format is a custom region file format that is designed to be more efficient than the ANVIL format.", "It uses uses ZSTD compression instead of ZLIB. This format saves about 50% of disk space.", "Read more information about linear region format at https://github.com/xymb-endcrystalme/LinearRegionFileFormatTools", "WARNING: If you are want to use this format, make sure to create backup of your world before switching to it, there is potential risk to lose chunk data."); - if (regionFormatTypeName == EnumRegionFileExtension.UNKNOWN) { - LOGGER.error("Unknown region file type: {}, falling back to MCA format.", regionFormatTypeName); - regionFormatTypeName = EnumRegionFileExtension.MCA; + if (regionFormatTypeName == RegionFileFormat.UNKNOWN) { + LOGGER.error("Unknown region file type: {}, falling back to ANVIL format.", regionFormatTypeName); + regionFormatTypeName = RegionFileFormat.ANVIL; } - if (linearCompressionLevel > 23 || linearCompressionLevel < 1) { + if (linearFlushMaxThreads <= 0) { + linearFlushMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + linearFlushMaxThreads, 1); + } + + if (linearCompressionLevel > 22 || linearCompressionLevel < 1) { LOGGER.warn("Invalid linear compression level: {}, resetting to default (1)", playerNearChunkDetectionRange); linearCompressionLevel = 1; } diff --git a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/IRegionFile.java b/divinemc-server/src/main/java/org/bxteam/divinemc/region/IRegionFile.java similarity index 96% rename from divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/IRegionFile.java rename to divinemc-server/src/main/java/org/bxteam/divinemc/region/IRegionFile.java index a09223d..94ecd4e 100644 --- a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/IRegionFile.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/region/IRegionFile.java @@ -1,4 +1,4 @@ -package org.stupidcraft.linearpaper.region; +package org.bxteam.divinemc.region; import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile; import net.minecraft.nbt.CompoundTag; diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/region/LinearImplementation.java b/divinemc-server/src/main/java/org/bxteam/divinemc/region/LinearImplementation.java new file mode 100644 index 0000000..e521593 --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/region/LinearImplementation.java @@ -0,0 +1,6 @@ +package org.bxteam.divinemc.region; + +public enum LinearImplementation { + V1, + V2 +} diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/region/LinearRegionFile.java b/divinemc-server/src/main/java/org/bxteam/divinemc/region/LinearRegionFile.java new file mode 100644 index 0000000..706f80f --- /dev/null +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/region/LinearRegionFile.java @@ -0,0 +1,659 @@ +package org.bxteam.divinemc.region; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +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.openhft.hashing.LongHashFunction; +import org.bxteam.divinemc.DivineConfig; +import org.bxteam.divinemc.spark.ThreadDumperRegistry; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +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.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +public class LinearRegionFile implements IRegionFile { + public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; + private static final Object SAVE_LOCK = new Object(); + private static final long SUPERBLOCK = 0xc3ff13183cca9d9aL; + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final byte V1_VERSION = 2; + private static final byte V2_VERSION = 3; + + private byte[][] bucketBuffers; + private final byte[][] chunkCompressedBuffers = new byte[1024][]; + private final int[] chunkUncompressedSizes = new int[1024]; + private final long[] chunkTimestamps = new long[1024]; + + private final Object markedToSaveLock = new Object(); + private boolean markedToSave = false; + + private final LZ4Compressor compressor; + private final LZ4FastDecompressor decompressor; + + private volatile boolean regionFileOpen = false; + private volatile boolean close = false; + + private final Path regionFilePath; + private final int gridSizeDefault = 8; + private int gridSize = gridSizeDefault; + private int bucketSize = 4; + private final int compressionLevel; + private final LinearImplementation linearImpl; + private final Thread schedulingThread; + + private static int activeSaveThreads = 0; + + public LinearRegionFile(Path path, LinearImplementation linearImplementation, int compressionLevel) { + this.regionFilePath = path; + this.linearImpl = linearImplementation; + this.compressionLevel = compressionLevel; + this.compressor = LZ4Factory.fastestInstance().fastCompressor(); + this.decompressor = LZ4Factory.fastestInstance().fastDecompressor(); + + Runnable flushCheck = () -> { + while (!close) { + synchronized (SAVE_LOCK) { + if (markedToSave && activeSaveThreads < DivineConfig.linearFlushMaxThreads) { + activeSaveThreads++; + Runnable flushOperation = () -> { + try { + flush(); + } catch (IOException ex) { + LOGGER.error("Region file {} flush failed", regionFilePath.toAbsolutePath(), ex); + } finally { + synchronized (SAVE_LOCK) { + activeSaveThreads--; + } + } + }; + Thread saveThread = DivineConfig.linearUseVirtualThread + ? Thread.ofVirtual().name("Linear IO - " + this.hashCode()).unstarted(flushOperation) + : Thread.ofPlatform().name("Linear IO - " + this.hashCode()).unstarted(flushOperation); + saveThread.setPriority(Thread.NORM_PRIORITY - 3); + saveThread.start(); + ThreadDumperRegistry.REGISTRY.add(saveThread.getName()); + } + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(DivineConfig.linearFlushDelay)); + } + }; + this.schedulingThread = DivineConfig.linearUseVirtualThread + ? Thread.ofVirtual().unstarted(flushCheck) + : Thread.ofPlatform().unstarted(flushCheck); + this.schedulingThread.setName("Linear IO Schedule - " + this.hashCode()); + ThreadDumperRegistry.REGISTRY.add(this.schedulingThread.getName()); + } + + private synchronized void openRegionFile() { + if (regionFileOpen) return; + regionFileOpen = true; + + File file = regionFilePath.toFile(); + if (!file.canRead()) { + schedulingThread.start(); + return; + } + + try { + byte[] fileContent = Files.readAllBytes(regionFilePath); + ByteBuffer byteBuffer = ByteBuffer.wrap(fileContent); + + long superBlock = byteBuffer.getLong(); + if (superBlock != SUPERBLOCK) { + throw new RuntimeException("Invalid superblock: " + superBlock + " file " + regionFilePath); + } + + byte version = byteBuffer.get(); + if (version == V1_VERSION) { + parseLinearV1(byteBuffer); + } else if (version == V2_VERSION) { + parseLinearV2(byteBuffer); + } else { + throw new RuntimeException("Invalid version: " + version + " file " + regionFilePath); + } + + schedulingThread.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to open region file " + regionFilePath, e); + } + } + + private void parseLinearV1(ByteBuffer buffer) throws IOException { + final int HEADER_SIZE = 32; + final int FOOTER_SIZE = 8; + buffer.position(buffer.position() + 11); + + int dataCount = buffer.getInt(); + long fileLength = regionFilePath.toFile().length(); + if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE) { + throw new IOException("Invalid file length: " + regionFilePath + " " + fileLength + " expected " + (HEADER_SIZE + dataCount + FOOTER_SIZE)); + } + + buffer.position(buffer.position() + 8); + + byte[] rawCompressed = new byte[dataCount]; + buffer.get(rawCompressed); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(rawCompressed); + ZstdInputStream zstdIn = new ZstdInputStream(bais)) { + ByteBuffer decompressedBuffer = ByteBuffer.wrap(zstdIn.readAllBytes()); + int[] starts = new int[1024]; + for (int i = 0; i < 1024; i++) { + starts[i] = decompressedBuffer.getInt(); + decompressedBuffer.getInt(); + } + + for (int i = 0; i < 1024; i++) { + if (starts[i] > 0) { + int size = starts[i]; + byte[] chunkData = new byte[size]; + decompressedBuffer.get(chunkData); + + int maxCompressedLength = compressor.maxCompressedLength(size); + byte[] compressed = new byte[maxCompressedLength]; + int compressedLength = compressor.compress(chunkData, 0, size, compressed, 0, maxCompressedLength); + byte[] finalCompressed = new byte[compressedLength]; + System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength); + + chunkCompressedBuffers[i] = finalCompressed; + chunkUncompressedSizes[i] = size; + chunkTimestamps[i] = currentTimestamp(); + } + } + } + } + + private void parseLinearV2(ByteBuffer buffer) throws IOException { + buffer.getLong(); + gridSize = buffer.get(); + if (!(gridSize == 1 || gridSize == 2 || gridSize == 4 || gridSize == 8 || gridSize == 16 || gridSize == 32)) { + throw new RuntimeException("Invalid grid size: " + gridSize + " file " + regionFilePath); + } + bucketSize = 32 / gridSize; + + buffer.getInt(); + buffer.getInt(); + + boolean[] chunkExistenceBitmap = deserializeExistenceBitmap(buffer); + + while (true) { + byte featureNameLength = buffer.get(); + if (featureNameLength == 0) break; + byte[] featureNameBytes = new byte[featureNameLength]; + buffer.get(featureNameBytes); + String featureName = new String(featureNameBytes); + int featureValue = buffer.getInt(); + } + + int bucketCount = gridSize * gridSize; + int[] bucketSizes = new int[bucketCount]; + byte[] bucketCompressionLevels = new byte[bucketCount]; + long[] bucketHashes = new long[bucketCount]; + + for (int i = 0; i < bucketCount; i++) { + bucketSizes[i] = buffer.getInt(); + bucketCompressionLevels[i] = buffer.get(); + bucketHashes[i] = buffer.getLong(); + } + + bucketBuffers = new byte[bucketCount][]; + for (int i = 0; i < bucketCount; i++) { + if (bucketSizes[i] > 0) { + bucketBuffers[i] = new byte[bucketSizes[i]]; + buffer.get(bucketBuffers[i]); + long rawHash = LongHashFunction.xx().hashBytes(bucketBuffers[i]); + if (rawHash != bucketHashes[i]) { + throw new IOException("Region file hash incorrect " + regionFilePath); + } + } + } + + long footerSuperBlock = buffer.getLong(); + if (footerSuperBlock != SUPERBLOCK) { + throw new IOException("Footer superblock invalid " + regionFilePath); + } + } + + private synchronized void markToSave() { + synchronized (markedToSaveLock) { + markedToSave = true; + } + } + + private synchronized boolean isMarkedToSave() { + synchronized (markedToSaveLock) { + if (markedToSave) { + markedToSave = false; + return true; + } + return false; + } + } + + @Override + public synchronized boolean doesChunkExist(ChunkPos pos) { + openRegionFile(); + return hasChunk(pos); + } + + @Override + public synchronized boolean hasChunk(ChunkPos pos) { + openRegionFile(); + openBucketForChunk(pos.x, pos.z); + int index = getChunkIndex(pos.x, pos.z); + return chunkUncompressedSizes[index] > 0; + } + + @Override + public synchronized void flush() throws IOException { + if (!isMarkedToSave()) return; + openRegionFile(); + if (linearImpl == LinearImplementation.V1) { + flushLinearV1(); + } else if (linearImpl == LinearImplementation.V2) { + flushLinearV2(); + } + } + + private void flushLinearV1() throws IOException { + long timestamp = currentTimestamp(); + short chunkCount = 0; + File tempFile = new File(regionFilePath.toString() + ".tmp"); + + try (FileOutputStream fos = new FileOutputStream(tempFile); + ByteArrayOutputStream zstdBAOS = new ByteArrayOutputStream(); + ZstdOutputStream zstdOut = new ZstdOutputStream(zstdBAOS, compressionLevel); + DataOutputStream zstdDataOut = new DataOutputStream(zstdOut); + DataOutputStream fileDataOut = new DataOutputStream(fos)) { + + fileDataOut.writeLong(SUPERBLOCK); + fileDataOut.writeByte(V1_VERSION); + fileDataOut.writeLong(timestamp); + fileDataOut.writeByte(compressionLevel); + + ArrayList decompressedChunks = new ArrayList<>(1024); + for (int i = 0; i < 1024; i++) { + if (chunkUncompressedSizes[i] != 0) { + chunkCount++; + byte[] decompressed = new byte[chunkUncompressedSizes[i]]; + decompressor.decompress(chunkCompressedBuffers[i], 0, decompressed, 0, chunkUncompressedSizes[i]); + decompressedChunks.add(decompressed); + } else { + decompressedChunks.add(null); + } + } + + for (int i = 0; i < 1024; i++) { + zstdDataOut.writeInt(chunkUncompressedSizes[i]); + zstdDataOut.writeInt((int) chunkTimestamps[i]); + } + + for (int i = 0; i < 1024; i++) { + if (decompressedChunks.get(i) != null) { + zstdDataOut.write(decompressedChunks.get(i)); + } + } + zstdDataOut.close(); + + fileDataOut.writeShort(chunkCount); + byte[] compressedZstdData = zstdBAOS.toByteArray(); + fileDataOut.writeInt(compressedZstdData.length); + fileDataOut.writeLong(0); + fileDataOut.write(compressedZstdData); + fileDataOut.writeLong(SUPERBLOCK); + + fileDataOut.flush(); + fos.getFD().sync(); + fos.getChannel().force(true); + } + Files.move(tempFile.toPath(), regionFilePath, StandardCopyOption.REPLACE_EXISTING); + } + + private void flushLinearV2() throws IOException { + long timestamp = currentTimestamp(); + File tempFile = new File(regionFilePath.toString() + ".tmp"); + + try (FileOutputStream fos = new FileOutputStream(tempFile); + DataOutputStream dataOut = new DataOutputStream(fos)) { + + dataOut.writeLong(SUPERBLOCK); + dataOut.writeByte(V2_VERSION); + dataOut.writeLong(timestamp); + dataOut.writeByte(gridSize); + + int[] regionCoords = parseRegionCoordinates(regionFilePath.getFileName().toString()); + dataOut.writeInt(regionCoords[0]); + dataOut.writeInt(regionCoords[1]); + + boolean[] chunkExistence = new boolean[1024]; + for (int i = 0; i < 1024; i++) { + chunkExistence[i] = (chunkUncompressedSizes[i] > 0); + } + writeExistenceBitmap(dataOut, chunkExistence); + + writeNBTFeatures(dataOut); + + byte[][] buckets = buildBuckets(); + + int bucketCount = gridSize * gridSize; + for (int i = 0; i < bucketCount; i++) { + dataOut.writeInt(buckets[i] != null ? buckets[i].length : 0); + dataOut.writeByte(compressionLevel); + long bucketHash = buckets[i] != null ? LongHashFunction.xx().hashBytes(buckets[i]) : 0; + dataOut.writeLong(bucketHash); + } + for (int i = 0; i < bucketCount; i++) { + if (buckets[i] != null) { + dataOut.write(buckets[i]); + } + } + dataOut.writeLong(SUPERBLOCK); + + dataOut.flush(); + fos.getFD().sync(); + fos.getChannel().force(true); + } + Files.move(tempFile.toPath(), regionFilePath, StandardCopyOption.REPLACE_EXISTING); + } + + private void writeNBTFeatures(DataOutputStream dataOut) throws IOException { + dataOut.writeByte(0); + } + + private byte[][] buildBuckets() throws IOException { + int bucketCount = gridSize * gridSize; + byte[][] buckets = new byte[bucketCount][]; + + for (int bx = 0; bx < gridSize; bx++) { + for (int bz = 0; bz < gridSize; bz++) { + int bucketIdx = bx * gridSize + bz; + if (bucketBuffers != null && bucketBuffers[bucketIdx] != null) { + buckets[bucketIdx] = bucketBuffers[bucketIdx]; + continue; + } + + try (ByteArrayOutputStream bucketBAOS = new ByteArrayOutputStream(); + ZstdOutputStream bucketZstdOut = new ZstdOutputStream(bucketBAOS, compressionLevel); + DataOutputStream bucketDataOut = new DataOutputStream(bucketZstdOut)) { + + boolean hasData = false; + int cellCount = 32 / gridSize; + for (int cx = 0; cx < cellCount; cx++) { + for (int cz = 0; cz < cellCount; cz++) { + int chunkIndex = (bx * cellCount + cx) + (bz * cellCount + cz) * 32; + if (chunkUncompressedSizes[chunkIndex] > 0) { + hasData = true; + byte[] chunkData = new byte[chunkUncompressedSizes[chunkIndex]]; + decompressor.decompress(chunkCompressedBuffers[chunkIndex], 0, chunkData, 0, chunkUncompressedSizes[chunkIndex]); + bucketDataOut.writeInt(chunkData.length + 8); + bucketDataOut.writeLong(chunkTimestamps[chunkIndex]); + bucketDataOut.write(chunkData); + } else { + bucketDataOut.writeInt(0); + bucketDataOut.writeLong(chunkTimestamps[chunkIndex]); + } + } + } + bucketDataOut.close(); + if (hasData) { + buckets[bucketIdx] = bucketBAOS.toByteArray(); + } + } + } + } + return buckets; + } + + private void openBucketForChunk(int chunkX, int chunkZ) { + int modX = Math.floorMod(chunkX, 32); + int modZ = Math.floorMod(chunkZ, 32); + int bucketIdx = chunkToBucketIndex(modX, modZ); + if (bucketBuffers == null || bucketBuffers[bucketIdx] == null) { + return; + } + + try (ByteArrayInputStream bucketBAIS = new ByteArrayInputStream(bucketBuffers[bucketIdx]); + ZstdInputStream bucketZstdIn = new ZstdInputStream(bucketBAIS)) { + + ByteBuffer bucketBuffer = ByteBuffer.wrap(bucketZstdIn.readAllBytes()); + int cellsPerBucket = 32 / gridSize; + int bx = modX / bucketSize, bz = modZ / bucketSize; + for (int cx = 0; cx < cellsPerBucket; cx++) { + for (int cz = 0; cz < cellsPerBucket; cz++) { + int chunkIndex = (bx * cellsPerBucket + cx) + (bz * cellsPerBucket + cz) * 32; + int chunkSize = bucketBuffer.getInt(); + long timestamp = bucketBuffer.getLong(); + chunkTimestamps[chunkIndex] = timestamp; + + if (chunkSize > 0) { + byte[] chunkData = new byte[chunkSize - 8]; + bucketBuffer.get(chunkData); + + int maxCompressedLength = compressor.maxCompressedLength(chunkData.length); + byte[] compressed = new byte[maxCompressedLength]; + int compressedLength = compressor.compress(chunkData, 0, chunkData.length, compressed, 0, maxCompressedLength); + byte[] finalCompressed = new byte[compressedLength]; + System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength); + + chunkCompressedBuffers[chunkIndex] = finalCompressed; + chunkUncompressedSizes[chunkIndex] = chunkData.length; + } + } + } + } catch (IOException ex) { + throw new RuntimeException("Region file corrupted: " + regionFilePath + " bucket: " + bucketIdx, ex); + } + bucketBuffers[bucketIdx] = null; + } + + @Override + public synchronized void write(ChunkPos pos, ByteBuffer buffer) { + openRegionFile(); + openBucketForChunk(pos.x, pos.z); + try { + byte[] rawData = toByteArray(new ByteArrayInputStream(buffer.array())); + int uncompressedSize = rawData.length; + if (uncompressedSize > MAX_CHUNK_SIZE) { + LOGGER.error("Chunk dupe attempt {}", regionFilePath); + clear(pos); + } else { + int maxCompressedLength = compressor.maxCompressedLength(uncompressedSize); + byte[] compressed = new byte[maxCompressedLength]; + int compressedLength = compressor.compress(rawData, 0, uncompressedSize, compressed, 0, maxCompressedLength); + byte[] finalCompressed = new byte[compressedLength]; + System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength); + + int index = getChunkIndex(pos.x, pos.z); + chunkCompressedBuffers[index] = finalCompressed; + chunkTimestamps[index] = currentTimestamp(); + chunkUncompressedSizes[index] = uncompressedSize; + } + } catch (IOException e) { + LOGGER.error("Chunk write IOException {} {}", e, regionFilePath); + } + markToSave(); + } + + @Override + public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { + openRegionFile(); + openBucketForChunk(pos.x, pos.z); + return new DataOutputStream(new BufferedOutputStream(new ChunkBuffer(pos))); + } + + @Override + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { + DataOutputStream out = getChunkDataOutputStream(pos); + return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( + data, + ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, + out, + regionFile -> { + try { + out.close(); + } catch (IOException e) { + LOGGER.error("Failed to close region file stream", e); + } + } + ); + } + + private class ChunkBuffer extends ByteArrayOutputStream { + private final ChunkPos pos; + public ChunkBuffer(ChunkPos pos) { + super(); + this.pos = pos; + } + @Override + public void close() { + 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 + @Override + public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) { + openRegionFile(); + openBucketForChunk(pos.x, pos.z); + int index = getChunkIndex(pos.x, pos.z); + if (chunkUncompressedSizes[index] != 0) { + byte[] decompressed = new byte[chunkUncompressedSizes[index]]; + decompressor.decompress(chunkCompressedBuffers[index], 0, decompressed, 0, chunkUncompressedSizes[index]); + return new DataInputStream(new ByteArrayInputStream(decompressed)); + } + return null; + } + + @Override + public synchronized void clear(ChunkPos pos) { + openRegionFile(); + openBucketForChunk(pos.x, pos.z); + int index = getChunkIndex(pos.x, pos.z); + chunkCompressedBuffers[index] = null; + chunkUncompressedSizes[index] = 0; + chunkTimestamps[index] = 0; + markToSave(); + } + + @Override + public synchronized void close() throws IOException { + openRegionFile(); + close = true; + try { + flush(); + } catch (IOException e) { + throw new IOException("Region flush IOException " + e + " " + regionFilePath, e); + } + } + + private static int getChunkIndex(int x, int z) { + return (x & 31) + ((z & 31) << 5); + } + + private static int currentTimestamp() { + return (int) (System.currentTimeMillis() / 1000L); + } + + @Override + public boolean recalculateHeader() { + return false; + } + + @Override + public void setOversized(int x, int z, boolean something) { + // stub + } + + @Override + public CompoundTag getOversizedData(int x, int z) throws IOException { + throw new IOException("getOversizedData is a stub " + regionFilePath); + } + + @Override + public boolean isOversized(int x, int z) { + return false; + } + + @Override + public Path getPath() { + return regionFilePath; + } + + private boolean[] deserializeExistenceBitmap(ByteBuffer buffer) { + boolean[] result = new boolean[1024]; + for (int i = 0; i < 128; i++) { + byte b = buffer.get(); + for (int j = 0; j < 8; j++) { + result[i * 8 + j] = ((b >> (7 - j)) & 1) == 1; + } + } + return result; + } + + private void writeExistenceBitmap(DataOutputStream out, boolean[] bitmap) throws IOException { + for (int i = 0; i < 128; i++) { + byte b = 0; + for (int j = 0; j < 8; j++) { + if (bitmap[i * 8 + j]) { + b |= (1 << (7 - j)); + } + } + out.writeByte(b); + } + } + + private int chunkToBucketIndex(int chunkX, int chunkZ) { + int bx = chunkX / bucketSize, bz = chunkZ / bucketSize; + return bx * gridSize + bz; + } + + private int[] parseRegionCoordinates(String fileName) { + int regionX = 0; + int regionZ = 0; + String[] parts = fileName.split("\\."); + if (parts.length >= 4) { + try { + regionX = Integer.parseInt(parts[1]); + regionZ = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + LOGGER.error("Failed to parse region coordinates from file name: {}", fileName, e); + } + } else { + LOGGER.warn("Unexpected file name format: {}", fileName); + } + return new int[]{regionX, regionZ}; + } +} diff --git a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/IRegionFileFactory.java b/divinemc-server/src/main/java/org/bxteam/divinemc/region/RegionFileFactory.java similarity index 54% rename from divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/IRegionFileFactory.java rename to divinemc-server/src/main/java/org/bxteam/divinemc/region/RegionFileFactory.java index 3252852..fbcdd68 100644 --- a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/IRegionFileFactory.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/region/RegionFileFactory.java @@ -1,4 +1,4 @@ -package org.stupidcraft.linearpaper.region; +package org.bxteam.divinemc.region; import net.minecraft.world.level.chunk.storage.RegionFile; import net.minecraft.world.level.chunk.storage.RegionFileVersion; @@ -10,34 +10,20 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.file.Path; -public class IRegionFileFactory { +public class RegionFileFactory { @Contract("_, _, _, _ -> new") public static @NotNull IRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { return getAbstractRegionFile(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); } @Contract("_, _, _, _, _ -> new") - public static @NotNull IRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync, boolean canRecalcHeader) throws IOException { - return getAbstractRegionFile(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync, canRecalcHeader); - } - - @Contract("_, _, _, _, _ -> new") - public static @NotNull IRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync) throws IOException { - return getAbstractRegionFile(storageKey, path, directory, compressionFormat, dsync, true); - } - - @Contract("_, _, _, _, _, _ -> new") - public static @NotNull IRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, @NotNull Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync, boolean canRecalcHeader) throws IOException { + public static @NotNull IRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, @NotNull Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync) throws IOException { final String fullFileName = path.getFileName().toString(); final String[] fullNameSplit = fullFileName.split("\\."); final String extensionName = fullNameSplit[fullNameSplit.length - 1]; - switch (EnumRegionFileExtension.fromExtension(extensionName)) { - case UNKNOWN -> { - return new RegionFile(storageKey, path, directory, compressionFormat, dsync); - } - + switch (RegionFileFormat.fromExtension(extensionName)) { case LINEAR -> { - return new LinearRegionFile(path, DivineConfig.linearCompressionLevel); + return new LinearRegionFile(path, DivineConfig.linearImplementation, DivineConfig.linearCompressionLevel); } default -> { diff --git a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/EnumRegionFileExtension.java b/divinemc-server/src/main/java/org/bxteam/divinemc/region/RegionFileFormat.java similarity index 53% rename from divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/EnumRegionFileExtension.java rename to divinemc-server/src/main/java/org/bxteam/divinemc/region/RegionFileFormat.java index 2440bd3..30a54e5 100644 --- a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/EnumRegionFileExtension.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/region/RegionFileFormat.java @@ -1,46 +1,46 @@ -package org.stupidcraft.linearpaper.region; +package org.bxteam.divinemc.region; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; - import java.util.Locale; -public enum EnumRegionFileExtension { +public enum RegionFileFormat { LINEAR(".linear"), - MCA(".mca"), + ANVIL(".mca"), UNKNOWN(null); - private final String extensionName; + private final String extension; - EnumRegionFileExtension(String extensionName) { - this.extensionName = extensionName; + RegionFileFormat(String extension) { + this.extension = extension; } public String getExtensionName() { - return this.extensionName; + return this.extension; } @Contract(pure = true) - public static EnumRegionFileExtension fromName(@NotNull String name) { + public static RegionFileFormat fromName(@NotNull String name) { switch (name.toUpperCase(Locale.ROOT)) { - case "MCA" -> { - return MCA; + case "MCA", "ANVIL" -> { + return ANVIL; } case "LINEAR" -> { return LINEAR; } + default -> { - return UNKNOWN; + throw new IllegalArgumentException("Unknown region file format: " + name); } } } @Contract(pure = true) - public static EnumRegionFileExtension fromExtension(@NotNull String name) { + public static RegionFileFormat fromExtension(@NotNull String name) { switch (name.toLowerCase()) { - case "mca" -> { - return MCA; + case "mca", "anvil" -> { + return ANVIL; } case "linear" -> { diff --git a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/LinearRegionFile.java b/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/LinearRegionFile.java deleted file mode 100644 index 2ec67ce..0000000 --- a/divinemc-server/src/main/java/org/stupidcraft/linearpaper/region/LinearRegionFile.java +++ /dev/null @@ -1,308 +0,0 @@ -package org.stupidcraft.linearpaper.region; - -import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; -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 org.bxteam.divinemc.DivineConfig; -import org.slf4j.Logger; - -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -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.TimeUnit; -import javax.annotation.Nullable; - -public class LinearRegionFile implements IRegionFile { - 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 final byte[][] buffer = new byte[1024][]; - private final int[] bufferUncompressedSize = new int[1024]; - private final int[] chunkTimestamps = new int[1024]; - private final LZ4Compressor compressor; - private final LZ4FastDecompressor decompressor; - private final int compressionLevel; - public boolean closed = false; - public Path path; - private volatile long lastFlushed = System.nanoTime(); - - 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); - - 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); - - 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); - } - - 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; - } - } - } - } - } - - private static int getChunkIndex(int x, int z) { - return (x & 31) + ((z & 31) << 5); - } - - private static int getTimestamp() { - return (int) (System.currentTimeMillis() / 1000L); - } - - public void flush() throws IOException { - flushWrapper(); - } - - 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]); - zstdDataStream.writeInt(this.chunkTimestamps[i]); - } - 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); - } - Files.move(tempFile.toPath(), this.path, StandardCopyOption.REPLACE_EXISTING); - this.lastFlushed = System.nanoTime(); - } - - 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); - } - - if ((System.nanoTime() - this.lastFlushed) >= TimeUnit.NANOSECONDS.toSeconds(DivineConfig.linearFlushFrequency)) { - this.flushWrapper(); - } - } - - public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { - return new DataOutputStream(new BufferedOutputStream(new ChunkBuffer(pos))); - } - - @Override - public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { - final DataOutputStream out = this.getChunkDataOutputStream(pos); - - return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( - data, ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, - out, regionFile -> out.close() - ); - } - - 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 void clear(ChunkPos pos) { - int i = getChunkIndex(pos.x, pos.z); - this.buffer[i] = null; - this.bufferUncompressedSize[i] = 0; - this.chunkTimestamps[i] = getTimestamp(); - this.flushWrapper(); - } - - public Path getPath() { - return this.path; - } - - 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(); - } - - 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; - } - - private class ChunkBuffer extends ByteArrayOutputStream { - private final ChunkPos pos; - - public ChunkBuffer(ChunkPos chunkcoordintpair) { - super(); - this.pos = chunkcoordintpair; - } - - public void close() { - ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); - LinearRegionFile.this.write(this.pos, bytebuffer); - } - } -}