diff --git a/lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch b/lophine-server/minecraft-patches/features/0006-Anonymous-Object-Configurable-region-format-framewor.patch similarity index 73% rename from lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch rename to lophine-server/minecraft-patches/features/0006-Anonymous-Object-Configurable-region-format-framewor.patch index a9779ca..e3bc7a4 100644 --- a/lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch +++ b/lophine-server/minecraft-patches/features/0006-Anonymous-Object-Configurable-region-format-framewor.patch @@ -1,11 +1,14 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Helvetica Volubi Date: Sun, 28 Jun 2026 18:54:50 +0800 -Subject: [PATCH] Configurable region format framework +Subject: [PATCH] Anonymous Object: Configurable region format framework +As requested by the original author, this patch will be anonymous and hide the original commit address. +This patch is used with permission from the original author. +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java -index a814512fcfb85312474ae2c2c21443843bf57831..2e084a5b28cbe4737f48c25e10af589213525362 100644 +index a814512fcfb85312474ae2c2c21443843bf57831..9d69a022872e06b320bee46329cd8889f74fbbaa 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 +16,15 @@ index a814512fcfb85312474ae2c2c21443843bf57831..2e084a5b28cbe4737f48c25e10af5892 public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ); - public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); -+ public abomination.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // Luminol - Configurable region file format ++ public io.anonymous.anonymous.data.RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // Anonymous - Configurable region file format - public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; -+ public abomination.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // Luminol - Configurable region file format ++ public io.anonymous.anonymous.data.RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // Anonymous - Configurable 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 3bb9b58ee97464687e348e23b99159226415c267..fc264727141c3897e6c06819b9713d4f6395baf0 100644 +index 3bb9b58ee97464687e348e23b99159226415c267..4c3ad63f17bb4a04f982b36ad3f53d3c0f58bdb5 100644 --- a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java @@ -1273,7 +1273,7 @@ public final class MoonriseRegionFileIO { @@ -29,7 +32,7 @@ index 3bb9b58ee97464687e348e23b99159226415c267..fc264727141c3897e6c06819b9713d4f // 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 abomination.IRegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // Luminol - Add configurable region file ++ final io.anonymous.anonymous.data.RegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // Anonymous - Configurable region file format if (regionFile != null) { regionFile.flush(); } // else: evicted from cache, which should have called flush @@ -38,12 +41,12 @@ index 3bb9b58ee97464687e348e23b99159226415c267..fc264727141c3897e6c06819b9713d4f public static interface IORunnable { - public void run(final RegionFile regionFile) throws IOException; -+ public void run(final abomination.IRegionFile regionFile) throws IOException; // Luminol - Configurable region file format ++ public void run(final io.anonymous.anonymous.data.RegionFile regionFile) throws IOException; // Anonymous - Configurable 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..c7d4d944eb198ac53a3eeae717a25c7d5815c8c1 100644 +index 51c126735ace8fdde89ad97b5cab62f244212db0..ea2f45964fb8c617f90938597c230b50d83744aa 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 +54,10 @@ index 51c126735ace8fdde89ad97b5cab62f244212db0..c7d4d944eb198ac53a3eeae717a25c7d public void moonrise$setWriteOnClose(final boolean value); - public void moonrise$write(final RegionFile regionFile) throws IOException; -+ public void moonrise$write(final abomination.IRegionFile regionFile) throws IOException; // Luminol - Configurable region file format ++ public void moonrise$write(final io.anonymous.anonymous.data.RegionFile regionFile) throws IOException; // Anonymous - Configurable region file format } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index a49c46eb7df50fc3c5587a2c3668ab6edc887107..83b9aab16124ddcc416f7b4cd95084e1c64fe5b7 100644 +index a49c46eb7df50fc3c5587a2c3668ab6edc887107..c74f76b46259204b33b69ad08bb03c81b8b36961 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -986,10 +986,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop chunksToUpgrade) { -+public record FileToUpgrade(abomination.IRegionFile file, List chunksToUpgrade) { // Luminol - Configurable region file format ++public record FileToUpgrade(io.anonymous.anonymous.data.RegionFile file, List chunksToUpgrade) { // Anonymous - Configurable region file format } diff --git a/net/minecraft/util/worldupdate/RegionStorageUpgrader.java b/net/minecraft/util/worldupdate/RegionStorageUpgrader.java -index b228cd6c54e8c8998923ab332882d99092769c67..140ef4959fa2fe5b9cd251955c93683e8e69d5ba 100644 +index b228cd6c54e8c8998923ab332882d99092769c67..2dd3d0afd8f58e5e68a361dd38c95da06b4d133b 100644 --- a/net/minecraft/util/worldupdate/RegionStorageUpgrader.java +++ b/net/minecraft/util/worldupdate/RegionStorageUpgrader.java @@ -36,7 +36,7 @@ import org.slf4j.Logger; @@ -90,7 +93,7 @@ index b228cd6c54e8c8998923ab332882d99092769c67..140ef4959fa2fe5b9cd251955c93683e private static final Logger LOGGER = LogUtils.getLogger(); private static final String NEW_DIRECTORY_PREFIX = "new_"; - private static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mca$"); -+ private static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\." + me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getArgument() + "$"); // Luminol - Configurable region file format ++ private static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\." + me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getArgument() + "$"); // Anonymous - Configurable region file format private final DataFixer dataFixer; private final UpgradeProgress upgradeProgress; private final String type; @@ -99,8 +102,8 @@ index b228cd6c54e8c8998923ab332882d99092769c67..140ef4959fa2fe5b9cd251955c93683e List chunkPositions = Lists.newArrayList(); - try (RegionFile regionSource = new RegionFile(info, regionFile.toPath(), regionFolder, true)) { -+ var regionFileInfo = new me.earthme.luminol.utils.RegionCreatorInfo(info, regionFile.toPath(), regionFolder, true); // Luminol - Configurable region file format -+ try (abomination.IRegionFile regionSource = me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getCreator().create(regionFileInfo)) { // Luminol - Configurable region file format ++ var regionFileInfo = new io.anonymous.anonymous.utils.RegionCreatorInfo(info, regionFile.toPath(), regionFolder, true); // Anonymous - Configurable region file format ++ try (io.anonymous.anonymous.data.RegionFile regionSource = me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getCreator().newFile(regionFileInfo)) { // Anonymous - Configurable region file format for (int x = 0; x < 32; x++) { for (int z = 0; z < 32; z++) { ChunkPos pos = new ChunkPos(x + xOffset, z + zOffset); @@ -109,12 +112,12 @@ index b228cd6c54e8c8998923ab332882d99092769c67..140ef4959fa2fe5b9cd251955c93683e } - private void onFileFinished(final RegionFile regionFile) { -+ private void onFileFinished(final abomination.IRegionFile regionFile) { // Luminol - Configurable region file format ++ private void onFileFinished(final io.anonymous.anonymous.data.RegionFile regionFile) { // Anonymous - Configurable region file format if (this.recreateRegionFiles) { if (this.previousWriteFuture != null) { this.previousWriteFuture.join(); diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java -index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93addb767392 100644 +index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..821924f85a507589a039832be8657e1dc3c0ce0c 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.world.level.ChunkPos; @@ -122,7 +125,7 @@ index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93ad 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 , abomination.IRegionFile{ // Paper - rewrite chunk system // Luminol - Configurable region file format ++public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile , io.anonymous.anonymous.data.RegionFile{ // Paper - rewrite chunk system // Anonymous - Configurable 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; @@ -131,7 +134,7 @@ index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93ad } - boolean recalculateHeader() throws IOException { -+ public boolean recalculateHeader() throws IOException { // Luminol - Configurable region file format ++ public boolean recalculateHeader() throws IOException { // Anonymous - Configurable region file format if (!this.canRecalcHeader) { return false; } @@ -140,7 +143,7 @@ index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93ad } - protected synchronized void write(final ChunkPos pos, final ByteBuffer data) throws IOException { -+ public synchronized void write(final ChunkPos pos, final ByteBuffer data) throws IOException { // Luminol - Configurable region file format ++ public synchronized void write(final ChunkPos pos, final ByteBuffer data) throws IOException { // Anonymous - Configurable region file format int offsetIndex = getOffsetIndex(pos); int offset = this.offsets.get(offsetIndex); int sectorNumber = getSectorNumber(offset); @@ -149,7 +152,7 @@ index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93ad @Override - public final void moonrise$write(final RegionFile regionFile) throws IOException { -+ public final void moonrise$write(final abomination.IRegionFile regionFile) throws IOException { // Luminol - Configurable region file format ++ public final void moonrise$write(final io.anonymous.anonymous.data.RegionFile regionFile) throws IOException { // Anonymous - Configurable region file format regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); } // Paper end - rewrite chunk system @@ -158,12 +161,12 @@ index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93ad } - synchronized boolean isOversized(int x, int z) { -+ public synchronized boolean isOversized(int x, int z) { // Luminol - Configurable region file format ++ public synchronized boolean isOversized(int x, int z) { // Anonymous - Configurable region file format 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 { // Luminol - Configurable region file format ++ public synchronized void setOversized(int x, int z, boolean oversized) throws IOException { // Anonymous - Configurable region file format final int offset = getChunkIndex(x, z); boolean previous = this.oversized[offset] == 1; this.oversized[offset] = (byte) (oversized ? 1 : 0); @@ -172,12 +175,12 @@ index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..d3350dee1d6d5bc15dcdfba3611f93ad } - synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException { -+ public synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException { // Luminol - Configurable region file format ++ public synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException { // Anonymous - Configurable region file format Path file = getOversizedFile(x, z); try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new java.util.zip.InflaterInputStream(Files.newInputStream(file))))) { return net.minecraft.nbt.NbtIo.read((java.io.DataInput) out); diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977ebfb18517 100644 +index 63b40c420030d935bb81a219fb33defe0946e21f..e0e19f091f6b85d7788259cfcc53cc3db848b68e 100644 --- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -19,7 +19,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise @@ -185,7 +188,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e public static final String ANVIL_EXTENSION = ".mca"; private static final int MAX_CACHE_SIZE = 256; - private final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); -+ private final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); // Luminol - Configurable region file format ++ private final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); // Anonymous - Configurable region file format private final RegionStorageInfo info; private final Path folder; private final boolean sync; @@ -194,7 +197,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e public static ChunkPos getRegionFileCoordinates(Path file) { String fileName = file.getFileName().toString(); - if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { -+ if (!fileName.startsWith("r.") || !fileName.endsWith(getExtensionName())) { // Luminol - Configurable region file format ++ if (!fileName.startsWith("r.") || !fileName.endsWith(getExtensionName())) { // Anonymous - Configurable region file format return null; } @@ -203,32 +206,32 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e 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) { - return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca"; -+ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + getExtensionName(); // Luminol - Configurable region file format ++ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + getExtensionName(); // Anonymous - Configurable region file format } -+ // Luminol start - Configurable region file format -+ public static abomination.IRegionFile createNew(RegionStorageInfo info, Path filePath, Path folder, boolean sync) throws IOException{ -+ final me.earthme.luminol.enums.EnumRegionFormat regionFormat = me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat; ++ // Anonymous start - Configurable region file format ++ public static io.anonymous.anonymous.data.RegionFile createNew(RegionStorageInfo info, Path filePath, Path folder, boolean sync) throws IOException{ ++ final io.anonymous.anonymous.enums.EnumRegionFormat regionFormat = me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat; + final String fullFileName = filePath.getFileName().toString(); + final String[] fullNameSplit = fullFileName.split("\\."); + final String extensionName = fullNameSplit[fullNameSplit.length - 1]; + + if (!regionFormat.getArgument().equalsIgnoreCase(extensionName)) { -+ // delayed crash ++ // raise a delayed crash + io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { -+ new RuntimeException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); ++ throw new RuntimeException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); + }); + + + throw new IOException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); + } + -+ return regionFormat.getCreator().create(new me.earthme.luminol.utils.RegionCreatorInfo(info, filePath, folder, sync)); ++ return regionFormat.getCreator().newFile(new io.anonymous.anonymous.utils.RegionCreatorInfo(info, filePath, folder, sync)); + } + + public static String getExtensionName() { + return "." + me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getArgument(); + } -+ // Luminol end ++ // Anonymous end private boolean doesRegionFilePossiblyExist(final long position) { synchronized (this.nonExistingRegionFiles) { @@ -237,17 +240,17 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e @Override - public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { -+ public synchronized final abomination.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // Luminol - Configurable region file format ++ public synchronized final io.anonymous.anonymous.data.RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // Anonymous - Configurable region file format return this.regionCache.getAndMoveToFirst(ChunkPos.pack(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); } @Override - public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { -+ public synchronized final abomination.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // Luminol - Configurable region file format ++ public synchronized final io.anonymous.anonymous.data.RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // Anonymous - Configurable region file format final long key = ChunkPos.pack(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - RegionFile ret = this.regionCache.getAndMoveToFirst(key); -+ abomination.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Luminol - Configurable region file format ++ io.anonymous.anonymous.data.RegionFile ret = this.regionCache.getAndMoveToFirst(key); // Anonymous - Configurable region file format if (ret != null) { return ret; } @@ -256,7 +259,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e FileUtil.createDirectoriesSafe(this.folder); - ret = new RegionFile(this.info, regionPath, this.folder, this.sync); -+ ret = this.createNew(this.info, regionPath, this.folder, this.sync); // Luminol - Configurable region file format ++ ret = this.createNew(this.info, regionPath, this.folder, this.sync); // Anonymous - Configurable region file format this.regionCache.putAndMoveToFirst(key, ret); @@ -265,7 +268,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e final ChunkPos pos = new ChunkPos(chunkX, chunkZ); - final RegionFile regionFile = this.getRegionFile(pos); -+ final abomination.IRegionFile regionFile = this.getRegionFile(pos); // Luminol - Configurable region file format ++ final io.anonymous.anonymous.data.RegionFile regionFile = this.getRegionFile(pos); // Anonymous - Configurable 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) @@ -274,7 +277,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e 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 abomination.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Luminol - Configurable region file format ++ final io.anonymous.anonymous.data.RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Anonymous - Configurable region file format if (regionFile != null) { regionFile.clear(pos); } // else: didn't exist @@ -283,7 +286,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e final int chunkX, final int chunkZ ) throws IOException { - final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); -+ final abomination.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Luminol - Configurable region file format ++ final io.anonymous.anonymous.data.RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Anonymous - Configurable region file format final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); @@ -292,7 +295,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e final ChunkPos pos = new ChunkPos(chunkX, chunkZ); final ChunkPos headerChunkPos = SerializableChunkData.getChunkCoordinate(ret); - final RegionFile regionFile = this.getRegionFile(pos); -+ final abomination.IRegionFile regionFile = this.getRegionFile(pos); // Luminol - Configurable region file format ++ final io.anonymous.anonymous.data.RegionFile regionFile = this.getRegionFile(pos); // Anonymous - Configurable region file format if (regionFile.getRecalculateCount() != readData.recalculateCount()) { return null; @@ -301,7 +304,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e // Paper end - rewrite chunk system // Paper start - rewrite chunk system - public RegionFile getRegionFile(ChunkPos pos) throws IOException { -+ public abomination.IRegionFile getRegionFile(ChunkPos pos) throws IOException { // Luminol - Configurable region file format ++ public io.anonymous.anonymous.data.RegionFile getRegionFile(ChunkPos pos) throws IOException { // Anonymous - Configurable region file format return this.getRegionFile(pos, false); } // Paper end - rewrite chunk system @@ -310,7 +313,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e } - @org.jetbrains.annotations.Contract("_, false -> !null") private @Nullable RegionFile getRegionFile(final ChunkPos pos, boolean existingOnly) throws IOException { // CraftBukkit -+ @org.jetbrains.annotations.Contract("_, false -> !null") private abomination.@Nullable IRegionFile getRegionFile(final ChunkPos pos, boolean existingOnly) throws IOException { // CraftBukkit // Luminol - Configurable region file format ++ @org.jetbrains.annotations.Contract("_, false -> !null") private io.anonymous.anonymous.data.RegionFile getRegionFile(final ChunkPos pos, boolean existingOnly) throws IOException { // CraftBukkit // Anonymous - Configurable region file format // Paper start - rewrite chunk system if (existingOnly) { return this.moonrise$getRegionFileIfExists(pos.x(), pos.z()); @@ -319,7 +322,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e final long key = ChunkPos.pack(pos.x() >> REGION_SHIFT, pos.z() >> REGION_SHIFT); - RegionFile ret = this.regionCache.getAndMoveToFirst(key); -+ abomination.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Luminol - Configurable region file format ++ io.anonymous.anonymous.data.RegionFile ret = this.regionCache.getAndMoveToFirst(key); // Anonymous - Configurable region file format if (ret != null) { return ret; } @@ -328,7 +331,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e FileUtil.createDirectoriesSafe(this.folder); - ret = new RegionFile(this.info, regionPath, this.folder, this.sync); -+ ret = this.createNew(this.info, regionPath, this.folder, this.sync); // Luminol - Configurable region file format ++ ret = this.createNew(this.info, regionPath, this.folder, this.sync); // Anonymous - Configurable region file format this.regionCache.putAndMoveToFirst(key, ret); @@ -337,7 +340,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e } - private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { -+ private static CompoundTag readOversizedChunk(abomination.IRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // Luminol - Configurable region file format ++ private static CompoundTag readOversizedChunk(io.anonymous.anonymous.data.RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // Anonymous - Configurable region file format synchronized (regionfile) { try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x(), chunkCoordinate.z()); @@ -346,7 +349,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e public @Nullable CompoundTag read(final ChunkPos pos) throws IOException { // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing - RegionFile region = this.getRegionFile(pos, true); -+ abomination.IRegionFile region = this.getRegionFile(pos, true); // Luminol - Configurable region file format ++ io.anonymous.anonymous.data.RegionFile region = this.getRegionFile(pos, true); // Anonymous - Configurable region file format if (region == null) { return null; } @@ -355,7 +358,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e public void scanChunk(final ChunkPos pos, final StreamTagVisitor scanner) throws IOException { // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing - RegionFile region = this.getRegionFile(pos, true); -+ abomination.IRegionFile region = this.getRegionFile(pos, true); // Luminol - Configurable region file format ++ io.anonymous.anonymous.data.RegionFile region = this.getRegionFile(pos, true); // Anonymous - Configurable region file format if (region == null) { return; } @@ -364,7 +367,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e public void write(final ChunkPos pos, final @Nullable CompoundTag value) throws IOException { if (!SharedConstants.DEBUG_DONT_SAVE_WORLD) { - RegionFile region = this.getRegionFile(pos, value == null); // CraftBukkit // Paper - rewrite chunk system -+ abomination.IRegionFile region = this.getRegionFile(pos, value == null); // CraftBukkit // Paper - rewrite chunk system // Luminol - Configurable region file format ++ io.anonymous.anonymous.data.RegionFile region = this.getRegionFile(pos, value == null); // CraftBukkit // Paper - rewrite chunk system // Anonymous - Configurable region file format // Paper start - rewrite chunk system if (region == null) { // if the RegionFile doesn't exist, no point in deleting from it @@ -373,7 +376,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e synchronized (this) { final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); - for (final RegionFile regionFile : this.regionCache.values()) { -+ for (final abomination.IRegionFile regionFile : this.regionCache.values()) { // Luminol - Configurable region file format ++ for (final io.anonymous.anonymous.data.RegionFile regionFile : this.regionCache.values()) { // Anonymous - Configurable region file format try { regionFile.close(); } catch (final IOException ex) { @@ -382,7 +385,7 @@ index 63b40c420030d935bb81a219fb33defe0946e21f..2e67e69862cbf5d804de50193442977e synchronized (this) { final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); - for (final RegionFile regionFile : this.regionCache.values()) { -+ for (final abomination.IRegionFile regionFile : this.regionCache.values()) { // Luminol - Configurable region file format ++ for (final io.anonymous.anonymous.data.RegionFile regionFile : this.regionCache.values()) { // Anonymous - Configurable region file format try { regionFile.flush(); } catch (final IOException ex) { diff --git a/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch b/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch index 220ce28..ef10887 100644 --- a/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch +++ b/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch @@ -6,7 +6,7 @@ Subject: [PATCH] Force disable builtin spark plugin The spark passed down from paper has some memory leaking issue, so we fully removed it from the code to prevent that memory leaking issue. diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 83b9aab16124ddcc416f7b4cd95084e1c64fe5b7..7683c12d48d23dd43bce3d3b5633029c596930a5 100644 +index c74f76b46259204b33b69ad08bb03c81b8b36961..cc8364891a5d2196e8809479db984b67b5bcb079 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -672,8 +672,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop localPlayers = new CopyOnWriteArrayList<>(); diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index f0a3fdaf7e9d342b472b3db42f5b59b0f6691b49..ac49bcc53951c025de0160fd8afd319b1f3d1347 100644 +index e43aa6daa8d4465764348be5b82be5f163ef0819..f4274e844018992bba7c32d2146198efc034631e 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -1973,6 +1973,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= limit) { diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index f76e40f8bbea076ad620f7525d4d8666603a5175..77bbc527b017c2ac42ebe4638457caf32868e3f6 100644 +index 5bc6baf2a13b78f65e5851a22ca79dbcaed957b2..803faa6b80a42d16687ea8b109973938d02f4a36 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -1909,7 +1909,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop out.close() ); } diff --git a/lophine-server/src/main/java/io/anonymous/anonymous/data/BufferedLinearRegionFile.java b/lophine-server/src/main/java/io/anonymous/anonymous/data/BufferedLinearRegionFile.java new file mode 100644 index 0000000..7d5d506 --- /dev/null +++ b/lophine-server/src/main/java/io/anonymous/anonymous/data/BufferedLinearRegionFile.java @@ -0,0 +1,1794 @@ +package io.anonymous.anonymous.data; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdInputStream; +import net.jpountz.lz4.LZ4Compressor; +import net.jpountz.lz4.LZ4Factory; +import net.jpountz.lz4.LZ4FastDecompressor; +import net.jpountz.xxhash.XXHash32; +import net.jpountz.xxhash.XXHashFactory; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.openhft.hashing.LongHashFunction; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.io.*; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Lock hierarchy (always acquire top to bottom, never the reverse): + *
    + *
  1. {@code syncLock} — serializes master file syncs against close
  2. + *
  3. {@code Bucket.lock} — per-bucket lazy-load guard
  4. + *
  5. {@code masterFileLock} — master file read / append / replace
  6. + *
  7. {@code regionObjectLock} — in-memory sector table + swap file channel
  8. + *
+ * The atomic flags (closed / synced / beingSynced / lastWritten), the bucket epochs + * and the swap space counters (currentAcquiredIndex / liveBytes, mutated only under + * the region write lock) are lock-free readable and may be touched while holding any + * (or no) lock. + *

+ * The swap file is fully transient: it is deleted at open, opened with + * DELETE_ON_CLOSE and never parsed back after a crash, so it carries no header and + * is never fsynced. Durability comes exclusively from the master file, whose v3 + * on-disk format is unchanged. + */ +public class BufferedLinearRegionFile implements io.anonymous.anonymous.data.RegionFile { + private static final double SWAP_FILE_AUTO_COMPACT_PERCENT = 3.0 / 5.0; // 60 % + private static final long SWAP_FILE_AUTO_COMPACT_SIZE = 1024 * 1024; // 1 MiB + + // master file WAL appends leave the replaced bucket records behind as garbage; once + // it piles up past this threshold the next sync compacts via a full tmp-file rewrite + private static final double MASTER_FILE_AUTO_COMPACT_PERCENT = SWAP_FILE_AUTO_COMPACT_PERCENT; + private static final long MASTER_FILE_AUTO_COMPACT_SIZE = SWAP_FILE_AUTO_COMPACT_SIZE; + + private static final int XXHASH32_SEED = 0x0721; // ~(∠・ω< )⌒★ + + private static final long MASTER_FILE_SUPER_BLOCK = -0x200812250269L; + private static final byte MASTER_FILE_VERSION = 0x02; // ver 2.0 + private static final byte MASTER_FILE_VERSION_BUCKET = 0x03; // ver 3.0 + + private static final long LINEAR_FILE_SUPER_BLOCK = 0xc3ff13183cca9d9aL; + + private static final int BUCKET_SHIFT = 6; + private static final int BUCKET_SIZE = 1 << BUCKET_SHIFT; + private static final int BUCKET_COUNT = 1024 / BUCKET_SIZE; + + private static final long MAX_SIZE_PER_CHUNK = RegionFile.MAX_CHUNK_SIZE; + + // on-disk sector layout in the swap file: + // dataLen(int) + timestamp(long) + xxhash32(int) + lz4(chunk data) + // the 16 meta bytes stay OUTSIDE the compression so neither the write nor the read + // path needs a full-size intermediate copy of the chunk data; dataLen doubles as + // the lz4 original size, so no separate length prefix is needed + private static final int SECTOR_META_SIZE = Integer.BYTES + Long.BYTES + Integer.BYTES; + + // all three are stateless and thread-safe + private static final LZ4Compressor LZ4_COMPRESSOR = LZ4Factory.fastestInstance().fastCompressor(); + private static final LZ4FastDecompressor LZ4_DECOMPRESSOR = LZ4Factory.fastestInstance().fastDecompressor(); + private static final XXHash32 XX_HASH_32 = XXHashFactory.fastestInstance().hash32(); + + // per-thread staging buffer for the hot chunk read/write paths: the compressed + // bytes never outlive the single pread/pwrite they are staged for, so they never + // need to escape into a fresh allocation + private static final int SCRATCH_RETAIN_LIMIT = 2 * 1024 * 1024; // 2 MiB + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(() -> ByteBuffer.allocate(64 * 1024)); + + private static final StandardOpenOption[] SWAP_FILE_CHANNEL_OPTIONS = new StandardOpenOption[]{ + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.READ, + StandardOpenOption.DELETE_ON_CLOSE + }; + + private static final class Bucket { + private final Object lock = new Object(); + + private final AtomicLong writeEpoch = new AtomicLong(); + private final AtomicLong syncedEpoch = new AtomicLong(); + private volatile boolean loaded = false; + } + + private final Bucket[] buckets = new Bucket[BUCKET_COUNT]; + + private final Path masterFilePath; + private final Path swapFilePath; + + // outermost lock: serializes syncToMasterFile() against closeInternal(), so the + // swap channel can never be torn down while a sync is still reading from it + private final Object syncLock = new Object(); + + private final ReadWriteLock regionObjectLock = new ReentrantReadWriteLock(); + private Sector[] sectors = new Sector[1024]; + private FileChannel swapFileChannel; + + // mutated only under regionObjectLock's write lock; volatile so flushInternal() + // can run its garbage estimate without taking any lock at all + private volatile long currentAcquiredIndex; + private volatile long liveBytes; + + private final byte compressionLevel; + private final MasterFileParser masterFileParser = new MasterFileParser(); + + // managed by VarHandles following + private boolean closed = false; + private boolean beingSynced = false; + private boolean synced = false; + private long lastWritten = System.nanoTime(); + + private static final VarHandle CLOSED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "closed", boolean.class); + private static final VarHandle SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "synced", boolean.class); + private static final VarHandle BEING_SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "beingSynced", boolean.class); + private static final VarHandle LAST_WRITTEN_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "lastWritten", long.class); + + private final BufferedLinearRegionFileFlusher flusher; + + public BufferedLinearRegionFile(Path masterFilePath, int compressionLevel, @NotNull BufferedLinearRegionFileFlusher flusher) throws IOException { + this.masterFilePath = masterFilePath; + this.swapFilePath = Path.of(this.masterFilePath.toString() + ".swp"); + + Validate.inclusiveBetween(1, 22, compressionLevel); + + for (int i = 0; i < this.buckets.length; i++) { + this.buckets[i] = new Bucket(); + } + + this.compressionLevel = (byte) compressionLevel; + + this.cleanUpSwapFile(); + this.initSwapFile(); + this.tryLoadOldBlinearMasterFileData(); + + // resume WAL mode directly from an existing v3 master file: without this, the + // first sync after every open rewrites the whole file even for one dirty chunk + this.masterFileParser.tryEnterWalMode(this.masterFilePath); + + this.flusher = flusher; + this.flusher.addFile(this); + } + + private static @NotNull ByteBuffer acquireScratch(int capacity) { + ByteBuffer buf = SCRATCH.get(); + + if (buf.capacity() < capacity) { + buf = ByteBuffer.allocate(Math.max(capacity, buf.capacity() << 1)); + + // oversized one-off requests get a throwaway buffer instead of pinning + // megabytes onto every io thread forever + if (buf.capacity() <= SCRATCH_RETAIN_LIMIT) { + SCRATCH.set(buf); + } + } + + buf.clear(); + return buf; + } + + private static void writeFullyAt(FileChannel channel, @NonNull ByteBuffer buf, long startOffset) throws IOException { + long offset = startOffset; + while (buf.hasRemaining()) { + offset += channel.write(buf, offset); + } + } + + private static void readFullyAt(FileChannel channel, @NonNull ByteBuffer buf, long startOffset) throws IOException { + long offset = startOffset; + while (buf.hasRemaining()) { + final int read = channel.read(buf, offset); + if (read < 0) throw new EOFException("Unexpected EOF at offset " + offset); + offset += read; + } + } + + private static void transferFully(FileChannel source, long sourceOffset, long count, FileChannel target, long targetOffset) throws IOException { + target.position(targetOffset); + + long transferred = 0; + while (transferred < count) { + transferred += source.transferTo(sourceOffset + transferred, count - transferred, target); + } + } + + // replaces target with source, deleting source if both attempts fail + private static void atomicReplace(Path source, Path target) throws IOException { + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (Throwable e) { + // atomic move might be unsupported on some file systems, so give it an attempt to retry without atomic move + try { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } catch (Throwable ex) { + e.addSuppressed(ex); + + // delete file that failed to replace + Files.deleteIfExists(source); + + throw new IOException("Failed to replace " + target + "!", e); + } + } + } + + private void cleanUpSwapFile() throws IOException { + Files.deleteIfExists(this.swapFilePath); + + // a crash between compact's tmp creation and the atomic replace leaves a stale + // .swp.tmp behind, which would make every future compact fail at CREATE_NEW + Files.deleteIfExists(Path.of(this.swapFilePath + ".tmp")); + } + + private void ensureBucketLoaded(int chunkIndex) throws IOException { + final int bucketIndex = chunkIndex >> BUCKET_SHIFT; + final Bucket bucket = this.buckets[bucketIndex]; + + if (bucket.loaded) { // volatile fast path + return; + } + + // bucket lock -> master read lock -> swap write lock + synchronized (bucket.lock) { + if (bucket.loaded) { + return; + } + + this.masterFileParser.loadBucketsFor(this.masterFilePath, bucketIndex); + bucket.loaded = true; + } + } + + // used by the legacy parsers: their data goes through the write path directly, + // so the bucket must be flagged loaded first to avoid a recursive lazy-load + private void markBucketLoaded(int chunkIndex) { + final Bucket bucket = this.buckets[chunkIndex >> BUCKET_SHIFT]; + + synchronized (bucket.lock) { + bucket.loaded = true; + } + } + + private void markBucketDirty(int chunkIndex) { + this.buckets[chunkIndex >> BUCKET_SHIFT].writeEpoch.incrementAndGet(); + } + + private long getBucketWriteEpoch(int bucketIndex) { + return this.buckets[bucketIndex].writeEpoch.get(); + } + + private void markBucketSynced(int bucketIndex, long syncedEpoch) { + this.buckets[bucketIndex].syncedEpoch.accumulateAndGet(syncedEpoch, Math::max); + } + + private boolean isBucketDirty(int bucketIndex) { + final Bucket bucket = this.buckets[bucketIndex]; + + return bucket.writeEpoch.get() != bucket.syncedEpoch.get(); + } + + public boolean markAsBeingSynced() { + return BEING_SYNCED_HANDLE.compareAndSet(this, false, true); + } + + + public long getLastWritten() { + return (long) LAST_WRITTEN_HANDLE.getVolatile(this); + } + + public boolean shouldSync() { + return !((boolean) SYNCED_HANDLE.getVolatile(this)); + } + + public boolean softReadLock() { + // not done close logic yet + return this.regionObjectLock.readLock().tryLock(); + } + + public void releaseReadLock() { + this.regionObjectLock.readLock().unlock(); + } + + public boolean isClosedRaw() { + return (boolean) CLOSED_HANDLE.getVolatile(this); + } + + public boolean isClosed() { + this.regionObjectLock.readLock().lock(); + try { + return (boolean) CLOSED_HANDLE.getVolatile(this); + } finally { + this.regionObjectLock.readLock().unlock(); + } + } + + public void syncIfNeeded() throws IOException { + try { + this.syncToMasterFile(false, false); + } finally { + BEING_SYNCED_HANDLE.setVolatile(this, false); // mark as not being synced + } + } + + private void syncToMasterFile(boolean forceSync, boolean forceCompact) throws IOException { + // serialized against close: the swap channel cannot go away under a running sync + synchronized (this.syncLock) { + // skip if closed already + if (this.isClosedRaw()) { + return; + } + + // fast skip when there is nothing to sync; writers flip the flag back + // via markAsToSync() which triggers the next round + if (!SYNCED_HANDLE.compareAndSet(this, false, true) && !forceSync) { + return; + } + + try { + this.masterFileParser.sync(this.masterFilePath, forceCompact); + } catch (Throwable e) { + // set back + SYNCED_HANDLE.setVolatile(this, false); + + throw new IOException("Failed to sync to master file!", e); + } + } + } + + private void tryLoadOldBlinearMasterFileData() throws IOException { + this.masterFileParser.tryParseMainFileOld(this.masterFilePath); + } + + private void initSwapFile() throws IOException { + this.swapFileChannel = FileChannel.open( + this.swapFilePath, + SWAP_FILE_CHANNEL_OPTIONS + ); + + // fill default sectors; the swap file has no header, data starts at offset 0 + for (int i = 0; i < 1024; i++) { + this.sectors[i] = new Sector(i, 0, 0); + } + + this.currentAcquiredIndex = 0; + this.liveBytes = 0; + } + + private void recalculateCounters() { + long acquired = 0; + long live = 0; + + for (Sector sector : this.sectors) { + // cleared sectors keep their stale extent for in-place reuse (see store()), + // so their extent MUST still be counted into the acquired watermark here, + // or later appends could land inside it and get overwritten by a reuse + acquired = Math.max(acquired, sector.offset + sector.length); + + if (sector.hasData()) { + live += sector.length; + } + } + + this.currentAcquiredIndex = acquired; + this.liveBytes = live; + } + + private void flushInternal() throws IOException { + if (this.isClosedRaw()) { + return; + } + + // lock-free garbage estimate from the incrementally maintained counters: + // this runs after EVERY chunk write, so no write lock, no O(1024) sector + // scan and no Files.exists() stat on the hot path + final long live = this.liveBytes; + final long spare = this.currentAcquiredIndex - live; + final boolean compactRequested = spare > SWAP_FILE_AUTO_COMPACT_SIZE && (double) spare > (double) live * SWAP_FILE_AUTO_COMPACT_PERCENT; + + // try auto compact to clean the garbage area + if (compactRequested) { + this.regionObjectLock.writeLock().lock(); + try { + if (!this.isClosedRaw()) { + // recheck with the authoritative values under the lock + final long liveNow = this.liveBytes; + final long spareNow = this.currentAcquiredIndex - liveNow; + + if (spareNow > SWAP_FILE_AUTO_COMPACT_SIZE && (double) spareNow > (double) liveNow * SWAP_FILE_AUTO_COMPACT_PERCENT) { + // do compact + this.compactSwapFile(); + } + } + } finally { + this.regionObjectLock.writeLock().unlock(); + } + } + + // create the master file eagerly on the very first write of a fresh region; + // afterwards this is a single volatile read per chunk write. + // prevent syncing after compact because it could be time costing sometimes + if (!compactRequested && !this.masterFileParser.masterFileExists()) { + this.syncToMasterFile(false, false); + } + } + + private void closeInternal() throws IOException { + synchronized (this.syncLock) { + if (this.isClosedRaw()) { + // already closed (possibly by a compact disaster path): just make sure + // both channels are really gone — close is idempotent + this.regionObjectLock.writeLock().lock(); + try { + this.swapFileChannel.close(); + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + this.masterFileParser.close(); + return; + } + + // final sync so no buffered data is lost; holding syncLock also guarantees no + // concurrent flusher sync is still running when we tear down below. + // if this throws we deliberately stay open: the flusher can retry the sync + // later, and the not-yet-synced swap data is not dropped on the floor + this.syncToMasterFile(true, true); + + IOException failure = null; + + this.regionObjectLock.writeLock().lock(); + try { + this.markClosed(); + + this.swapFileChannel.close(); + } catch (IOException e) { + failure = e; + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + try { + // acquired after the region lock is fully released, never inside it (lock hierarchy) + this.masterFileParser.close(); + } catch (IOException e) { + if (failure == null) failure = e; else failure.addSuppressed(e); + } + + if (failure != null) { + throw failure; + } + } + } + + private void markClosed() { + // lenient CAS: the disaster path of compactSwapFile() may have closed us already + if (CLOSED_HANDLE.compareAndSet(this, false, true)) { + this.flusher.removeFile(this); + } + } + + private void compactSwapFile() throws IOException { + final Sector[] newSectorsToBeReplaced = new Sector[this.sectors.length]; + + for (int i = 0; i < this.sectors.length; i++) { + final Sector old = this.sectors[i]; + + if (old.hasData()) { + newSectorsToBeReplaced[i] = old; + continue; + } + + // note: + // we reset length to 0 and this would make length <= newLength(which is >= 0) is always true. + // so that the following write operation wouldn't override the data of other sectors + // see the store method in Sector class + newSectorsToBeReplaced[i] = new Sector(i, 0, 0); + } + + long newAcquiredIndex; + + final Path targetTemp = Path.of(this.swapFilePath + ".tmp"); + + try (FileChannel tempChannel = FileChannel.open( + targetTemp, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE, + StandardOpenOption.READ, + StandardOpenOption.TRUNCATE_EXISTING + )) { + long offsetPointer = 0; + + for (Sector sector : newSectorsToBeReplaced) { + // skip cleared or no data-contained sectors + if (!sector.hasData()) { + continue; + } + + // transfer to target + transferFully(this.swapFileChannel, sector.offset, sector.length, tempChannel, offsetPointer); + + // recalculate the offset and length + final Sector newRecalculated = new Sector(sector.index, offsetPointer, sector.length); + newRecalculated.hasData = true; + + offsetPointer += sector.length; + newSectorsToBeReplaced[sector.index] = newRecalculated; // update sector infos + } + + // note: NO force here — the swap file is transient and never read back + // after a crash, so fsyncing it (twice, like before) was pure overhead + + newAcquiredIndex = offsetPointer; + } catch (Throwable ex) { + // recalculate counters + this.recalculateCounters(); + // delete the target temp file + Files.deleteIfExists(targetTemp); + // fast-fail + // note: we don't block new write operations here as this is recoverable + throw new IOException("Failed to compact swap file!", ex); + } + + this.swapFileChannel.close(); + + // replace swap file + try { + atomicReplace(targetTemp, this.swapFilePath); + } catch (Throwable e) { + // recalculate counters + this.recalculateCounters(); + // reopen closed channel + this.reopenSwapFileChannel(); + // fast-fail + this.markClosed(); // prevent new writing & sync operations + throw new IOException("Failed to replace original swap file!", e); + } + + try { + // reopen file channel + this.reopenSwapFileChannel(); + + // replace with recalculated infos: after a compact everything left is live + this.sectors = newSectorsToBeReplaced; + this.currentAcquiredIndex = newAcquiredIndex; + this.liveBytes = newAcquiredIndex; + } catch (Throwable ex) { + // we are totally failed here, + // directly mark as closed as the swap file is already replaced, and we failed to update the + // data which is still in the memory + // + // which means we might write any data into any incorrect indexed sectors which will blow the whole data + this.markClosed(); + throw new IOException(ex); + } + } + + private void reopenSwapFileChannel() throws IOException { + if (this.swapFileChannel.isOpen()) { + this.swapFileChannel.close(); + } + + this.swapFileChannel = FileChannel.open( + this.swapFilePath, + SWAP_FILE_CHANNEL_OPTIONS + ); + } + + // stores an already lz4-encoded sector (meta + compressed data), typically staged + // in the thread-local scratch: nothing here escapes to the heap + private void storeSector(int index, @NotNull ByteBuffer encoded, boolean skipSync) throws IOException { + this.regionObjectLock.writeLock().lock(); + try { + this.sectors[index].store(encoded, this.swapFileChannel); + + if (!skipSync) { + this.markBucketDirty(index); + } + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + if (skipSync) { + return; + } + + this.markAsToSync(); + } + + // section = dataLen(int) + timestamp(long) + xxhash32(int) + data, i.e. the exact + // per-chunk byte layout persisted inside master file bucket records + private void writeSection(int index, @NotNull ByteBuffer section, boolean skipSync) throws IOException { + if (section.remaining() < SECTOR_META_SIZE) { + throw new IOException("Truncated chunk section (" + section.remaining() + " bytes) for index " + index); + } + + final int dataLen = section.remaining() - SECTOR_META_SIZE; + final ByteBuffer out = acquireScratch(SECTOR_META_SIZE + LZ4_COMPRESSOR.maxCompressedLength(dataLen)); + + // meta bytes are carried over verbatim, only the chunk data goes through lz4 + final int oldLimit = section.limit(); + section.limit(section.position() + SECTOR_META_SIZE); + out.put(section); + section.limit(oldLimit); + + LZ4_COMPRESSOR.compress(section, out); + out.flip(); + + this.storeSector(index, out, skipSync); + } + + private void clearChunkData(int index) throws IOException { + this.ensureBucketLoaded(index); + + this.regionObjectLock.writeLock().lock(); + try { + this.sectors[index].clear(); + this.markBucketDirty(index); + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + this.markAsToSync(); + } + + private void markAsToSync() { + SYNCED_HANDLE.setVolatile(this, false); // mark as unsynced + LAST_WRITTEN_HANDLE.setVolatile(this, System.nanoTime()); // update last written time + } + + private static int getChunkIndex(int x, int z) { + return (x & 31) + ((z & 31) << 5); + } + + private boolean hasData(int index) throws IOException { + this.ensureBucketLoaded(index); + + this.regionObjectLock.readLock().lock(); + try { + return this.sectors[index].hasData(); + } finally { + this.regionObjectLock.readLock().unlock(); + } + } + + private void writeChunk(int x, int z, @NotNull ByteBuffer data) throws IOException { + final int chunkIndex = getChunkIndex(x, z); + + this.ensureBucketLoaded(chunkIndex); + + final int dataLen = data.remaining(); + + if (dataLen > MAX_SIZE_PER_CHUNK) { + throw new RegionFileStorage.RegionFileSizeException("Writing too large chunk, limit : " + MAX_SIZE_PER_CHUNK + " but got : " + dataLen); + } + + // absolute-offset hash: no position save/restore dance needed + final int xxHash32OfData = XX_HASH_32.hash(data, data.position(), dataLen, XXHASH32_SEED); + + // meta + compressed data are built directly in the reusable scratch: no + // full-size intermediate copy of the chunk data, no allocation that escapes + final ByteBuffer out = acquireScratch(SECTOR_META_SIZE + LZ4_COMPRESSOR.maxCompressedLength(dataLen)); + + out.putInt(dataLen); // uncompressed length, doubles as the lz4 original size + out.putLong(System.currentTimeMillis()); // timestamp + out.putInt(xxHash32OfData); // xxHash32 of the original data + LZ4_COMPRESSOR.compress(data, out); + out.flip(); + + this.storeSector(chunkIndex, out, false); + } + + private @Nullable ByteBuffer readChunk(int x, int z) throws IOException { + final int chunkIndex = getChunkIndex(x, z); + + this.ensureBucketLoaded(chunkIndex); + + final ByteBuffer stage; + + this.regionObjectLock.readLock().lock(); + try { + final Sector sector = this.sectors[chunkIndex]; + + if (!sector.hasData()) { + return null; + } + + // only the pread runs under the lock, staged into the reusable scratch + stage = acquireScratch((int) sector.length); + stage.limit((int) sector.length); + + readFullyAt(this.swapFileChannel, stage, sector.offset); + } finally { + this.regionObjectLock.readLock().unlock(); + } + + stage.flip(); + + final int dataLen = stage.getInt(); + stage.getLong(); // TODO use this timestamp(long) for something? + final int expectedXXHash32 = stage.getInt(); + + // lz4 decompresses straight from the scratch into the result buffer: the + // compressed bytes are never copied into an intermediate array + final byte[] data = new byte[dataLen]; + LZ4_DECOMPRESSOR.decompress(stage.array(), stage.arrayOffset() + SECTOR_META_SIZE, data, 0, dataLen); + + final int actualXXHash32 = XX_HASH_32.hash(data, 0, dataLen, XXHASH32_SEED); + if (actualXXHash32 != expectedXXHash32) { + throw new IOException("XXHash32 check failed ! Expected: " + expectedXXHash32 + ",but got: " + actualXXHash32); // prevent from loading + } + + return ByteBuffer.wrap(data); + } + + @Override + public Path getPath() { + return this.masterFilePath; + } + + @Override + public DataInputStream getChunkDataInputStream(@NotNull ChunkPos pos) throws IOException { + final ByteBuffer data = this.readChunk(pos.x(), pos.z()); + + if (data == null) { + return null; + } + + return new DataInputStream(new ByteBufferInputStream(data)); + } + + @Override + public boolean doesChunkExist(@NotNull ChunkPos pos) throws IOException { + return this.hasData(getChunkIndex(pos.x(), pos.z())); + } + + @Override + public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { + return new DataOutputStream(new ChunkBufferHelper(pos)); + } + + @Override + public void clear(@NotNull ChunkPos pos) throws IOException { + this.clearChunkData(getChunkIndex(pos.x(), pos.z())); + } + + @Override + public boolean hasChunk(@NotNull ChunkPos pos) { + try { + return this.hasData(getChunkIndex(pos.x(), pos.z())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void write(@NotNull ChunkPos pos, ByteBuffer buf) throws IOException { + this.writeChunk(pos.x(), pos.z(), buf); + } + + // MCC 的玩意,这东西也用不上给Linear了() + @Override + public CompoundTag getOversizedData(int x, int z) { + return null; + } + + @Override + public boolean isOversized(int x, int z) { + return false; + } + + @Override + public boolean recalculateHeader() { + return false; + } + + @Override + public void setOversized(int x, int z, boolean oversized) { + + } + // MCC end + + @Override + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { + final DataOutputStream out = this.getChunkDataOutputStream(pos); + + return new MoonriseRegionFileIO.RegionDataController.WriteData( + data, MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, + out, regionFile -> out.close() + ); + } + + @Override + public void flush() throws IOException { + this.flushInternal(); + } + + @Override + public void close() throws IOException { + this.closeInternal(); + } + + public static class ByteBufferInputStream extends InputStream { + protected final ByteBuffer internal; + + public ByteBufferInputStream(ByteBuffer buf) { + this.internal = buf; + } + + @Override + public int available() { + return this.internal.remaining(); + } + + @Override + public int read() throws IOException { + return this.internal.hasRemaining() ? (this.internal.get() & 0xFF) : -1; + } + + @Override + public int read(byte @NotNull [] bytes, int off, int len) throws IOException { + if (!this.internal.hasRemaining()) return -1; + len = Math.min(len, this.internal.remaining()); + this.internal.get(bytes, off, len); + return len; + } + } + + public class Sector { + private final int index; + private long offset; + private long length; + private boolean hasData = false; + + private Sector(int index, long offset, long length) { + this.index = index; + this.offset = offset; + this.length = length; + } + + public void store(@NotNull ByteBuffer newData, @NotNull FileChannel channel) throws IOException { + final long oldLength = this.length; + final long oldLive = this.hasData ? oldLength : 0L; + final long newDataLength = newData.remaining(); + + this.hasData = true; + this.length = newDataLength; + + // data fits into the extent this sector already owns (a cleared sector keeps + // its stale extent exactly for this reuse), write it in place + if (newDataLength <= oldLength) { + writeFullyAt(channel, newData, this.offset); + } else { + // or we will append to the end of file + this.offset = BufferedLinearRegionFile.this.currentAcquiredIndex; + BufferedLinearRegionFile.this.currentAcquiredIndex = this.offset + newDataLength; + + writeFullyAt(channel, newData, this.offset); + } + + // single mutator under the region write lock; keeps the garbage estimate + // in flushInternal() lock-free and scan-free + BufferedLinearRegionFile.this.liveBytes += newDataLength - oldLive; + } + + public void clear() { + if (this.hasData) { + BufferedLinearRegionFile.this.liveBytes -= this.length; + } + + this.hasData = false; + } + + public boolean hasData() { + return this.hasData; + } + } + + private class ChunkBufferHelper extends ByteArrayOutputStream { + private final ChunkPos pos; + + private ChunkBufferHelper(ChunkPos pos) { + // chunk NBT payloads are tens to hundreds of KiB: BAOS's default 32 bytes + // means a dozen grow-and-copy rounds per single chunk serialization + super(8192); + this.pos = pos; + } + + @Override + public void close() throws IOException { + ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); + + BufferedLinearRegionFile.this.writeChunk(this.pos.x(), this.pos.z(), bytebuffer); + + BufferedLinearRegionFile.this.flushInternal(); + } + } + + private class MasterFileParser { + // V3 bucketed format layout (UNCHANGED, fully compatible with existing files): + // [0, 14): header — superblock(8) + version(1) + compressionLevel(1) + xxHash32Seed(4) + // [14, 142): position table — BUCKET_COUNT(16) × long(8) each; 0 = no data for that bucket + // [142, EOF): bucket records — originalLen(int) + compressedLen(int) + compressedData + private static final int V3_HEADER_SIZE = 14; + private static final long V3_POS_TABLE_OFFSET = V3_HEADER_SIZE; + private static final int V3_POS_TABLE_SIZE = BUCKET_COUNT * Long.BYTES; // 128 + private static final long V3_DATA_AREA_OFFSET = V3_POS_TABLE_OFFSET + V3_POS_TABLE_SIZE; // 142 + private static final int V3_RECORD_HEADER_SIZE = Integer.BYTES * 2; // originalLen + compressedLen + + private final ReadWriteLock masterFileLock = new ReentrantReadWriteLock(); + + // WAL(append) state, guarded by masterFileLock: non-null whenever a valid v3 + // master file is open for appending — restored directly at open time by + // tryEnterWalMode(), or (re)established by rewriteFully(); syncs then only + // append changed buckets to the tail and update the position table in place. + // recordSizes mirrors positionTable (size of each live record) so the garbage + // ratio can be computed without touching the disk + private @Nullable FileChannel appendChannel; + private long[] positionTable; + private long[] recordSizes; + private long appendOffset; + + // single volatile read instead of a Files.exists() stat per chunk write + private volatile boolean fileExists; + + // a consistent snapshot of one bucket taken from the swap file; + // payload == null means the bucket holds no chunks at all + private record BucketRecord(long epoch, @Nullable ByteBuffer payload) { + } + + public boolean masterFileExists() { + return this.fileExists; + } + + // resumes WAL mode from an existing, structurally valid v3 master file so the + // first sync after open can append instead of rewriting the entire file. + // bails out silently (leaving the full-rewrite path armed) if the file is + // missing, not v3, or its position table doesn't validate + public void tryEnterWalMode(@NotNull Path mainFile) throws IOException { + this.masterFileLock.writeLock().lock(); + try { + // legacy migration in tryParseMainFileOld() may have entered WAL already + if (this.appendChannel != null) { + return; + } + + if (!Files.exists(mainFile)) { + return; + } + + this.fileExists = true; + + final FileChannel channel = FileChannel.open(mainFile, StandardOpenOption.READ, StandardOpenOption.WRITE); + boolean success = false; + try { + final long fileSize = channel.size(); + + if (fileSize < V3_DATA_AREA_OFFSET) { + return; + } + + final ByteBuffer header = ByteBuffer.allocate(V3_HEADER_SIZE); + readFullyAt(channel, header, 0); + header.flip(); + + if (header.getLong() != MASTER_FILE_SUPER_BLOCK || header.get() != MASTER_FILE_VERSION_BUCKET) { + return; + } + + final long[] table = this.parseOffsetTable(channel); + final long[] sizes = new long[BUCKET_COUNT]; + long dataEnd = V3_DATA_AREA_OFFSET; + + for (int i = 0; i < BUCKET_COUNT; i++) { + final long recordOffset = table[i]; + + if (recordOffset == 0) { + continue; + } + + if (recordOffset < V3_DATA_AREA_OFFSET || recordOffset + V3_RECORD_HEADER_SIZE > fileSize) { + return; // corrupted table: stay in full-rewrite mode + } + + final ByteBuffer lens = this.readRecordLengths(channel, recordOffset); + final int originalLen = lens.getInt(); + final int compressedLen = lens.getInt(); + + if (originalLen < 0 || compressedLen < 0 || recordOffset + V3_RECORD_HEADER_SIZE + compressedLen > fileSize) { + return; // corrupted record header: stay in full-rewrite mode + } + + sizes[i] = V3_RECORD_HEADER_SIZE + (long) compressedLen; + dataEnd = Math.max(dataEnd, recordOffset + sizes[i]); + } + + // append after the last referenced record: anything past that is + // uncommitted garbage from a torn previous append and may be reused + this.appendChannel = channel; + this.positionTable = table; + this.recordSizes = sizes; + this.appendOffset = dataEnd; + success = true; + } finally { + if (!success) { + channel.close(); + } + } + } finally { + this.masterFileLock.writeLock().unlock(); + } + } + + // must be called under syncLock (see syncToMasterFile) + public void sync(@NotNull Path mainFile, boolean forceCompact) throws IOException { + this.masterFileLock.writeLock().lock(); + try { + // full rewrite whenever no valid append state exists (fresh region / + // corrupted table / legacy migration), and afterwards whenever the + // appended garbage passed the auto-compact threshold: writes a tmp file, + // then atomically replaces the master file with it + if (this.appendChannel == null || this.shouldCompactMasterFile() || forceCompact) { + this.rewriteFully(mainFile); + } else { + // WAL-style otherwise: only append the dirty buckets + this.appendDirtyBuckets(); + } + } finally { + this.masterFileLock.writeLock().unlock(); + } + } + + // only valid in WAL mode (appendChannel != null); mirrors the swap file heuristic + private boolean shouldCompactMasterFile() { + long liveSize = 0; + for (final long size : this.recordSizes) { + liveSize += size; + } + + final long spareSize = this.appendOffset - V3_DATA_AREA_OFFSET - liveSize; + + return spareSize > MASTER_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) liveSize) * MASTER_FILE_AUTO_COMPACT_PERCENT; + } + + private void rewriteFully(@NotNull Path mainFile) throws IOException { + final boolean wal = this.appendChannel != null; + final Path tmpFilePath = Path.of(mainFile + ".tmp"); + final long[] syncedBucketEpochs = new long[BUCKET_COUNT]; + final long[] newPositionTable = new long[BUCKET_COUNT]; + final long[] newRecordSizes = new long[BUCKET_COUNT]; + final long newAppendOffset; + + FileChannel legacySource = null; + try { + final FileChannel oldChannel; + final long[] oldPositionTable; + + if (wal) { + // reuse the live append channel as the copy source together with the + // cached table/sizes: no reopen and no per-bucket length pread needed + oldChannel = this.appendChannel; + oldPositionTable = this.positionTable; + } else { + legacySource = this.openV3MasterFile(mainFile); + oldChannel = legacySource; + oldPositionTable = oldChannel == null ? null : this.parseOffsetTable(oldChannel); + } + + try (FileChannel outChannel = FileChannel.open(tmpFilePath, + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + this.writeV3Header(outChannel); + + // position table placeholder (all zeros, filled in at the end) + writeFullyAt(outChannel, ByteBuffer.allocate(V3_POS_TABLE_SIZE), V3_POS_TABLE_OFFSET); + + long dataOffset = V3_DATA_AREA_OFFSET; + + for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) { + if (BufferedLinearRegionFile.this.isBucketDirty(bucketIndex)) { + final BucketRecord record = this.buildBucketRecord(bucketIndex); + + if (record.payload() != null) { + final int recordSize = record.payload().remaining(); + + writeFullyAt(outChannel, record.payload(), dataOffset); + newPositionTable[bucketIndex] = dataOffset; + newRecordSizes[bucketIndex] = recordSize; + dataOffset += recordSize; + } + // else: the bucket is empty now, its table entry stays 0 + + syncedBucketEpochs[bucketIndex] = record.epoch(); + } else if (oldPositionTable != null && oldPositionTable[bucketIndex] != 0) { + // not dirty: copy the record bytes straight from the old file + final long oldOffset = oldPositionTable[bucketIndex]; + final long recordSize; + + if (wal) { + recordSize = this.recordSizes[bucketIndex]; + } else { + final ByteBuffer lens = this.readRecordLengths(oldChannel, oldOffset); + lens.getInt(); // skip originalLen + recordSize = V3_RECORD_HEADER_SIZE + (long) lens.getInt(); + } + + transferFully(oldChannel, oldOffset, recordSize, outChannel, dataOffset); + newPositionTable[bucketIndex] = dataOffset; + newRecordSizes[bucketIndex] = recordSize; + dataOffset += recordSize; + } + } + + // write the finalized position table + writeFullyAt(outChannel, this.encodePositionTable(newPositionTable), V3_POS_TABLE_OFFSET); + + outChannel.force(true); + + newAppendOffset = dataOffset; + } + } catch (Throwable e) { + // don't leak the half-written tmp file; in WAL mode the append state is + // untouched so the next sync just retries the compact, in legacy mode + // the next sync retries this full-rewrite path + try { + Files.deleteIfExists(tmpFilePath); + } catch (Throwable e2) { + e.addSuppressed(e2); + } + + throw e instanceof IOException io ? io : new IOException("Failed to rewrite master file!", e); + } finally { + if (legacySource != null) { + legacySource.close(); + } + } + + // close the append channel before the replace: some platforms (windows) + // refuse to replace a file that still has open handles + if (wal) { + final FileChannel toClose = this.appendChannel; + this.appendChannel = null; // if close() throws, fall back to full rewrite next sync + toClose.close(); + } + + atomicReplace(tmpFilePath, mainFile); + + // (re)enter WAL mode: keep the freshly written master file open for appending syncs + this.appendChannel = FileChannel.open(mainFile, StandardOpenOption.READ, StandardOpenOption.WRITE); + this.positionTable = newPositionTable; + this.recordSizes = newRecordSizes; + this.appendOffset = newAppendOffset; + this.fileExists = true; + + this.markBucketsSynced(syncedBucketEpochs); + } + + private void appendDirtyBuckets() throws IOException { + final FileChannel channel = this.appendChannel; + final long[] syncedBucketEpochs = new long[BUCKET_COUNT]; + final long[] newPositionTable = this.positionTable.clone(); + final long[] newRecordSizes = this.recordSizes.clone(); + final ByteBuffer[] pending = new ByteBuffer[BUCKET_COUNT]; + long dataOffset = this.appendOffset; + int pendingCount = 0; + boolean anyDirty = false; + + for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) { + if (!BufferedLinearRegionFile.this.isBucketDirty(bucketIndex)) { + continue; + } + + final BucketRecord record = this.buildBucketRecord(bucketIndex); + final ByteBuffer payload = record.payload(); + + if (payload != null) { + pending[pendingCount++] = payload; + newPositionTable[bucketIndex] = dataOffset; + newRecordSizes[bucketIndex] = payload.remaining(); + dataOffset += payload.remaining(); + } else { + // the bucket is empty now + newPositionTable[bucketIndex] = 0; + newRecordSizes[bucketIndex] = 0; + } + + syncedBucketEpochs[bucketIndex] = record.epoch(); + anyDirty = true; + } + + if (!anyDirty) { + return; + } + + if (pendingCount > 0) { + // all records land contiguously at the tail: one gathering write (writev) + // instead of one pwrite per dirty bucket + channel.position(this.appendOffset); + + final ByteBuffer last = pending[pendingCount - 1]; + while (last.hasRemaining()) { + channel.write(pending, 0, pendingCount); + } + + // make the appended records durable before the position table may point at them + channel.force(false); + } + + // commit the new tail first: even a torn position table write can then never + // cause a later append to overwrite records the on-disk table already references + this.appendOffset = dataOffset; + + writeFullyAt(channel, this.encodePositionTable(newPositionTable), V3_POS_TABLE_OFFSET); + channel.force(true); + + this.positionTable = newPositionTable; + this.recordSizes = newRecordSizes; + + this.markBucketsSynced(syncedBucketEpochs); + } + + // snapshots one bucket under a short read lock (raw sector bytes only, with + // sectors that sit back to back in the swap file coalesced into single preads); + // LZ4 decompression and zstd compression both run outside any lock so writers + // are only blocked while the raw bytes are copied + private @NotNull BucketRecord buildBucketRecord(int bucketIndex) throws IOException { + final int baseChunkIndex = bucketIndex << BUCKET_SHIFT; + final ByteBuffer[] rawSectors = new ByteBuffer[BUCKET_SIZE]; // slices into run buffers, null = no data + + final long[] offsets = new long[BUCKET_SIZE]; + final long[] lengths = new long[BUCKET_SIZE]; + final int[] slots = new int[BUCKET_SIZE]; + int liveCount = 0; + + final long epoch; + + BufferedLinearRegionFile.this.regionObjectLock.readLock().lock(); + try { + // the epoch is taken before the data: writes completing afterwards bump + // it further, so they simply get picked up by the next sync round + epoch = BufferedLinearRegionFile.this.getBucketWriteEpoch(bucketIndex); + + for (int i = 0; i < BUCKET_SIZE; i++) { + final Sector sector = BufferedLinearRegionFile.this.sectors[baseChunkIndex + i]; + + if (!sector.hasData()) { + continue; + } + + offsets[liveCount] = sector.offset; + lengths[liveCount] = sector.length; + slots[liveCount] = i; + liveCount++; + } + + if (liveCount == 0) { + return new BucketRecord(epoch, null); + } + + sortByOffset(offsets, lengths, slots, liveCount); + + int i = 0; + while (i < liveCount) { + int j = i; + long runEnd = offsets[i] + lengths[i]; + + while (j + 1 < liveCount && offsets[j + 1] == runEnd) { + j++; + runEnd += lengths[j]; + } + + final ByteBuffer run = ByteBuffer.allocate((int) (runEnd - offsets[i])); + readFullyAt(BufferedLinearRegionFile.this.swapFileChannel, run, offsets[i]); + + for (int k = i; k <= j; k++) { + rawSectors[slots[k]] = run.slice((int) (offsets[k] - offsets[i]), (int) lengths[k]); + } + + i = j + 1; + } + } finally { + BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock(); + } + + // exact size budget up front: 4 bytes size prefix per chunk slot plus + // meta + decompressed data for the live ones — one allocation, no growing + // ByteArrayOutputStream and no toByteArray() copy at the end + int sectionSize = BUCKET_SIZE * Integer.BYTES; + for (int i = 0; i < BUCKET_SIZE; i++) { + final ByteBuffer raw = rawSectors[i]; + + if (raw != null) { + sectionSize += SECTOR_META_SIZE + raw.getInt(raw.position()); + } + } + + final byte[] section = new byte[sectionSize]; + final ByteBuffer sectionBuf = ByteBuffer.wrap(section); + + for (int i = 0; i < BUCKET_SIZE; i++) { + final ByteBuffer raw = rawSectors[i]; + + // note: null -> no data contained + if (raw == null) { + sectionBuf.putInt(0); + continue; + } + + final byte[] runArray = raw.array(); + final int rawBase = raw.arrayOffset() + raw.position(); + final int dataLen = raw.getInt(raw.position()); + + sectionBuf.putInt(SECTOR_META_SIZE + dataLen); + sectionBuf.put(runArray, rawBase, SECTOR_META_SIZE); // meta bytes carried over verbatim + + // lz4 decompresses straight into the section buffer, no intermediate arrays + final int destPos = sectionBuf.position(); + LZ4_DECOMPRESSOR.decompress(runArray, rawBase + SECTOR_META_SIZE, section, destPos, dataLen); + sectionBuf.position(destPos + dataLen); + } + + // zstd compresses straight into the final payload: skips Zstd.compress()'s + // internal bound-sized temp array plus its exact-size copy at the end + final int bound = (int) Zstd.compressBound(sectionSize); + final byte[] payload = new byte[V3_RECORD_HEADER_SIZE + bound]; + final long compressedLen = Zstd.compressByteArray(payload, V3_RECORD_HEADER_SIZE, bound, section, 0, sectionSize, BufferedLinearRegionFile.this.compressionLevel); + + if (Zstd.isError(compressedLen)) { + throw new IOException("Failed to zstd compress bucket " + bucketIndex + ": " + Zstd.getErrorName(compressedLen)); + } + + final ByteBuffer result = ByteBuffer.wrap(payload, 0, V3_RECORD_HEADER_SIZE + (int) compressedLen); + result.putInt(sectionSize); // original (uncompressed) length + result.putInt((int) compressedLen); // compressed length + result.position(0); + + return new BucketRecord(epoch, result); + } + + private static void sortByOffset(long[] offsets, long[] lengths, int[] slots, int count) { + // n <= 64, insertion sort is plenty and allocation-free + for (int i = 1; i < count; i++) { + final long offset = offsets[i]; + final long length = lengths[i]; + final int slot = slots[i]; + int j = i - 1; + + while (j >= 0 && offsets[j] > offset) { + offsets[j + 1] = offsets[j]; + lengths[j + 1] = lengths[j]; + slots[j + 1] = slots[j]; + j--; + } + + offsets[j + 1] = offset; + lengths[j + 1] = length; + slots[j + 1] = slot; + } + } + + private void markBucketsSynced(long[] syncedBucketEpochs) { + for (int i = 0; i < syncedBucketEpochs.length; i++) { + // note: a dirty bucket always has a write epoch >= 1, so 0 = untouched + if (syncedBucketEpochs[i] != 0L) { + BufferedLinearRegionFile.this.markBucketSynced(i, syncedBucketEpochs[i]); + } + } + } + + // opens the master file for reading if it exists and is a valid V3 bucketed file, else null + private @Nullable FileChannel openV3MasterFile(@NotNull Path mainFile) throws IOException { + if (!Files.exists(mainFile)) { + return null; + } + + final FileChannel channel = FileChannel.open(mainFile, StandardOpenOption.READ); + try { + if (channel.size() >= V3_DATA_AREA_OFFSET) { + final ByteBuffer header = ByteBuffer.allocate(V3_HEADER_SIZE); + readFullyAt(channel, header, 0); + header.flip(); + + if (header.getLong() == MASTER_FILE_SUPER_BLOCK && header.get() == MASTER_FILE_VERSION_BUCKET) { + return channel; + } + } + } catch (Throwable e) { + try { + channel.close(); + } catch (IOException e2) { + e.addSuppressed(e2); + } + + throw e; + } + + channel.close(); + return null; + } + + private void writeV3Header(@NotNull FileChannel channel) throws IOException { + final ByteBuffer header = ByteBuffer.allocate(V3_HEADER_SIZE); + + header.putLong(MASTER_FILE_SUPER_BLOCK); + header.put(MASTER_FILE_VERSION_BUCKET); + header.put(BufferedLinearRegionFile.this.compressionLevel); + header.putInt(XXHASH32_SEED); + header.flip(); + + writeFullyAt(channel, header, 0); + } + + private @NotNull ByteBuffer encodePositionTable(long[] table) { + final ByteBuffer buf = ByteBuffer.allocate(V3_POS_TABLE_SIZE); + + for (final long pos : table) { + buf.putLong(pos); + } + + return buf.flip(); + } + + private @NotNull ByteBuffer readRecordLengths(@NotNull FileChannel channel, long recordOffset) throws IOException { + final ByteBuffer lens = ByteBuffer.allocate(V3_RECORD_HEADER_SIZE); + + readFullyAt(channel, lens, recordOffset); + + return lens.flip(); + } + + public void close() throws IOException { + this.masterFileLock.writeLock().lock(); + try { + if (this.appendChannel != null) { + this.appendChannel.close(); + this.appendChannel = null; + } + } finally { + this.masterFileLock.writeLock().unlock(); + } + } + + private void loadBucketsFor(@NotNull Path file, int bucketIndex) throws IOException { + final int beginChunkIndex = bucketIndex << BUCKET_SHIFT; + + this.masterFileLock.readLock().lock(); + try { + final ByteBuffer decompressed; + + if (this.appendChannel != null) { + // WAL mode: reuse the always-open channel and the cached position table + decompressed = this.readBucketData(this.appendChannel, this.positionTable[bucketIndex]); + } else { + if (!Files.exists(file)) { + return; + } + + try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) { + if (channel.size() < V3_DATA_AREA_OFFSET) { + return; + } + + this.checkV3Header(channel); + + decompressed = this.readBucketData(channel, this.parseOffsetTable(channel)[bucketIndex]); + } + } + + if (decompressed != null) { + this.loadChunksFromBucketData(decompressed, beginChunkIndex); + } + } finally { + this.masterFileLock.readLock().unlock(); + } + } + + private void checkV3Header(@NotNull FileChannel channel) throws IOException { + final ByteBuffer headerBuf = ByteBuffer.allocate(V3_HEADER_SIZE); + readFullyAt(channel, headerBuf, 0); + headerBuf.flip(); + + final long superblock = headerBuf.getLong(); + if (superblock != MASTER_FILE_SUPER_BLOCK) + throw new IOException("Invalid superblock " + superblock + "!"); + + final byte version = headerBuf.get(); + if (version != MASTER_FILE_VERSION_BUCKET) + throw new IOException("Unknown version: " + version); + + // compressionLevel and hashSeed are not used here + } + + // reads and decompresses one bucket record; null when the table entry is empty + private @Nullable ByteBuffer readBucketData(@NotNull FileChannel channel, long recordOffset) throws IOException { + if (recordOffset == 0) { + return null; + } + + final ByteBuffer lens = this.readRecordLengths(channel, recordOffset); + final int originalLen = lens.getInt(); + final int compressedLen = lens.getInt(); + + final byte[] compressedData = new byte[compressedLen]; + readFullyAt(channel, ByteBuffer.wrap(compressedData), recordOffset + V3_RECORD_HEADER_SIZE); + + return ByteBuffer.wrap(Zstd.decompress(compressedData, originalLen)); + } + + private long @NonNull [] parseOffsetTable(FileChannel channel) throws IOException { + final ByteBuffer buf = ByteBuffer.allocate(V3_POS_TABLE_SIZE); + readFullyAt(channel, buf, V3_POS_TABLE_OFFSET); + buf.flip(); + + final long[] table = new long[BUCKET_COUNT]; + + for (int i = 0; i < BUCKET_COUNT; i++) { + table[i] = buf.getLong(); + } + + return table; + } + + private void loadChunksFromBucketData(ByteBuffer decompressed, int beginChunkIndex) throws IOException { + for (int chunkIndex = beginChunkIndex; chunkIndex < beginChunkIndex + BUCKET_SIZE; chunkIndex++) { + final int chunkSectionDataSize = decompressed.getInt(); + if (chunkSectionDataSize <= 0) continue; + + // slice instead of copying the section bytes out + final ByteBuffer section = decompressed.slice(decompressed.position(), chunkSectionDataSize); + decompressed.position(decompressed.position() + chunkSectionDataSize); + + BufferedLinearRegionFile.this.writeSection(chunkIndex, section, true); + } + } + + private void parseLinearV2(@NonNull DataInputStream ioStream, Path file) throws IOException { + try (ioStream) { + ioStream.readLong(); // Skip newestTimestamp (Long) + + byte gridSize = ioStream.readByte(); + if (gridSize != 1 && gridSize != 2 && gridSize != 4 && gridSize != 8 && gridSize != 16 && gridSize != 32) + throw new RuntimeException("Invalid grid size: " + gridSize + " file " + file); + int bucketSize = 32 / gridSize; + + ioStream.readInt(); // Skip region_x (Int) + ioStream.readInt(); // Skip region_z (Int) + + ioStream.skipBytes(128); // Skip existence bitmap + + // Skip NBT features + while (true) { + byte featureNameLength = ioStream.readByte(); + if (featureNameLength == 0) break; + byte[] featureNameBytes = new byte[featureNameLength]; + ioStream.readFully(featureNameBytes); + ioStream.readInt(); // featureValue + } + + // Read bucket metadata + int totalBuckets = gridSize * gridSize; + int[] bucketSizes = new int[totalBuckets]; + byte[] bucketCompressionLevels = new byte[totalBuckets]; + long[] bucketHashes = new long[totalBuckets]; + for (int i = 0; i < totalBuckets; i++) { + bucketSizes[i] = ioStream.readInt(); + bucketCompressionLevels[i] = ioStream.readByte(); + bucketHashes[i] = ioStream.readLong(); + } + + // Read and decompress each bucket, load chunks into swap + for (int bx = 0; bx < gridSize; bx++) { + for (int bz = 0; bz < gridSize; bz++) { + int bucketIdx = bx * gridSize + bz; + + if (bucketSizes[bucketIdx] <= 0) continue; + + byte[] compressedBucket = new byte[bucketSizes[bucketIdx]]; + ioStream.readFully(compressedBucket); + + long rawHash = LongHashFunction.xx().hashBytes(compressedBucket); + if (rawHash != bucketHashes[bucketIdx]) { + throw new IOException("Region file hash incorrect for bucket " + bucketIdx + " in " + file); + } + + ByteArrayInputStream bucketByteStream = new ByteArrayInputStream(compressedBucket); + ZstdInputStream zstdStream = new ZstdInputStream(bucketByteStream); + ByteBuffer bucketBuffer = ByteBuffer.wrap(zstdStream.readAllBytes()); + zstdStream.close(); + + for (int cx = 0; cx < bucketSize; cx++) { + for (int cz = 0; cz < bucketSize; cz++) { + int chunkX = bx * bucketSize + cx; + int chunkZ = bz * bucketSize + cz; + int chunkIndex = chunkX + chunkZ * 32; + + int chunkSize = bucketBuffer.getInt(); + long timestamp = bucketBuffer.getLong(); + + if (chunkSize > 0) { + // chunkSize includes the 8 bytes of timestamp already written + int dataLen = chunkSize - 8; + byte[] chunkData = new byte[dataLen]; + bucketBuffer.get(chunkData); + + // Mark bucket as loaded. writeChunk() bumps the bucket epoch so it gets synced to the new master format. + BufferedLinearRegionFile.this.markBucketLoaded(chunkIndex); + + // Use writeChunk to go through the full path (adds length + timestamp + xxhash header) + BufferedLinearRegionFile.this.writeChunk(chunkX, chunkZ, ByteBuffer.wrap(chunkData)); + } + } + } + } + } + + // Footer validation + long footerSuperBlock = ioStream.readLong(); + if (footerSuperBlock != LINEAR_FILE_SUPER_BLOCK) { + throw new IOException("Footer superblock invalid " + file); + } + } + } + + private boolean tryParseBlinearV2(@NotNull DataInputStream ioStream, Path file) throws IOException { + final byte version = ioStream.readByte(); + + // we will parse dynamically (V3) + if (version == MASTER_FILE_VERSION_BUCKET) { + ioStream.close(); + return false; + } + + if (version != MASTER_FILE_VERSION) + throw new RuntimeException("Invalid version: " + version + " in " + file); + + // Skip newestTimestamp (Long) + Compression level (Byte): Unused. + ioStream.skipBytes(9); + + try (final ZstdInputStream decompressStream = new ZstdInputStream(ioStream)) { + // only used as a helper stream + // the parent stream will be closed in the try-catch block upper + final DataInputStream decompressedStreamHelper = new DataInputStream(decompressStream); + + for (int index = 0; index < 1024; index++) { + int size = decompressedStreamHelper.readInt(); // len + + if (size > 0) { + byte[] sectorData = new byte[size]; + decompressedStreamHelper.readFully(sectorData, 0, size); // data + + final ByteBuffer sectorDataNioBuffer = ByteBuffer.wrap(sectorData); + + BufferedLinearRegionFile.this.markBucketLoaded(index); + // blinear v2 stored the exact section layout, feed it through the section path + BufferedLinearRegionFile.this.writeSection(index, sectorDataNioBuffer, false); + } + } + } + + return true; + } + + @Contract(value = "_ -> new", pure = true) + public static int @NotNull [] coordinatesFromIndex(int chunkIndex) { + int x = chunkIndex & 31; + int z = (chunkIndex >> 5) & 31; + return new int[]{x, z}; + } + + private void parseLinearV1(@NotNull DataInputStream ioStream) throws IOException { + // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused. + ioStream.skipBytes(11); + // Skip chunk data len(Int)(Unused). + ioStream.skipBytes(4); + // Skip data hash (Long): Unused. + ioStream.skipBytes(8); + + try (final ZstdInputStream decompressedStream = new ZstdInputStream(ioStream)) { + // only used as a helper stream + // the parent stream will be closed in the try-catch block upper + final DataInputStream bufferHelper = new DataInputStream(decompressedStream); + + final int[] chunkStarts = new int[1024]; + for (int i = 0; i < 1024; i++) { + chunkStarts[i] = bufferHelper.readInt(); + bufferHelper.skipBytes(4); // Skip timestamps (Int): Unused. + } + + for (int i = 0; i < 1024; i++) { + if (chunkStarts[i] > 0) { + int size = chunkStarts[i]; + byte[] chunkData = new byte[size]; + bufferHelper.readFully(chunkData); + + final ByteBuffer chunkDataNioBuffer = ByteBuffer.wrap(chunkData); + + final int[] posByAxis = coordinatesFromIndex(i); + + final int x = posByAxis[0]; + final int z = posByAxis[1]; + + BufferedLinearRegionFile.this.markBucketLoaded(i); + BufferedLinearRegionFile.this.writeChunk(x, z, chunkDataNioBuffer); + } + } + } + } + + // won't and need not hold any region locks as we are calling this in a safe point (initially newed) + public void tryParseMainFileOld(@NotNull Path mainFilePath) throws IOException { + final File file = mainFilePath.toFile(); + + if (!file.exists() || !file.canRead()) { + return; + } + + // those streams will be closed in the parse logic, or we will close it manually + final FileInputStream fileStream = new FileInputStream(file); + final DataInputStream rawDataStream = new DataInputStream(fileStream); + + boolean oldParsed = false; + final long superBlock; + try { + superBlock = rawDataStream.readLong(); + + if (superBlock == MASTER_FILE_SUPER_BLOCK) { + oldParsed = this.tryParseBlinearV2(rawDataStream, mainFilePath); + + // false -> v3 -> closed in parse block + if (!oldParsed) { + return; + } + } + + if (superBlock == LINEAR_FILE_SUPER_BLOCK) { + final byte version = rawDataStream.readByte(); + + if (version == 1 || version == 2) { + this.parseLinearV1(rawDataStream); + + oldParsed = true; + } + + if (version == 3) { + this.parseLinearV2(rawDataStream, mainFilePath); + + oldParsed = true; + } + } + + } catch (Throwable ex) { + try { + rawDataStream.close(); + } catch (IOException ex2) { + ex.addSuppressed(ex2); + } + + throw new IOException("Failed to parse master file: " + mainFilePath, ex); + } + + // old parsed, remove the original file, and we will recreate it as we sync + if (oldParsed) { + // immediately do sync operation + BufferedLinearRegionFile.this.syncToMasterFile(true, true); + return; + } + + // anyone non-matched, close stream and throw the error + rawDataStream.close(); + + throw new IOException("Unknown or unsupported super block : " + superBlock); + } + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java b/lophine-server/src/main/java/io/anonymous/anonymous/data/BufferedLinearRegionFileFlusher.java similarity index 98% rename from lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java rename to lophine-server/src/main/java/io/anonymous/anonymous/data/BufferedLinearRegionFileFlusher.java index 081fe9f..5f20147 100644 --- a/lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java +++ b/lophine-server/src/main/java/io/anonymous/anonymous/data/BufferedLinearRegionFileFlusher.java @@ -1,10 +1,9 @@ -package me.earthme.luminol.utils; +package io.anonymous.anonymous.data; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectArraySet; -import me.earthme.luminol.data.BufferedLinearRegionFile; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; diff --git a/lophine-server/src/main/java/abomination/IRegionFile.java b/lophine-server/src/main/java/io/anonymous/anonymous/data/RegionFile.java similarity index 86% rename from lophine-server/src/main/java/abomination/IRegionFile.java rename to lophine-server/src/main/java/io/anonymous/anonymous/data/RegionFile.java index 52d72f0..455c715 100644 --- a/lophine-server/src/main/java/abomination/IRegionFile.java +++ b/lophine-server/src/main/java/io/anonymous/anonymous/data/RegionFile.java @@ -1,4 +1,5 @@ -package abomination; +package io.anonymous.anonymous.data; + import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile; import net.minecraft.nbt.CompoundTag; @@ -10,7 +11,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.file.Path; -public interface IRegionFile extends ChunkSystemRegionFile, AutoCloseable { +public interface RegionFile extends ChunkSystemRegionFile, AutoCloseable { Path getPath(); DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException; @@ -39,5 +40,5 @@ public interface IRegionFile extends ChunkSystemRegionFile, AutoCloseable { default int getRecalculateCount() { return 0; - } // Luminol - Configurable region file format + } // Anonymous - Configurable region file format } \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java b/lophine-server/src/main/java/io/anonymous/anonymous/enums/EnumRegionFormat.java similarity index 72% rename from lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java rename to lophine-server/src/main/java/io/anonymous/anonymous/enums/EnumRegionFormat.java index 174a196..bcb27ad 100644 --- a/lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java +++ b/lophine-server/src/main/java/io/anonymous/anonymous/enums/EnumRegionFormat.java @@ -1,9 +1,9 @@ -package me.earthme.luminol.enums; +package io.anonymous.anonymous.enums; import abomination.LinearRegionFile; import me.earthme.luminol.config.modules.function.RegionFormatConfig; -import me.earthme.luminol.data.BufferedLinearRegionFile; -import me.earthme.luminol.utils.IRegionCreateFunction; +import io.anonymous.anonymous.data.BufferedLinearRegionFile; +import io.anonymous.anonymous.utils.RegionFileFactory; import net.minecraft.world.level.chunk.storage.RegionFile; public enum EnumRegionFormat { @@ -12,14 +12,14 @@ public enum EnumRegionFormat { B_LINEAR("b_linear", (info) -> new BufferedLinearRegionFile(info.filePath(), RegionFormatConfig.linearCompressionLevel, RegionFormatConfig.blinearFlusher)); private final String argument; - private final IRegionCreateFunction creator; + private final RegionFileFactory creator; - EnumRegionFormat(String argument, IRegionCreateFunction creator) { + EnumRegionFormat(String argument, RegionFileFactory creator) { this.argument = argument; this.creator = creator; } - public IRegionCreateFunction getCreator() { + public RegionFileFactory getCreator() { return this.creator; } diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java b/lophine-server/src/main/java/io/anonymous/anonymous/utils/RegionCreatorInfo.java similarity index 83% rename from lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java rename to lophine-server/src/main/java/io/anonymous/anonymous/utils/RegionCreatorInfo.java index aafb3b9..5981612 100644 --- a/lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java +++ b/lophine-server/src/main/java/io/anonymous/anonymous/utils/RegionCreatorInfo.java @@ -1,4 +1,4 @@ -package me.earthme.luminol.utils; +package io.anonymous.anonymous.utils; import net.minecraft.world.level.chunk.storage.RegionStorageInfo; diff --git a/lophine-server/src/main/java/io/anonymous/anonymous/utils/RegionFileFactory.java b/lophine-server/src/main/java/io/anonymous/anonymous/utils/RegionFileFactory.java new file mode 100644 index 0000000..ffca7bc --- /dev/null +++ b/lophine-server/src/main/java/io/anonymous/anonymous/utils/RegionFileFactory.java @@ -0,0 +1,10 @@ +package io.anonymous.anonymous.utils; + +import io.anonymous.anonymous.data.RegionFile; + +import java.io.IOException; + +@FunctionalInterface +public interface RegionFileFactory { + RegionFile newFile(RegionCreatorInfo info) throws IOException; +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java index 5e06563..50896e2 100644 --- a/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java @@ -9,8 +9,8 @@ import me.earthme.luminol.config.flags.ConfigInfo; import me.earthme.luminol.config.flags.DoNotLoad; import me.earthme.luminol.config.flags.HotReloadUnsupported; import me.earthme.luminol.enums.EnumConfigCategory; -import me.earthme.luminol.enums.EnumRegionFormat; -import me.earthme.luminol.utils.BufferedLinearRegionFileFlusher; +import io.anonymous.anonymous.enums.EnumRegionFormat; +import io.anonymous.anonymous.data.BufferedLinearRegionFileFlusher; import net.minecraft.server.MinecraftServer; import org.jetbrains.annotations.Nullable; diff --git a/lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java b/lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java deleted file mode 100644 index 516b393..0000000 --- a/lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java +++ /dev/null @@ -1,1442 +0,0 @@ -package me.earthme.luminol.data; - -import abomination.IRegionFile; -import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; -import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; -import com.github.luben.zstd.Zstd; -import com.github.luben.zstd.ZstdInputStream; -import me.earthme.luminol.utils.BufferedLinearRegionFileFlusher; -import net.jpountz.lz4.LZ4Compressor; -import net.jpountz.lz4.LZ4Factory; -import net.jpountz.lz4.LZ4FastDecompressor; -import net.jpountz.xxhash.XXHash32; -import net.jpountz.xxhash.XXHashFactory; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.chunk.storage.RegionFile; -import net.minecraft.world.level.chunk.storage.RegionFileStorage; -import net.openhft.hashing.LongHashFunction; -import org.apache.commons.lang3.Validate; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.jspecify.annotations.NonNull; - -import java.io.*; -import java.lang.invoke.VarHandle; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; -import java.nio.file.StandardOpenOption; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class BufferedLinearRegionFile implements IRegionFile { - private static final double SWAP_FILE_AUTO_COMPACT_PERCENT = 3.0 / 5.0; // 60 % - private static final long SWAP_FILE_AUTO_COMPACT_SIZE = 1024 * 1024; // 1 MiB - - private static final long SWAP_FILE_SUPER_BLOCK = 0x1145141919810L; - private static final int SWAP_FILE_HASH_SEED = 0x0721; // ~(∠・ω< )⌒★ - private static final byte SWAP_FILE_VERSION = 0x02; // ver 2.0 - - private static final long MASTER_FILE_SUPER_BLOCK = -0x200812250269L; - private static final byte MASTER_FILE_VERSION = 0x02; // ver 2.0 - private static final byte MASTER_FILE_VERSION_BUCKET = 0x03; // ver 3.0 - - private static final long LINEAR_FILE_SUPER_BLOCK = 0xc3ff13183cca9d9aL; - - private static final int BUCKET_SHIFT = 6; - private static final int BUCKET_SIZE = 1 << BUCKET_SHIFT; - private static final int BUCKET_COUNT = 1024 / BUCKET_SIZE; - - private static final long MAX_SIZE_PER_CHUNK = RegionFile.MAX_CHUNK_SIZE; - - private static final StandardOpenOption[] SWAP_FILE_CHANNEL_OPTIONS = new StandardOpenOption[]{ - StandardOpenOption.CREATE, - StandardOpenOption.WRITE, - StandardOpenOption.READ, - StandardOpenOption.DELETE_ON_CLOSE - }; - - private static final class Bucket { - private final Object lock = new Object(); - - private final AtomicLong writeEpoch = new AtomicLong(); - private final AtomicLong syncedEpoch = new AtomicLong(); - private volatile boolean loaded = false; - } - - private final Bucket[] buckets = new Bucket[BUCKET_COUNT]; - - private final Path masterFilePath; - private final Path swapFilePath; - - private final ReadWriteLock regionObjectLock = new ReentrantReadWriteLock(); - private final XXHash32 xxHash32 = XXHashFactory.fastestInstance().hash32(); - private Sector[] sectors = new Sector[1024]; - private long currentAcquiredIndex = this.headerSize(); - private int xxHash32Seed = SWAP_FILE_HASH_SEED; - private FileChannel swapFileChannel; - - private final byte compressionLevel; - private final LinearMasterFileParser masterFileParser = new LinearMasterFileParser(); - private final CompressingOps compressingOps = new CompressingOps(); - - // managed by VarHandles following - private boolean closed = false; - private boolean beingSynced = false; - private boolean synced = false; - private long lastWritten = System.nanoTime(); - - private static final VarHandle CLOSED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "closed", boolean.class); - private static final VarHandle SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "synced", boolean.class); - private static final VarHandle BEING_SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "beingSynced", boolean.class); - private static final VarHandle LAST_WRITTEN_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "lastWritten", long.class); - - private final BufferedLinearRegionFileFlusher flusher; - - public BufferedLinearRegionFile(Path masterFilePath, int compressionLevel, @NotNull BufferedLinearRegionFileFlusher flusher) throws IOException { - this.masterFilePath = masterFilePath; - this.swapFilePath = Path.of(this.masterFilePath.toString() + ".swp"); - - Validate.inclusiveBetween(1, 22, compressionLevel); - - for (int i = 0; i < this.buckets.length; i++) { - this.buckets[i] = new Bucket(); - } - - this.compressionLevel = (byte) compressionLevel; - - this.cleanUpSwapFile(); - this.initSwapFile(); - this.tryLoadOldBlinearMasterFileData(); - - this.flusher = flusher; - this.flusher.addFile(this); - } - - private static void writeFullyAt(FileChannel channel, @NonNull ByteBuffer buf, long startOffset) throws IOException { - long offset = startOffset; - while (buf.hasRemaining()) { - offset += channel.write(buf, offset); - } - } - - private static void readFullyAt(FileChannel channel, @NonNull ByteBuffer buf, long startOffset) throws IOException { - long offset = startOffset; - while (buf.hasRemaining()) { - final int read = channel.read(buf, offset); - if (read < 0) throw new EOFException("Unexpected EOF at offset " + offset); - offset += read; - } - } - - private void cleanUpSwapFile() throws IOException { - Files.deleteIfExists(this.swapFilePath); - } - - private void ensureBucketLoaded(int chunkIndex) throws IOException { - final int bucketIndex = chunkIndex >> BUCKET_SHIFT; - final Bucket bucket = this.buckets[bucketIndex]; - - // bucket lock -> master read lock -> swap write lock - synchronized (bucket.lock) { - if (bucket.loaded) { - return; - } - - this.masterFileParser.loadBucketsFor(this.masterFilePath, bucketIndex); - bucket.loaded = true; - } - } - - private long markBucketDirty(int chunkIndex) { - return this.markBucketDirtyByIndex(chunkIndex >> BUCKET_SHIFT); - } - - private long markBucketDirtyByIndex(int bucketIndex) { - final Bucket bucket = this.buckets[bucketIndex]; - - return bucket.writeEpoch.incrementAndGet(); - } - - private long getBucketWriteEpoch(int bucketIndex) { - final Bucket bucket = this.buckets[bucketIndex]; - - return bucket.writeEpoch.get(); - } - - private long getBucketSyncedEpoch(int bucketIndex) { - final Bucket bucket = this.buckets[bucketIndex]; - - return bucket.syncedEpoch.get(); - } - - private void markBucketSynced(int bucketIndex, long syncedEpoch) { - final Bucket bucket = this.buckets[bucketIndex]; - - bucket.syncedEpoch.accumulateAndGet(syncedEpoch, Math::max); - } - - private boolean isBucketDirty(int bucketIndex) { - final Bucket bucket = this.buckets[bucketIndex]; - - return bucket.writeEpoch.get() != bucket.syncedEpoch.get(); - } - - public boolean markAsBeingSynced() { - return BEING_SYNCED_HANDLE.compareAndSet(this, false, true); - } - - - public long getLastWritten() { - return (long) LAST_WRITTEN_HANDLE.getVolatile(this); - } - - public boolean shouldSync() { - return !((boolean) SYNCED_HANDLE.getVolatile(this)); - } - - public boolean softReadLock() { - // not done close logic yet - return this.regionObjectLock.readLock().tryLock(); - } - - public void releaseReadLock() { - this.regionObjectLock.readLock().unlock(); - } - - public boolean isClosedRaw() { - return (boolean) CLOSED_HANDLE.getVolatile(this); - } - - public boolean isClosed() { - this.regionObjectLock.readLock().lock(); - try { - return (boolean) CLOSED_HANDLE.getVolatile(this); - } finally { - this.regionObjectLock.readLock().unlock(); - } - } - - public void syncIfNeeded() throws IOException { - // the sync operation is just coping the data from swap file to the master file - // so we could acquire read lock simply so that we won't block any other read operations - try { - // skip if closed already - if (this.isClosed()) { - return; - } - - this.syncToMasterFile(); - } finally { - BEING_SYNCED_HANDLE.setVolatile(this, false); // mark as not being synced - } - } - - private void syncToMasterFile() throws IOException { - // prevent multiple syncs in the same time - if (!SYNCED_HANDLE.compareAndSet(this, false, true)) { - return; - } - - try { - // this.masterFileParser.writeMainFile(this.masterFilePath); - this.masterFileParser.writeMainFileBucketed(this.masterFilePath); - } catch (Throwable e) { - // set back - SYNCED_HANDLE.setVolatile(this, false); - - throw new IOException("Failed to sync to master file!", e); - } - } - - private void tryLoadOldBlinearMasterFileData() throws IOException { - this.masterFileParser.tryParseMainFileOld(this.masterFilePath); - } - - private void initSwapFile() throws IOException { - this.swapFileChannel = FileChannel.open( - this.swapFilePath, - SWAP_FILE_CHANNEL_OPTIONS - ); - - // fill default sectors - for (int i = 0; i < 1024; i++) { - this.sectors[i] = new Sector(i, this.headerSize(), 0); - } - } - - private void recalculateAcquiredIndex() { - long newValue = this.headerSize(); - - for (Sector sector : this.sectors) { - if (sector.hasData()) { - newValue = Math.max(newValue, sector.offset + sector.length); - } - } - - this.currentAcquiredIndex = newValue; - } - - private void writeSwapFileHeaders(boolean forceFile, boolean forceMeta) throws IOException { - final ByteBuffer buffer = ByteBuffer.allocate(this.headerSize()); - - buffer.putLong(SWAP_FILE_SUPER_BLOCK); // Magic - buffer.put(SWAP_FILE_VERSION); // Version - buffer.putInt(this.xxHash32Seed); // XXHash32 seed - buffer.putLong(this.currentAcquiredIndex); // Acquired index - - for (Sector sector : this.sectors) { - // encode each sector - buffer.put(sector.getEncoded()); - } - - buffer.flip(); - - writeFullyAt(this.swapFileChannel, buffer, 0); - - if (forceFile) { - this.swapFileChannel.force(forceMeta); - } - } - - private int sectorSize() { - return this.sectors.length * Sector.sizeOfSingle(); - } - - private int headerSize() { - int result = 0; - - result += Long.BYTES; // Magic - result += Byte.BYTES; // Version - result += Integer.BYTES; // XXHash32 seed - result += Long.BYTES; // Acquired index - result += this.sectorSize(); // Sectors - - return result; - } - - private void flushInternal() throws IOException { - boolean initiallySyncRequired; - - this.regionObjectLock.writeLock().lock(); - try { - if (this.isClosedRaw()) { - return; - } - - long spareSize = this.currentAcquiredIndex; - - spareSize -= this.headerSize(); - for (Sector sector : this.sectors) { - // skip no data sectors - if (!sector.hasData()) { - continue; - } - - spareSize -= sector.length; - } - - long sectorSize = 0; - for (Sector sector : this.sectors) { - // skip no data sectors - if (!sector.hasData()) { - continue; - } - - sectorSize += sector.length; - } - - final boolean compactRequested = spareSize > SWAP_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) sectorSize) * SWAP_FILE_AUTO_COMPACT_PERCENT; - - // try auto compact to clean the garbage area - if (compactRequested) { - // do compact - this.compactSwapFile(); - } - - // prevent syncing after compact because it could be time costing sometimes - initiallySyncRequired = !Files.exists(this.masterFilePath) && !compactRequested; - } finally { - this.regionObjectLock.writeLock().unlock(); - } - - if (initiallySyncRequired) { - this.syncToMasterFile(); - } - } - - private void closeInternal() throws IOException { - this.syncIfNeeded(); - - this.regionObjectLock.writeLock().lock(); - try { - this.markClosed(); - - this.swapFileChannel.close(); - } finally { - this.regionObjectLock.writeLock().unlock(); - } - } - - private void markClosed() throws IOException { - if (!CLOSED_HANDLE.compareAndSet(this, false, true)) { - throw new IOException("Already closed!"); - } - - this.flusher.removeFile(this); - } - - private void compactSwapFile() throws IOException { - this.writeSwapFileHeaders(true, true); // save headers for compact - - final Sector[] newSectorsToBeReplaced = new Sector[this.sectors.length]; - - for (int i = 0; i < this.sectors.length; i++) { - final Sector old = this.sectors[i]; - - if (old.hasData()) { - newSectorsToBeReplaced[i] = old; - continue; - } - - // note: - // we reset length to 0 and this would make length <= newLength(which is >= 0) is always true. - // so that the following write operation wouldn't override the data of other sectors - // see the write method in Sector class - newSectorsToBeReplaced[i] = new Sector(i, 0, 0); - } - - long newAcquiredIndex; - - final Path targetTemp = new File(this.swapFilePath.toString() + ".tmp").toPath(); - - try (FileChannel tempChannel = FileChannel.open( - targetTemp, - StandardOpenOption.CREATE_NEW, - StandardOpenOption.WRITE, - StandardOpenOption.READ, - StandardOpenOption.TRUNCATE_EXISTING - )) { - long offsetPointer = this.headerSize(); - tempChannel.position(offsetPointer); - - for (Sector sector : newSectorsToBeReplaced) { - // skip cleared or no data-contained sectors - if (!sector.hasData()) { - continue; - } - - // transfer to target - sector.transferTo(this.swapFileChannel, tempChannel); - - // recalculate the offset and length - final Sector newRecalculated = new Sector(sector.index, offsetPointer, sector.length); - newRecalculated.hasData = true; - - offsetPointer += sector.length; - newSectorsToBeReplaced[sector.index] = newRecalculated; // update sector infos - } - - tempChannel.force(true); - - newAcquiredIndex = offsetPointer; - } catch (Throwable ex) { - // recalculate acquired index - this.recalculateAcquiredIndex(); - // delete the target temp file - Files.deleteIfExists(targetTemp); - // fast-fail - // note: we don't block new write operations here as this is recoverable - throw new IOException("Failed to compact swap file!", ex); - } - - this.swapFileChannel.close(); - - // replace swap file - try { - Files.move( - targetTemp, - this.swapFilePath, - StandardCopyOption.REPLACE_EXISTING, - StandardCopyOption.ATOMIC_MOVE - ); - } catch (Throwable e) { - // atomic move might be unsupported on some file systems, so give it an attempt to retry without atomic move - try { - Files.move( - targetTemp, - this.swapFilePath, - StandardCopyOption.REPLACE_EXISTING - ); - } catch (Throwable ex) { - // now we are totally failed - e.addSuppressed(ex); - - // delete file that failed to replace - Files.deleteIfExists(targetTemp); - // recalculate acquired index - this.recalculateAcquiredIndex(); - // reopen closed channel - this.reopenSwapFileChannel(); - // fast-fail - this.markClosed(); // prevent new writing & sync operations - throw new IOException("Failed to replace original swap file!", e); - } - } - - - try { - // reopen file channel - this.reopenSwapFileChannel(); - - // replace with recalculated file headers - this.sectors = newSectorsToBeReplaced; - this.currentAcquiredIndex = newAcquiredIndex; - - // flush to file - this.writeSwapFileHeaders(true, true); - } catch (Throwable ex) { - // we are totally failed here, - // directly mark as closed as the swap file is already replaced, and we failed to update the - // data which is still in the memory - // - // which means we might write any data into any incorrect indexed sectors which will blow the whole data - this.markClosed(); - throw new IOException(ex); - } - } - - private void reopenSwapFileChannel() throws IOException { - if (this.swapFileChannel.isOpen()) { - this.swapFileChannel.close(); - } - - this.swapFileChannel = FileChannel.open( - this.swapFilePath, - SWAP_FILE_CHANNEL_OPTIONS - ); - } - - private void writeChunkDataRaw(int index, ByteBuffer chunkData, boolean skipSync) throws IOException { - final ByteBuffer committed = this.compressingOps.compress(chunkData); // run compression out of lock - - this.regionObjectLock.writeLock().lock(); - try { - final Sector sector = this.sectors[index]; - - sector.store(committed, this.swapFileChannel); - if (!skipSync) { - this.markBucketDirty(index); - } - } finally { - this.regionObjectLock.writeLock().unlock(); - } - - if (skipSync) { - return; - } - - this.markAsToSync(); - } - - private @Nullable ByteBuffer readChunkDataRaw(int index) throws IOException { - return this.readChunkDataRaw(index, true); - } - - private @Nullable ByteBuffer readChunkDataRaw(int index, boolean acquireLock) throws IOException { - final ByteBuffer raw; - - this.regionObjectLock.readLock().lock(); - try { - final Sector sector = this.sectors[index]; - - if (!sector.hasData()) { - return null; - } - - raw = sector.read(this.swapFileChannel); - } finally { - this.regionObjectLock.readLock().unlock(); - } - - return this.compressingOps.decompress(raw); - } - - private void clearChunkData(int index) throws IOException { - this.ensureBucketLoaded(index); - - this.regionObjectLock.writeLock().lock(); - try { - final Sector sector = this.sectors[index]; - - sector.clear(); - this.markBucketDirty(index); - } finally { - this.regionObjectLock.writeLock().unlock(); - } - - this.markAsToSync(); - } - - private void markAsToSync() { - SYNCED_HANDLE.setVolatile(this, false); // mark as unsynced - LAST_WRITTEN_HANDLE.setVolatile(this, System.nanoTime()); // update last written time - } - - private static int getChunkIndex(int x, int z) { - return (x & 31) + ((z & 31) << 5); - } - - private boolean hasData(int index) throws IOException { - this.ensureBucketLoaded(index); - - this.regionObjectLock.readLock().lock(); - try { - return this.sectors[index].hasData(); - } finally { - this.regionObjectLock.readLock().unlock(); - } - } - - private void writeChunk(int x, int z, @NotNull ByteBuffer data) throws IOException { - final int chunkIndex = getChunkIndex(x, z); - - if (data.remaining() > MAX_SIZE_PER_CHUNK) { - throw new RegionFileStorage.RegionFileSizeException("Writing too large chunk, limit : " + MAX_SIZE_PER_CHUNK + " but got : " + data.remaining()); - } - - final int oldPositionOfData = data.position(); - final int xxHash32OfData = this.xxHash32.hash(data, this.xxHash32Seed); - data.position(oldPositionOfData); - - // uncompressed length(int) + timestamp(long) + xxhash32(int) - final ByteBuffer chunkSectionBuilder = ByteBuffer.allocate(data.remaining() + 4 + 8 + 4); - - chunkSectionBuilder.putInt(data.remaining()); // Length(int) - chunkSectionBuilder.putLong(System.currentTimeMillis()); // Timestamp(long) - chunkSectionBuilder.putInt(xxHash32OfData); // xxHash32 of the original data(int) - chunkSectionBuilder.put(data); // Data(bytes) - chunkSectionBuilder.flip(); - - this.writeChunkDataRaw(chunkIndex, chunkSectionBuilder, false); - } - - private @Nullable ByteBuffer readChunk(int x, int z) throws IOException { - final int chunkIndex = getChunkIndex(x, z); - - this.ensureBucketLoaded(chunkIndex); - - final ByteBuffer data = this.readChunkDataRaw(chunkIndex); - - if (data == null) { - return null; - } - - final int length = data.getInt(); // compressed length(int) - final long timestamp = data.getLong(); // TODO use this timestamp(long) for something? - final int dataXXHash32 = data.getInt(); // XXHash32 for validation(int) - - final IOException xxHash32CheckFailedEx = this.checkXXHash32(dataXXHash32, data); - if (xxHash32CheckFailedEx != null) { - throw xxHash32CheckFailedEx; // prevent from loading - } - - return data; - } - - private @Nullable IOException checkXXHash32(long originalXXHash32, @NotNull ByteBuffer input) { - final int oldPositionOfInput = input.position(); - final int currentXXHash32 = this.xxHash32.hash(input, this.xxHash32Seed); - input.position(oldPositionOfInput); - - if (originalXXHash32 != currentXXHash32) { - return new IOException("XXHash32 check failed ! Expected: " + originalXXHash32 + ",but got: " + currentXXHash32); - } - - return null; - } - - @Override - public Path getPath() { - return this.masterFilePath; - } - - @Override - public DataInputStream getChunkDataInputStream(@NotNull ChunkPos pos) throws IOException { - final ByteBuffer data = this.readChunk(pos.x(), pos.z()); - - if (data == null) { - return null; - } - - return new DataInputStream(new ByteBufferInputStream(data)); - } - - @Override - public boolean doesChunkExist(@NotNull ChunkPos pos) throws IOException { - return this.hasData(getChunkIndex(pos.x(), pos.z())); - } - - @Override - public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { - return new DataOutputStream(new ChunkBufferHelper(pos)); - } - - @Override - public void clear(@NotNull ChunkPos pos) throws IOException { - this.clearChunkData(getChunkIndex(pos.x(), pos.z())); - } - - @Override - public boolean hasChunk(@NotNull ChunkPos pos) { - try { - return this.hasData(getChunkIndex(pos.x(), pos.z())); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void write(@NotNull ChunkPos pos, ByteBuffer buf) throws IOException { - - final int chunkIndex = getChunkIndex(pos.x(), pos.z()); - - this.ensureBucketLoaded(chunkIndex); - - this.writeChunk(pos.x(), pos.z(), buf); - } - - // MCC 的玩意,这东西也用不上给Linear了() - @Override - public CompoundTag getOversizedData(int x, int z) { - return null; - } - - @Override - public boolean isOversized(int x, int z) { - return false; - } - - @Override - public boolean recalculateHeader() { - return false; - } - - @Override - public void setOversized(int x, int z, boolean oversized) { - - } - // MCC end - - @Override - public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { - final DataOutputStream out = this.getChunkDataOutputStream(pos); - - return new MoonriseRegionFileIO.RegionDataController.WriteData( - data, MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, - out, regionFile -> out.close() - ); - } - - @Override - public void flush() throws IOException { - this.flushInternal(); - } - - @Override - public void close() throws IOException { - this.closeInternal(); - } - - public static class ByteBufferInputStream extends InputStream { - protected final ByteBuffer internal; - - public ByteBufferInputStream(ByteBuffer buf) { - this.internal = buf; - } - - @Override - public int available() { - return this.internal.remaining(); - } - - @Override - public int read() throws IOException { - return this.internal.hasRemaining() ? (this.internal.get() & 0xFF) : -1; - } - - @Override - public int read(byte @NotNull [] bytes, int off, int len) throws IOException { - if (!this.internal.hasRemaining()) return -1; - len = Math.min(len, this.internal.remaining()); - this.internal.get(bytes, off, len); - return len; - } - } - - // here we use this tool to prevent the swap file goes too large - // sometimes when a region contains all chunks, it might be very huge without any compressions(around 100MiB) - private static class CompressingOps { - private final LZ4Compressor lz4Compressor = LZ4Factory.fastestInstance().fastCompressor(); - private final LZ4FastDecompressor lz4Decompressor = LZ4Factory.fastestInstance().fastDecompressor(); - - public @NotNull ByteBuffer compress(@NotNull ByteBuffer in) { - final int bufferLenToAllocate = this.lz4Compressor.maxCompressedLength(in.remaining()); - final ByteBuffer result = ByteBuffer.allocate(bufferLenToAllocate + 4); - - result.putInt(in.remaining()); - this.lz4Compressor.compress(in, result); - - return result.flip(); - } - - public @NotNull ByteBuffer decompress(@NotNull ByteBuffer flippedIn) { - final int originalLen = flippedIn.getInt(); - final byte[] raw = new byte[flippedIn.remaining()]; - flippedIn.get(raw); - - final byte[] decompressed = new byte[originalLen]; - this.lz4Decompressor.decompress(raw, decompressed); - - return ByteBuffer.wrap(decompressed); - } - } - - public class Sector { - private final int index; - private long offset; - private long length; - private boolean hasData = false; - - private Sector(int index, long offset, long length) { - this.index = index; - this.offset = offset; - this.length = length; - } - - public void transferTo(@NotNull FileChannel source, @NotNull FileChannel target) throws IOException { - long transferred = 0; - while (transferred < this.length) { - transferred += source.transferTo( - this.offset + transferred, - this.length - transferred, - target); - } - } - - public @NotNull ByteBuffer read(@NotNull FileChannel channel) throws IOException { - final ByteBuffer result = ByteBuffer.allocate((int) this.length); - - readFullyAt(channel, result, this.offset); - - result.flip(); - return result; - } - - public void store(@NotNull ByteBuffer newData, @NotNull FileChannel channel) throws IOException { - final long oldLength = this.length; - final long newDataLength = newData.remaining(); - - this.hasData = true; - this.length = newDataLength; - - // data is smaller or its length equals to the local buffer we hold, write it directly - if (newDataLength <= oldLength) { - writeFullyAt(channel, newData, this.offset); - - return; - } - - // or we will append to the end of file - this.offset = BufferedLinearRegionFile.this.currentAcquiredIndex; - - BufferedLinearRegionFile.this.currentAcquiredIndex += this.length; - - writeFullyAt(channel, newData, this.offset); - } - - private @NotNull ByteBuffer getEncoded() { - final ByteBuffer buffer = ByteBuffer.allocate(sizeOfSingle()); - - buffer.putLong(this.offset); - buffer.putLong(this.length); - buffer.put((byte) (this.hasData ? 1 : 0)); - buffer.flip(); - - return buffer; - } - - public void restoreFrom(@NotNull ByteBuffer buffer) { - this.offset = buffer.getLong(); - this.length = buffer.getLong(); - this.hasData = buffer.get() == 1; - - if (this.length < 0 || this.offset < 0) { - throw new IllegalStateException("Invalid sector data: " + this); - } - } - - public void clear() { - this.hasData = false; - } - - public boolean hasData() { - return this.hasData; - } - - static int sizeOfSingle() { - // offset + length hasData - return Long.BYTES * 2 + 1; - } - } - - private class ChunkBufferHelper extends ByteArrayOutputStream { - private final ChunkPos pos; - - private ChunkBufferHelper(ChunkPos pos) { - this.pos = pos; - } - - @Override - public void close() throws IOException { - ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); - - final int chunkIndex = getChunkIndex(this.pos.x(), this.pos.z()); - - BufferedLinearRegionFile.this.ensureBucketLoaded(chunkIndex); - BufferedLinearRegionFile.this.writeChunk(this.pos.x(), this.pos.z(), bytebuffer); - - BufferedLinearRegionFile.this.flushInternal(); - } - } - - private class LinearMasterFileParser { - // V3 new format layout: - // [0, 14): header — superblock(8) + version(1) + compressionLevel(1) + xxHash32Seed(4) - // [14, 142): position table — BUCKET_COUNT(16) × long(8) each; 0 = no data for that bucket - // [526, EOF): bucket data — originalLen(int) + compressedLen(int) + compressedData - private static final long V3_POS_TABLE_OFFSET = 14L; - private static final int V3_POS_TABLE_SIZE = BUCKET_COUNT * Long.BYTES; // 128 - private static final long V3_DATA_AREA_OFFSET = V3_POS_TABLE_OFFSET + V3_POS_TABLE_SIZE; // 142 - - private final ReadWriteLock masterFileLock = new ReentrantReadWriteLock(); - - public void writeMainFileBucketed(@NotNull Path mainFile) throws IOException { - final Path tmpFilePath = Path.of(mainFile + ".tmp"); - final long[] syncedBucketEpochs = new long[BUCKET_COUNT]; - final long[] newPositionTable = new long[BUCKET_COUNT]; - - // note: there is no necessary hold the write lock for this stuff - // as the truly write operations only happens on replacing the master file (see the file move call below this hunk) - // and we had CAS flags to prevent multiple synchronization happening at the same time - - // Open old file to copy non-dirty buckets - long[] oldPositionTable = null; - FileChannel oldChannel = null; - - this.masterFileLock.writeLock().lock(); - try { - if (Files.exists(mainFile)) { - try { - oldChannel = FileChannel.open(mainFile, StandardOpenOption.READ); - if (oldChannel.size() >= V3_DATA_AREA_OFFSET) { - final ByteBuffer hdr = ByteBuffer.allocate(14); - readFullyAt(oldChannel, hdr, 0); - hdr.flip(); - if (hdr.getLong() == MASTER_FILE_SUPER_BLOCK && hdr.get() == MASTER_FILE_VERSION_BUCKET) { - oldPositionTable = this.parseOffsetTable(oldChannel); - } else { - oldChannel.close(); - oldChannel = null; - } - } else { - oldChannel.close(); - oldChannel = null; - } - } catch (Throwable e) { - if (oldChannel != null) { - try { - oldChannel.close(); - } catch (IOException e2) { - e.addSuppressed(e2); - } - } - - throw new RuntimeException(e); - } - } - - BufferedLinearRegionFile.this.regionObjectLock.readLock().lock(); - try (FileChannel outChannel = FileChannel.open(tmpFilePath, - StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { - - // Write header (14 bytes) - final ByteBuffer header = ByteBuffer.allocate(14); - header.putLong(MASTER_FILE_SUPER_BLOCK); - header.put(MASTER_FILE_VERSION_BUCKET); - header.put(BufferedLinearRegionFile.this.compressionLevel); - header.putInt(BufferedLinearRegionFile.this.xxHash32Seed); - header.flip(); - writeFullyAt(outChannel, header, 0); - - // Write position table placeholder (all zeros, filled in at the end) - writeFullyAt(outChannel, ByteBuffer.allocate(V3_POS_TABLE_SIZE), V3_POS_TABLE_OFFSET); - - long dataOffset = V3_DATA_AREA_OFFSET; - - for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) { - final long bucketWriteEpoch = BufferedLinearRegionFile.this.getBucketWriteEpoch(bucketIndex); - final boolean isBucketDirty = bucketWriteEpoch != BufferedLinearRegionFile.this.getBucketSyncedEpoch(bucketIndex); - - if (isBucketDirty) { - final int baseChunk = bucketIndex << BUCKET_SHIFT; - final ByteArrayOutputStream rawBuf = new ByteArrayOutputStream(); - final DataOutputStream rawOut = new DataOutputStream(rawBuf); - boolean hasAny = false; - - for (int i = 0; i < BUCKET_SIZE; i++) { - // swap read lock - final ByteBuffer data = BufferedLinearRegionFile.this.readChunkDataRaw(baseChunk + i, false); - - // note: null -> no data contained - if (data == null) { - rawOut.writeInt(0); - } else { - final byte[] arr = new byte[data.remaining()]; - data.get(arr); - rawOut.writeInt(arr.length); - rawOut.write(arr); - hasAny = true; - } - } - rawOut.flush(); - - if (hasAny) { - final byte[] raw = rawBuf.toByteArray(); - final byte[] compressed = Zstd.compress(raw, BufferedLinearRegionFile.this.compressionLevel); - - newPositionTable[bucketIndex] = dataOffset; - - final ByteBuffer bucketBuf = ByteBuffer.allocate(8 + compressed.length); - bucketBuf.putInt(raw.length); // original (uncompressed) length - bucketBuf.putInt(compressed.length); // compressed length - bucketBuf.put(compressed); - bucketBuf.flip(); - writeFullyAt(outChannel, bucketBuf, dataOffset); - dataOffset += bucketBuf.limit(); - } - // else: newPositionTable[bucketIndex] stays 0 - - syncedBucketEpochs[bucketIndex] = bucketWriteEpoch; - } else { - // Not dirty: copy bytes from old file if available - if (oldPositionTable != null && oldPositionTable[bucketIndex] != 0) { - final long oldOffset = oldPositionTable[bucketIndex]; - final ByteBuffer lensBuf = ByteBuffer.allocate(8); - readFullyAt(oldChannel, lensBuf, oldOffset); - lensBuf.flip(); - lensBuf.getInt(); // skip originalLen - final int compressedLen = lensBuf.getInt(); - - final long bucketTotalSize = 8L + compressedLen; - newPositionTable[bucketIndex] = dataOffset; - outChannel.position(dataOffset); - long remaining = bucketTotalSize; - long srcPos = oldOffset; - while (remaining > 0) { - final long transferred = oldChannel.transferTo(srcPos, remaining, outChannel); - srcPos += transferred; - remaining -= transferred; - } - dataOffset += bucketTotalSize; - } - } - } - - // Write the finalized position table - final ByteBuffer posTableBuf = ByteBuffer.allocate(V3_POS_TABLE_SIZE); - for (final long pos : newPositionTable) { - posTableBuf.putLong(pos); - } - posTableBuf.flip(); - writeFullyAt(outChannel, posTableBuf, V3_POS_TABLE_OFFSET); - - outChannel.force(true); - } finally { - BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock(); - - if (oldChannel != null) { - oldChannel.close(); - } - } - - try { - Files.move(tmpFilePath, mainFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (Throwable e) { - - try { - Files.move(tmpFilePath, mainFile, StandardCopyOption.REPLACE_EXISTING); - } catch (Throwable ex) { - Files.deleteIfExists(tmpFilePath); - - e.addSuppressed(ex); - - throw new IOException("Failed to replace master file!", e); - } - } - } finally { - this.masterFileLock.writeLock().unlock(); - } - - for (int i = 0; i < syncedBucketEpochs.length; i++) { - final long syncedEpoch = syncedBucketEpochs[i]; - if (syncedEpoch != 0L) { - BufferedLinearRegionFile.this.markBucketSynced(i, syncedEpoch); - } - } - } - - private void loadBucketsFor(Path file, int bucketIndex) throws IOException { - final int beginChunkIndex = bucketIndex << BUCKET_SHIFT; - - this.masterFileLock.readLock().lock(); - - try { - if (!Files.exists(file)) { - return; - } - - try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) { - if (channel.size() < V3_DATA_AREA_OFFSET) { - return; - } - - final ByteBuffer headerBuf = ByteBuffer.allocate(14); - readFullyAt(channel, headerBuf, 0); - headerBuf.flip(); - - final long superblock = headerBuf.getLong(); - if (superblock != MASTER_FILE_SUPER_BLOCK) - throw new IOException("Invalid superblock " + superblock + "!"); - - final byte version = headerBuf.get(); - if (version != MASTER_FILE_VERSION_BUCKET) - throw new IOException("Unknown version: " + version); - - // compressionLevel and hashSeed consumed but not used here - headerBuf.get(); - headerBuf.getInt(); - - final long[] posTable = this.parseOffsetTable(channel); - - // New format: jump directly to bucket data - final long bucketDataOffset = posTable[bucketIndex]; - if (bucketDataOffset == 0) return; - - final ByteBuffer lensBuf = ByteBuffer.allocate(8); - readFullyAt(channel, lensBuf, bucketDataOffset); - lensBuf.flip(); - final int originalLen = lensBuf.getInt(); - final int compressedLen = lensBuf.getInt(); - - final byte[] compressedData = new byte[compressedLen]; - readFullyAt(channel, ByteBuffer.wrap(compressedData), bucketDataOffset + 8); - - final ByteBuffer decompressed = ByteBuffer.wrap(Zstd.decompress(compressedData, originalLen)); - this.loadChunksFromBucketData(decompressed, beginChunkIndex); - } - } finally { - this.masterFileLock.readLock().unlock(); - } - } - - private long @NonNull [] parseOffsetTable(FileChannel channel) throws IOException { - final ByteBuffer buf = ByteBuffer.allocate(V3_POS_TABLE_SIZE); - readFullyAt(channel, buf, V3_POS_TABLE_OFFSET); - buf.flip(); - - final long[] table = new long[BUCKET_COUNT]; - - Arrays.fill(table, 0L); - for (int i = 0; i < BUCKET_COUNT; i++) { - final long pos = buf.getLong(); - table[i] = pos; - } - - return table; - } - - private void loadChunksFromBucketData(ByteBuffer decompressed, int beginChunkIndex) throws IOException { - for (int chunkIndex = beginChunkIndex; chunkIndex < beginChunkIndex + BUCKET_SIZE; chunkIndex++) { - final int chunkSectionDataSize = decompressed.getInt(); - if (chunkSectionDataSize <= 0) continue; - - final byte[] chunkSectionData = new byte[chunkSectionDataSize]; - decompressed.get(chunkSectionData); - - BufferedLinearRegionFile.this.writeChunkDataRaw(chunkIndex, ByteBuffer.wrap(chunkSectionData), true); - } - } - - private void parseLinearV2(@NonNull DataInputStream ioStream, Path file) throws IOException { - try (ioStream) { - ioStream.readLong(); // Skip newestTimestamp (Long) - - byte gridSize = ioStream.readByte(); - if (gridSize != 1 && gridSize != 2 && gridSize != 4 && gridSize != 8 && gridSize != 16 && gridSize != 32) - throw new RuntimeException("Invalid grid size: " + gridSize + " file " + file); - int bucketSize = 32 / gridSize; - - ioStream.readInt(); // Skip region_x (Int) - ioStream.readInt(); // Skip region_z (Int) - - ioStream.skipBytes(128); // Skip existence bitmap - - // Skip NBT features - while (true) { - byte featureNameLength = ioStream.readByte(); - if (featureNameLength == 0) break; - byte[] featureNameBytes = new byte[featureNameLength]; - ioStream.readFully(featureNameBytes); - ioStream.readInt(); // featureValue - } - - // Read bucket metadata - int totalBuckets = gridSize * gridSize; - int[] bucketSizes = new int[totalBuckets]; - byte[] bucketCompressionLevels = new byte[totalBuckets]; - long[] bucketHashes = new long[totalBuckets]; - for (int i = 0; i < totalBuckets; i++) { - bucketSizes[i] = ioStream.readInt(); - bucketCompressionLevels[i] = ioStream.readByte(); - bucketHashes[i] = ioStream.readLong(); - } - - // Read and decompress each bucket, load chunks into swap - for (int bx = 0; bx < gridSize; bx++) { - for (int bz = 0; bz < gridSize; bz++) { - int bucketIdx = bx * gridSize + bz; - - if (bucketSizes[bucketIdx] <= 0) continue; - - byte[] compressedBucket = new byte[bucketSizes[bucketIdx]]; - ioStream.readFully(compressedBucket); - - long rawHash = LongHashFunction.xx().hashBytes(compressedBucket); - if (rawHash != bucketHashes[bucketIdx]) { - throw new IOException("Region file hash incorrect for bucket " + bucketIdx + " in " + file); - } - - ByteArrayInputStream bucketByteStream = new ByteArrayInputStream(compressedBucket); - ZstdInputStream zstdStream = new ZstdInputStream(bucketByteStream); - ByteBuffer bucketBuffer = ByteBuffer.wrap(zstdStream.readAllBytes()); - zstdStream.close(); - - for (int cx = 0; cx < bucketSize; cx++) { - for (int cz = 0; cz < bucketSize; cz++) { - int chunkX = bx * bucketSize + cx; - int chunkZ = bz * bucketSize + cz; - int chunkIndex = chunkX + chunkZ * 32; - - int chunkSize = bucketBuffer.getInt(); - long timestamp = bucketBuffer.getLong(); - - if (chunkSize > 0) { - // chunkSize includes the 8 bytes of timestamp already written - int dataLen = chunkSize - 8; - byte[] chunkData = new byte[dataLen]; - bucketBuffer.get(chunkData); - - // Mark bucket as loaded. writeChunk() bumps the bucket epoch so it gets synced to the new master format. - final int blinearBucketIndex = chunkIndex >> BUCKET_SHIFT; - final Bucket bucket = BufferedLinearRegionFile.this.buckets[blinearBucketIndex]; - - synchronized (bucket.lock) { - bucket.loaded = true; - } - - // Use writeChunk to go through the full path (adds length + timestamp + xxhash header) - BufferedLinearRegionFile.this.writeChunk(chunkX, chunkZ, ByteBuffer.wrap(chunkData)); - } - } - } - } - } - - // Footer validation - long footerSuperBlock = ioStream.readLong(); - if (footerSuperBlock != LINEAR_FILE_SUPER_BLOCK) { - throw new IOException("Footer superblock invalid " + file); - } - } - } - - private boolean tryParseBlinearV2(@NotNull DataInputStream ioStream, Path file) throws IOException { - final byte version = ioStream.readByte(); - - // we will parse dynamically (V3) - if (version == MASTER_FILE_VERSION_BUCKET) { - ioStream.close(); - return false; - } - - if (version != MASTER_FILE_VERSION) - throw new RuntimeException("Invalid version: " + version + " in " + file); - - // Skip newestTimestamp (Long) + Compression level (Byte): Unused. - ioStream.skipBytes(9); - - try (final ZstdInputStream decompressStream = new ZstdInputStream(ioStream)) { - // only used as a helper stream - // the parent stream will be closed in the try-catch block upper - final DataInputStream decompressedStreamHelper = new DataInputStream(decompressStream); - - for (int index = 0; index < 1024; index++) { - int size = decompressedStreamHelper.readInt(); // len - - if (size > 0) { - byte[] sectorData = new byte[size]; - decompressedStreamHelper.readFully(sectorData, 0, size); // data - - final ByteBuffer sectorDataNioBuffer = ByteBuffer.wrap(sectorData); - - final int bucketIndex = index >> BUCKET_SHIFT; - final Bucket bucket = BufferedLinearRegionFile.this.buckets[bucketIndex]; - - synchronized (bucket.lock) { - bucket.loaded = true; - } - - BufferedLinearRegionFile.this.writeChunkDataRaw(index, sectorDataNioBuffer, false); - } - } - } - - return true; - } - - @Contract(value = "_ -> new", pure = true) - public static int @NotNull [] coordinatesFromIndex(int chunkIndex) { - int x = chunkIndex & 31; - int z = (chunkIndex >> 5) & 31; - return new int[]{x, z}; - } - - private void parseLinearV1(@NotNull DataInputStream ioStream) throws IOException { - // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused. - ioStream.skipBytes(11); - // Skip chunk data len(Int)(Unused). - ioStream.skipBytes(4); - // Skip data hash (Long): Unused. - ioStream.skipBytes(8); - - try (final ZstdInputStream decompressedStream = new ZstdInputStream(ioStream)) { - // only used as a helper stream - // the parent stream will be closed in the try-catch block upper - final DataInputStream bufferHelper = new DataInputStream(decompressedStream); - - final int[] chunkStarts = new int[1024]; - for (int i = 0; i < 1024; i++) { - chunkStarts[i] = bufferHelper.readInt(); - bufferHelper.skipBytes(4); // Skip timestamps (Int): Unused. - } - - for (int i = 0; i < 1024; i++) { - if (chunkStarts[i] > 0) { - int size = chunkStarts[i]; - byte[] chunkData = new byte[size]; - bufferHelper.readFully(chunkData); - - final ByteBuffer chunkDataNioBuffer = ByteBuffer.wrap(chunkData); - - final int[] posByAxis = coordinatesFromIndex(i); - - final int x = posByAxis[0]; - final int z = posByAxis[1]; - - - final int bucketIndex = i >> BUCKET_SHIFT; - final Bucket bucket = BufferedLinearRegionFile.this.buckets[bucketIndex]; - - synchronized (bucket.lock) { - bucket.loaded = true; - } - - BufferedLinearRegionFile.this.writeChunk(x, z, chunkDataNioBuffer); - } - } - } - } - - // won't and need not to hold any region locks as we are calling this in a safe point (initially newed) - public void tryParseMainFileOld(@NotNull Path mainFilePath) throws IOException { - final File file = mainFilePath.toFile(); - - if (!file.exists() || !file.canRead()) { - return; - } - - // those streams will be closed in the parse logic, or we will close it manually - final FileInputStream fileStream = new FileInputStream(file); - final DataInputStream rawDataStream = new DataInputStream(fileStream); - - boolean oldParsed = false; - final long superBlock; - try { - superBlock = rawDataStream.readLong(); - - if (superBlock == MASTER_FILE_SUPER_BLOCK) { - oldParsed = this.tryParseBlinearV2(rawDataStream, mainFilePath); - - // false -> v3 -> closed in parse block - if (!oldParsed) { - return; - } - } - - if (superBlock == LINEAR_FILE_SUPER_BLOCK) { - final byte version = rawDataStream.readByte(); - - if (version == 1 || version == 2) { - this.parseLinearV1(rawDataStream); - - oldParsed = true; - } - - if (version == 3) { - this.parseLinearV2(rawDataStream, mainFilePath); - - oldParsed = true; - } - } - - } catch (Throwable ex) { - try { - rawDataStream.close(); - } catch (IOException ex2) { - ex.addSuppressed(ex2); - } - - throw new IOException("Failed to parse master file: " + mainFilePath, ex); - } - - // old parsed, remove the original file, and we will recreate it as we sync - if (oldParsed) { - // immediately do sync operation - BufferedLinearRegionFile.this.syncToMasterFile(); - return; - } - - // anyone non-matched, close stream and throw the error - rawDataStream.close(); - - throw new IOException("Unknown or unsupported super block : " + superBlock); - } - } -} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java b/lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java deleted file mode 100644 index e041632..0000000 --- a/lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java +++ /dev/null @@ -1,10 +0,0 @@ -package me.earthme.luminol.utils; - -import abomination.IRegionFile; - -import java.io.IOException; - -@FunctionalInterface -public interface IRegionCreateFunction { - IRegionFile create(RegionCreatorInfo info) throws IOException; -} \ No newline at end of file