diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java b/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java index 05b507bc..55e7cf0b 100644 --- a/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java @@ -41,17 +41,20 @@ import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStor import com.hypixel.hytale.server.core.universe.world.worldgen.GeneratedChunk; import com.hypixel.hytale.server.core.universe.world.worldgen.IWorldGen; import com.hypixel.hytale.sneakythrow.SneakyThrow; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.longs.LongSets; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.io.BufferedWriter; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.io.PrintWriter; -import java.io.StringWriter; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -60,863 +63,861 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.StampedLock; import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; public class ChunkStore implements WorldProvider { - @Nonnull - public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); - public static final MetricsRegistry METRICS_REGISTRY = new MetricsRegistry() - .register("Store", ChunkStore::getStore, Store.METRICS_REGISTRY) - .register("ChunkLoader", MetricProvider.maybe(ChunkStore::getLoader)) - .register("ChunkSaver", MetricProvider.maybe(ChunkStore::getSaver)) - .register("WorldGen", MetricProvider.maybe(ChunkStore::getGenerator)) - .register("TotalGeneratedChunkCount", chunkComponentStore -> (long)chunkComponentStore.totalGeneratedChunksCount.get(), Codec.LONG) - .register("TotalLoadedChunkCount", chunkComponentStore -> (long)chunkComponentStore.totalLoadedChunksCount.get(), Codec.LONG); - public static final long MAX_FAILURE_BACKOFF_NANOS = TimeUnit.SECONDS.toNanos(10L); - public static final long FAILURE_BACKOFF_NANOS = TimeUnit.MILLISECONDS.toNanos(1L); - public static final ComponentRegistry REGISTRY = new ComponentRegistry<>(); - public static final CodecKey> HOLDER_CODEC_KEY = new CodecKey<>("ChunkHolder"); - @Nonnull - public static final SystemType LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( - ChunkStore.LoadPacketDataQuerySystem.class - ); - @Nonnull - public static final SystemType LOAD_FUTURE_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( - ChunkStore.LoadFuturePacketDataQuerySystem.class - ); - @Nonnull - public static final SystemType UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( - ChunkStore.UnloadPacketDataQuerySystem.class - ); - @Nonnull - public static final ResourceType UNLOAD_RESOURCE = REGISTRY.registerResource( - ChunkUnloadingSystem.Data.class, ChunkUnloadingSystem.Data::new - ); - @Nonnull - public static final ResourceType SAVE_RESOURCE = REGISTRY.registerResource( - ChunkSavingSystems.Data.class, ChunkSavingSystems.Data::new - ); - public static final SystemGroup INIT_GROUP = REGISTRY.registerSystemGroup(); - @Nonnull - private final World world; - @Nonnull - private final Long2ObjectConcurrentHashMap chunks = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND); - private Store store; - @Nullable - private IChunkLoader loader; - @Nullable - private IChunkSaver saver; - @Nullable - private IWorldGen generator; - @Nonnull - private CompletableFuture generatorLoaded = new CompletableFuture<>(); - private final StampedLock generatorLock = new StampedLock(); - private final AtomicInteger totalGeneratedChunksCount = new AtomicInteger(); - private final AtomicInteger totalLoadedChunksCount = new AtomicInteger(); + @Nonnull + public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + public static final MetricsRegistry METRICS_REGISTRY = new MetricsRegistry() + .register("Store", ChunkStore::getStore, Store.METRICS_REGISTRY) + .register("ChunkLoader", MetricProvider.maybe(ChunkStore::getLoader)) + .register("ChunkSaver", MetricProvider.maybe(ChunkStore::getSaver)) + .register("WorldGen", MetricProvider.maybe(ChunkStore::getGenerator)) + .register("TotalGeneratedChunkCount", chunkComponentStore -> (long) chunkComponentStore.totalGeneratedChunksCount.get(), Codec.LONG) + .register("TotalLoadedChunkCount", chunkComponentStore -> (long) chunkComponentStore.totalLoadedChunksCount.get(), Codec.LONG); + public static final long MAX_FAILURE_BACKOFF_NANOS = TimeUnit.SECONDS.toNanos(10L); + public static final long FAILURE_BACKOFF_NANOS = TimeUnit.MILLISECONDS.toNanos(1L); + public static final ComponentRegistry REGISTRY = new ComponentRegistry<>(); + public static final CodecKey> HOLDER_CODEC_KEY = new CodecKey<>("ChunkHolder"); + @Nonnull + public static final SystemType LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( + ChunkStore.LoadPacketDataQuerySystem.class + ); + @Nonnull + public static final SystemType LOAD_FUTURE_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( + ChunkStore.LoadFuturePacketDataQuerySystem.class + ); + @Nonnull + public static final SystemType UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( + ChunkStore.UnloadPacketDataQuerySystem.class + ); + @Nonnull + public static final ResourceType UNLOAD_RESOURCE = REGISTRY.registerResource( + ChunkUnloadingSystem.Data.class, ChunkUnloadingSystem.Data::new + ); + @Nonnull + public static final ResourceType SAVE_RESOURCE = REGISTRY.registerResource( + ChunkSavingSystems.Data.class, ChunkSavingSystems.Data::new + ); + public static final SystemGroup INIT_GROUP = REGISTRY.registerSystemGroup(); + @Nonnull + private final World world; + @Nonnull + private final Long2ObjectConcurrentHashMap chunks = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND); + private Store store; + @Nullable + private IChunkLoader loader; + @Nullable + private IChunkSaver saver; + @Nullable + private IWorldGen generator; + @Nonnull + private CompletableFuture generatorLoaded = new CompletableFuture<>(); + private final StampedLock generatorLock = new StampedLock(); + private final AtomicInteger totalGeneratedChunksCount = new AtomicInteger(); + private final AtomicInteger totalLoadedChunksCount = new AtomicInteger(); - public ChunkStore(@Nonnull World world) { - this.world = world; - } + public ChunkStore(@Nonnull World world) { + this.world = world; + } - @Nonnull - @Override - public World getWorld() { - return this.world; - } + @Nonnull + @Override + public World getWorld() { + return this.world; + } - @Nonnull - public Store getStore() { - return this.store; - } + @Nonnull + public Store getStore() { + return this.store; + } - @Nullable - public IChunkLoader getLoader() { - return this.loader; - } + @Nullable + public IChunkLoader getLoader() { + return this.loader; + } - @Nullable - public IChunkSaver getSaver() { - return this.saver; - } + @Nullable + public IChunkSaver getSaver() { + return this.saver; + } - @Nullable - public IWorldGen getGenerator() { - long readStamp = this.generatorLock.readLock(); + @Nullable + public IWorldGen getGenerator() { + long readStamp = this.generatorLock.readLock(); - IWorldGen var3; - try { - var3 = this.generator; - } finally { - this.generatorLock.unlockRead(readStamp); - } + IWorldGen var3; + try { + var3 = this.generator; + } finally { + this.generatorLock.unlockRead(readStamp); + } - return var3; - } + return var3; + } - public void shutdownGenerator() { - this.setGenerator(null); - } + public void shutdownGenerator() { + this.setGenerator(null); + } - public void setGenerator(@Nullable IWorldGen generator) { - long writeStamp = this.generatorLock.writeLock(); + public void setGenerator(@Nullable IWorldGen generator) { + long writeStamp = this.generatorLock.writeLock(); - try { - if (this.generator != null) { - this.generator.shutdown(); - } + try { + if (this.generator != null) { + this.generator.shutdown(); + } - this.totalGeneratedChunksCount.set(0); - this.generator = generator; - if (generator != null) { - this.generatorLoaded.complete(null); - this.generatorLoaded = new CompletableFuture<>(); - } - } finally { - this.generatorLock.unlockWrite(writeStamp); - } - } + this.totalGeneratedChunksCount.set(0); + this.generator = generator; + if (generator != null) { + this.generatorLoaded.complete(null); + this.generatorLoaded = new CompletableFuture<>(); + } + } finally { + this.generatorLock.unlockWrite(writeStamp); + } + } - @Nonnull - public LongSet getChunkIndexes() { - return LongSets.unmodifiable(this.chunks.keySet()); - } + @Nonnull + public LongSet getChunkIndexes() { + return LongSets.unmodifiable(this.chunks.keySet()); + } - public int getLoadedChunksCount() { - return this.chunks.size(); - } + public int getLoadedChunksCount() { + return this.chunks.size(); + } - public int getTotalGeneratedChunksCount() { - return this.totalGeneratedChunksCount.get(); - } + public int getTotalGeneratedChunksCount() { + return this.totalGeneratedChunksCount.get(); + } - public int getTotalLoadedChunksCount() { - return this.totalLoadedChunksCount.get(); - } + public int getTotalLoadedChunksCount() { + return this.totalLoadedChunksCount.get(); + } - public void start(@Nonnull IResourceStorage resourceStorage) { - this.store = REGISTRY.addStore(this, resourceStorage, store -> this.store = store); - } + public void start(@Nonnull IResourceStorage resourceStorage) { + this.store = REGISTRY.addStore(this, resourceStorage, store -> this.store = store); + } - public void waitForLoadingChunks() { - long start = System.nanoTime(); + public void waitForLoadingChunks() { + long start = System.nanoTime(); - boolean hasLoadingChunks; - do { - this.world.consumeTaskQueue(); - Thread.yield(); - hasLoadingChunks = false; + boolean hasLoadingChunks; + do { + this.world.consumeTaskQueue(); + Thread.yield(); + hasLoadingChunks = false; - for (Entry entry : this.chunks.long2ObjectEntrySet()) { - ChunkStore.ChunkLoadState chunkState = entry.getValue(); - long stamp = chunkState.lock.readLock(); + for (Entry entry : this.chunks.long2ObjectEntrySet()) { + ChunkStore.ChunkLoadState chunkState = entry.getValue(); + long stamp = chunkState.lock.readLock(); + + try { + CompletableFuture> future = chunkState.future; + if (future != null && !future.isDone()) { + hasLoadingChunks = true; + break; + } + } finally { + chunkState.lock.unlockRead(stamp); + } + } + } while (hasLoadingChunks && System.nanoTime() - start <= 5000000000L); + + this.world.consumeTaskQueue(); + } + + public void shutdown() { + this.store.shutdown(); + this.chunks.clear(); + } + + @Nonnull + private Ref add(@Nonnull Holder holder) { + this.world.debugAssertInTickingThread(); + WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + ChunkStore.ChunkLoadState chunkState = this.chunks.get(worldChunkComponent.getIndex()); + if (chunkState == null) { + throw new IllegalStateException("Expected the ChunkLoadState to exist!"); + } else { + Ref oldReference = null; + long stamp = chunkState.lock.writeLock(); try { - CompletableFuture> future = chunkState.future; - if (future != null && !future.isDone()) { - hasLoadingChunks = true; - break; - } - } finally { - chunkState.lock.unlockRead(stamp); - } - } - } while (hasLoadingChunks && System.nanoTime() - start <= 5000000000L); - - this.world.consumeTaskQueue(); - } - - public void shutdown() { - this.store.shutdown(); - this.chunks.clear(); - } - - @Nonnull - private Ref add(@Nonnull Holder holder) { - this.world.debugAssertInTickingThread(); - WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType()); - - assert worldChunkComponent != null; - - ChunkStore.ChunkLoadState chunkState = this.chunks.get(worldChunkComponent.getIndex()); - if (chunkState == null) { - throw new IllegalStateException("Expected the ChunkLoadState to exist!"); - } else { - Ref oldReference = null; - long stamp = chunkState.lock.writeLock(); - - try { - if (chunkState.future == null) { - throw new IllegalStateException("Expected the ChunkLoadState to have a future!"); - } - - if (chunkState.reference != null) { - oldReference = chunkState.reference; - chunkState.reference = null; - } - } finally { - chunkState.lock.unlockWrite(stamp); - } - - if (oldReference != null) { - WorldChunk oldWorldChunkComponent = this.store.getComponent(oldReference, WorldChunk.getComponentType()); - - assert oldWorldChunkComponent != null; - - oldWorldChunkComponent.setFlag(ChunkFlag.TICKING, false); - this.store.removeEntity(oldReference, RemoveReason.REMOVE); - this.world.getNotificationHandler().updateChunk(worldChunkComponent.getIndex()); - } - - oldReference = this.store.addEntity(holder, AddReason.SPAWN); - if (oldReference == null) { - throw new UnsupportedOperationException("Unable to add the chunk to the world!"); - } else { - worldChunkComponent.setReference(oldReference); - stamp = chunkState.lock.writeLock(); - - Ref var17; - try { - chunkState.reference = oldReference; - chunkState.flags = 0; - chunkState.future = null; - chunkState.throwable = null; - chunkState.failedWhen = 0L; - chunkState.failedCounter = 0; - var17 = oldReference; - } finally { - chunkState.lock.unlockWrite(stamp); - } - - return var17; - } - } - } - - public void remove(@Nonnull Ref reference, @Nonnull RemoveReason reason) { - this.world.debugAssertInTickingThread(); - WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType()); - - assert worldChunkComponent != null; - - long index = worldChunkComponent.getIndex(); - ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); - long stamp = chunkState.lock.readLock(); - - try { - worldChunkComponent.setFlag(ChunkFlag.TICKING, false); - this.store.removeEntity(reference, reason); - if (chunkState.future != null) { - chunkState.reference = null; - } else { - this.chunks.remove(index, chunkState); - } - } finally { - chunkState.lock.unlockRead(stamp); - } - } - - @Nullable - public Ref getChunkReference(long index) { - ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); - if (chunkState == null) { - return null; - } else { - long stamp = chunkState.lock.tryOptimisticRead(); - Ref reference = chunkState.reference; - if (chunkState.lock.validate(stamp)) { - return reference; - } else { - stamp = chunkState.lock.readLock(); - - Ref var7; - try { - var7 = chunkState.reference; - } finally { - chunkState.lock.unlockRead(stamp); - } - - return var7; - } - } - } - - @Nullable - public Ref getChunkSectionReference(int x, int y, int z) { - Ref ref = this.getChunkReference(ChunkUtil.indexChunk(x, z)); - if (ref == null) { - return null; - } else { - ChunkColumn chunkColumnComponent = this.store.getComponent(ref, ChunkColumn.getComponentType()); - return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y); - } - } - - @Nullable - public Ref getChunkSectionReference(@Nonnull ComponentAccessor commandBuffer, int x, int y, int z) { - Ref ref = this.getChunkReference(ChunkUtil.indexChunk(x, z)); - if (ref == null) { - return null; - } else { - ChunkColumn chunkColumnComponent = commandBuffer.getComponent(ref, ChunkColumn.getComponentType()); - return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y); - } - } - - @Nonnull - public CompletableFuture> getChunkSectionReferenceAsync(int x, int y, int z) { - return y >= 0 && y < 10 ? this.getChunkReferenceAsync(ChunkUtil.indexChunk(x, z)).thenApplyAsync(ref -> { - if (ref != null && ref.isValid()) { - Store store = ref.getStore(); - ChunkColumn chunkColumnComponent = store.getComponent((Ref)ref, ChunkColumn.getComponentType()); - return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y); - } else { - return null; - } - }, this.store.getExternalData().getWorld()) : CompletableFuture.failedFuture(new IndexOutOfBoundsException("Invalid y: " + y)); - } - - @Nullable - public > T getChunkComponent(long index, @Nonnull ComponentType componentType) { - Ref reference = this.getChunkReference(index); - return reference != null && reference.isValid() ? this.store.getComponent(reference, componentType) : null; - } - - @Nonnull - public CompletableFuture> getChunkReferenceAsync(long index) { - return this.getChunkReferenceAsync(index, 0); - } - - @Nonnull - public CompletableFuture> getChunkReferenceAsync(long index, int flags) { - if (this.store.isShutdown()) { - return CompletableFuture.completedFuture(null); - } else { - ChunkStore.ChunkLoadState chunkState; - if ((flags & 3) == 3) { - chunkState = this.chunks.get(index); - if (chunkState == null) { - return CompletableFuture.completedFuture(null); - } - - long stamp = chunkState.lock.readLock(); - - try { - if ((flags & 4) == 0 || (chunkState.flags & 4) != 0) { - if (chunkState.reference != null) { - return CompletableFuture.completedFuture(chunkState.reference); - } - - if (chunkState.future != null) { - return chunkState.future; - } - - return CompletableFuture.completedFuture(null); - } - } finally { - chunkState.lock.unlockRead(stamp); - } - } else { - chunkState = this.chunks.computeIfAbsent(index, l -> new ChunkStore.ChunkLoadState()); - } - - long stamp = chunkState.lock.writeLock(); - if (chunkState.future == null && chunkState.reference != null && (flags & 8) == 0) { - Ref reference = chunkState.reference; - if ((flags & 4) == 0) { - chunkState.lock.unlockWrite(stamp); - return CompletableFuture.completedFuture(reference); - } else if (this.world.isInThread() && (flags & -2147483648) == 0) { - chunkState.lock.unlockWrite(stamp); - WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType()); - - assert worldChunkComponent != null; - - worldChunkComponent.setFlag(ChunkFlag.TICKING, true); - return CompletableFuture.completedFuture(reference); - } else { - chunkState.lock.unlockWrite(stamp); - return CompletableFuture.supplyAsync(() -> { - WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType()); - - assert worldChunkComponent != null; - - worldChunkComponent.setFlag(ChunkFlag.TICKING, true); - return reference; - }, this.world); - } - } else { - try { - if (chunkState.throwable != null) { - long nanosSince = System.nanoTime() - chunkState.failedWhen; - int count = chunkState.failedCounter; - if (nanosSince < Math.min(MAX_FAILURE_BACKOFF_NANOS, count * count * FAILURE_BACKOFF_NANOS)) { - return CompletableFuture.failedFuture(new RuntimeException("Chunk failure backoff", chunkState.throwable)); - } - - chunkState.throwable = null; - chunkState.failedWhen = 0L; - } - - boolean isNew = chunkState.future == null; - if (isNew) { - chunkState.flags = flags; - } - - int x = ChunkUtil.xOfChunkIndex(index); - int z = ChunkUtil.zOfChunkIndex(index); - int seed = (int)this.world.getWorldConfig().getSeed(); - if ((isNew || (chunkState.flags & 1) != 0) && (flags & 1) == 0) { - if (chunkState.future == null) { - chunkState.future = this.loader.loadHolder(x, z).thenApplyAsync(holder -> { - if (holder != null && !this.store.isShutdown()) { - this.totalLoadedChunksCount.getAndIncrement(); - return this.preLoadChunkAsync(index, (Holder)holder, false); - } else { - return null; - } - }).exceptionallyCompose(throwable -> { - // Corruption detected during load - recover by regenerating - return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable); - }).thenApplyAsync(this::postLoadChunk, this.world); - } else { - chunkState.flags &= -2; - chunkState.future = chunkState.future - .thenCompose( - reference -> reference != null - ? CompletableFuture.completedFuture((Ref)reference) - : this.loader.loadHolder(x, z).thenApplyAsync(holder -> { - if (holder != null && !this.store.isShutdown()) { - this.totalLoadedChunksCount.getAndIncrement(); - return this.preLoadChunkAsync(index, (Holder)holder, false); - } else { - return null; - } - }).exceptionallyCompose(throwable -> { - // Corruption detected during load - recover by regenerating - return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable); - }).thenApplyAsync(this::postLoadChunk, this.world) - ); - } + if (chunkState.future == null) { + throw new IllegalStateException("Expected the ChunkLoadState to have a future!"); } - if ((isNew || (chunkState.flags & 2) != 0) && (flags & 2) == 0) { - if (chunkState.future == null) { - long readStamp = this.generatorLock.readLock(); - - CompletableFuture future; - try { - if (this.generator == null) { - future = this.generatorLoaded - .thenCompose(aVoid -> this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null)); - } else { - future = this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null); - } - } finally { - this.generatorLock.unlockRead(readStamp); - } - - chunkState.future = future.>thenApplyAsync(generatedChunk -> { - if (generatedChunk != null && !this.store.isShutdown()) { - this.totalGeneratedChunksCount.getAndIncrement(); - return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true); - } else { - return null; - } - }).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> { - LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z); - chunkState.fail(throwable); - throw SneakyThrow.sneakyThrow(throwable); - }); - } else { - chunkState.flags &= -3; - chunkState.future = chunkState.future.thenCompose(reference -> { - if (reference != null) { - return CompletableFuture.completedFuture((Ref)reference); - } else { - long readStampx = this.generatorLock.readLock(); - - CompletableFuture future; - try { - if (this.generator == null) { - future = this.generatorLoaded.thenCompose(aVoid -> this.generator.generate(seed, index, x, z, null)); - } else { - future = this.generator.generate(seed, index, x, z, null); - } - } finally { - this.generatorLock.unlockRead(readStampx); - } - - return future.>thenApplyAsync(generatedChunk -> { - if (generatedChunk != null && !this.store.isShutdown()) { - this.totalGeneratedChunksCount.getAndIncrement(); - return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true); - } else { - return null; - } - }).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> { - LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z); - chunkState.fail(throwable); - throw SneakyThrow.sneakyThrow(throwable); - }); - } - }); - } - } - - if ((isNew || (chunkState.flags & 4) == 0) && (flags & 4) != 0) { - chunkState.flags |= 4; - if (chunkState.future != null) { - chunkState.future = chunkState.future.>thenApplyAsync(reference -> { - if (reference != null) { - WorldChunk worldChunkComponent = this.store.getComponent((Ref)reference, WorldChunk.getComponentType()); - - assert worldChunkComponent != null; - - worldChunkComponent.setFlag(ChunkFlag.TICKING, true); - } - - return reference; - }, this.world).exceptionally(throwable -> { - LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to set chunk ticking! %s, %s", x, z); - chunkState.fail(throwable); - throw SneakyThrow.sneakyThrow(throwable); - }); - } - } - - return chunkState.future != null ? chunkState.future : CompletableFuture.completedFuture(null); + if (chunkState.reference != null) { + oldReference = chunkState.reference; + chunkState.reference = null; + } } finally { - chunkState.lock.unlockWrite(stamp); + chunkState.lock.unlockWrite(stamp); } - } - } - } - private boolean isChunkStillNeeded(long index) { - for (PlayerRef playerRef : this.world.getPlayerRefs()) { - if (playerRef.getChunkTracker().shouldBeVisible(index)) { - return true; - } - } + if (oldReference != null) { + WorldChunk oldWorldChunkComponent = this.store.getComponent(oldReference, WorldChunk.getComponentType()); - return false; - } + assert oldWorldChunkComponent != null; - /** - * Handles corruption recovery by logging the corruption, removing the corrupted chunk data, - * and triggering regeneration. - */ - @Nonnull - private CompletableFuture> handleCorruptionAndRegenerate( - long index, int x, int z, int seed, Throwable corruptionCause) { + oldWorldChunkComponent.setFlag(ChunkFlag.TICKING, false); + this.store.removeEntity(oldReference, RemoveReason.REMOVE); + this.world.getNotificationHandler().updateChunk(worldChunkComponent.getIndex()); + } - // Log corruption to file - CorruptedChunkLogger.logCorruption(this.world.getName(), x, z, corruptionCause); - - LOGGER.at(Level.WARNING).log( - "Corrupted chunk detected at (%d, %d) in world '%s' - removing and regenerating. See logs/corrupted_chunks.log for details.", - x, z, this.world.getName() - ); - - // Remove the corrupted chunk data - CompletableFuture removeFuture; - if (this.saver != null) { - removeFuture = this.saver.removeHolder(x, z).exceptionally(removeError -> { - LOGGER.at(Level.WARNING).withCause(removeError).log( - "Failed to remove corrupted chunk data at (%d, %d)", x, z - ); - return null; - }); - } else { - removeFuture = CompletableFuture.completedFuture(null); - } - - // After removal, regenerate the chunk - return removeFuture.thenCompose(ignored -> { - long readStamp = this.generatorLock.readLock(); - try { - CompletableFuture genFuture; - if (this.generator == null) { - genFuture = this.generatorLoaded.thenCompose( - aVoid -> this.generator.generate(seed, index, x, z, null) - ); + oldReference = this.store.addEntity(holder, AddReason.SPAWN); + if (oldReference == null) { + throw new UnsupportedOperationException("Unable to add the chunk to the world!"); } else { - genFuture = this.generator.generate(seed, index, x, z, null); + worldChunkComponent.setReference(oldReference); + stamp = chunkState.lock.writeLock(); + + Ref var17; + try { + chunkState.reference = oldReference; + chunkState.flags = 0; + chunkState.future = null; + chunkState.throwable = null; + chunkState.failedWhen = 0L; + chunkState.failedCounter = 0; + var17 = oldReference; + } finally { + chunkState.lock.unlockWrite(stamp); + } + + return var17; } - return genFuture.thenApplyAsync(generatedChunk -> { - if (generatedChunk != null && !this.store.isShutdown()) { - this.totalGeneratedChunksCount.getAndIncrement(); - return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true); - } else { - return null; - } - }); - } finally { - this.generatorLock.unlockRead(readStamp); - } - }); - } + } + } - public boolean isChunkOnBackoff(long index, long maxFailureBackoffNanos) { - ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); - if (chunkState == null) { - return false; - } else { - long stamp = chunkState.lock.readLock(); + public void remove(@Nonnull Ref reference, @Nonnull RemoveReason reason) { + this.world.debugAssertInTickingThread(); + WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType()); - boolean nanosSince; - try { - if (chunkState.throwable != null) { - long nanosSincex = System.nanoTime() - chunkState.failedWhen; - int count = chunkState.failedCounter; - return nanosSincex < Math.min(maxFailureBackoffNanos, count * count * FAILURE_BACKOFF_NANOS); + assert worldChunkComponent != null; + + long index = worldChunkComponent.getIndex(); + ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); + long stamp = chunkState.lock.readLock(); + + try { + worldChunkComponent.setFlag(ChunkFlag.TICKING, false); + this.store.removeEntity(reference, reason); + if (chunkState.future != null) { + chunkState.reference = null; + } else { + this.chunks.remove(index, chunkState); } - - nanosSince = false; - } finally { + } finally { chunkState.lock.unlockRead(stamp); - } + } + } - return nanosSince; - } - } + @Nullable + public Ref getChunkReference(long index) { + ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); + if (chunkState == null) { + return null; + } else { + long stamp = chunkState.lock.tryOptimisticRead(); + Ref reference = chunkState.reference; + if (chunkState.lock.validate(stamp)) { + return reference; + } else { + stamp = chunkState.lock.readLock(); - @Nonnull - private Holder preLoadChunkAsync(long index, @Nonnull Holder holder, boolean newlyGenerated) { - WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType()); - if (worldChunkComponent == null) { - throw new IllegalStateException( - String.format("Holder missing WorldChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index)) - ); - } else if (worldChunkComponent.getIndex() != index) { - throw new IllegalStateException( - String.format( - "Incorrect chunk index! Got (%d, %d) expected (%d, %d)", - worldChunkComponent.getX(), - worldChunkComponent.getZ(), - ChunkUtil.xOfChunkIndex(index), - ChunkUtil.zOfChunkIndex(index) - ) - ); - } else { - BlockChunk blockChunk = holder.getComponent(BlockChunk.getComponentType()); - if (blockChunk == null) { - throw new IllegalStateException( - String.format("Holder missing BlockChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index)) - ); - } else { - blockChunk.loadFromHolder(holder); - worldChunkComponent.setFlag(ChunkFlag.NEWLY_GENERATED, newlyGenerated); - worldChunkComponent.setLightingUpdatesEnabled(false); - if (newlyGenerated && this.world.getWorldConfig().shouldSaveNewChunks()) { - worldChunkComponent.markNeedsSaving(); + Ref var7; + try { + var7 = chunkState.reference; + } finally { + chunkState.lock.unlockRead(stamp); + } + + return var7; } + } + } + + @Nullable + public Ref getChunkSectionReference(int x, int y, int z) { + Ref ref = this.getChunkReference(ChunkUtil.indexChunk(x, z)); + if (ref == null) { + return null; + } else { + ChunkColumn chunkColumnComponent = this.store.getComponent(ref, ChunkColumn.getComponentType()); + return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y); + } + } + + @Nullable + public Ref getChunkSectionReference(@Nonnull ComponentAccessor commandBuffer, int x, int y, int z) { + Ref ref = this.getChunkReference(ChunkUtil.indexChunk(x, z)); + if (ref == null) { + return null; + } else { + ChunkColumn chunkColumnComponent = commandBuffer.getComponent(ref, ChunkColumn.getComponentType()); + return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y); + } + } + + @Nonnull + public CompletableFuture> getChunkSectionReferenceAsync(int x, int y, int z) { + return y >= 0 && y < 10 ? this.getChunkReferenceAsync(ChunkUtil.indexChunk(x, z)).thenApplyAsync(ref -> { + if (ref != null && ref.isValid()) { + Store store = ref.getStore(); + ChunkColumn chunkColumnComponent = store.getComponent((Ref) ref, ChunkColumn.getComponentType()); + return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y); + } else { + return null; + } + }, this.store.getExternalData().getWorld()) : CompletableFuture.failedFuture(new IndexOutOfBoundsException("Invalid y: " + y)); + } + + @Nullable + public > T getChunkComponent(long index, @Nonnull ComponentType componentType) { + Ref reference = this.getChunkReference(index); + return reference != null && reference.isValid() ? this.store.getComponent(reference, componentType) : null; + } + + @Nonnull + public CompletableFuture> getChunkReferenceAsync(long index) { + return this.getChunkReferenceAsync(index, 0); + } + + @Nonnull + public CompletableFuture> getChunkReferenceAsync(long index, int flags) { + if (this.store.isShutdown()) { + return CompletableFuture.completedFuture(null); + } else { + ChunkStore.ChunkLoadState chunkState; + if ((flags & 3) == 3) { + chunkState = this.chunks.get(index); + if (chunkState == null) { + return CompletableFuture.completedFuture(null); + } + + long stamp = chunkState.lock.readLock(); + + try { + if ((flags & 4) == 0 || (chunkState.flags & 4) != 0) { + if (chunkState.reference != null) { + return CompletableFuture.completedFuture(chunkState.reference); + } + + if (chunkState.future != null) { + return chunkState.future; + } + + return CompletableFuture.completedFuture(null); + } + } finally { + chunkState.lock.unlockRead(stamp); + } + } else { + chunkState = this.chunks.computeIfAbsent(index, l -> new ChunkStore.ChunkLoadState()); + } + + long stamp = chunkState.lock.writeLock(); + if (chunkState.future == null && chunkState.reference != null && (flags & 8) == 0) { + Ref reference = chunkState.reference; + if ((flags & 4) == 0) { + chunkState.lock.unlockWrite(stamp); + return CompletableFuture.completedFuture(reference); + } else if (this.world.isInThread() && (flags & -2147483648) == 0) { + chunkState.lock.unlockWrite(stamp); + WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + worldChunkComponent.setFlag(ChunkFlag.TICKING, true); + return CompletableFuture.completedFuture(reference); + } else { + chunkState.lock.unlockWrite(stamp); + return CompletableFuture.supplyAsync(() -> { + WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + worldChunkComponent.setFlag(ChunkFlag.TICKING, true); + return reference; + }, this.world); + } + } else { + try { + if (chunkState.throwable != null) { + long nanosSince = System.nanoTime() - chunkState.failedWhen; + int count = chunkState.failedCounter; + if (nanosSince < Math.min(MAX_FAILURE_BACKOFF_NANOS, count * count * FAILURE_BACKOFF_NANOS)) { + return CompletableFuture.failedFuture(new RuntimeException("Chunk failure backoff", chunkState.throwable)); + } + + chunkState.throwable = null; + chunkState.failedWhen = 0L; + } + + boolean isNew = chunkState.future == null; + if (isNew) { + chunkState.flags = flags; + } + + int x = ChunkUtil.xOfChunkIndex(index); + int z = ChunkUtil.zOfChunkIndex(index); + int seed = (int) this.world.getWorldConfig().getSeed(); + if ((isNew || (chunkState.flags & 1) != 0) && (flags & 1) == 0) { + if (chunkState.future == null) { + chunkState.future = this.loader.loadHolder(x, z).thenApplyAsync(holder -> { + if (holder != null && !this.store.isShutdown()) { + this.totalLoadedChunksCount.getAndIncrement(); + return this.preLoadChunkAsync(index, (Holder) holder, false); + } else { + return null; + } + }).exceptionallyCompose(throwable -> { + // Corruption detected during load - recover by regenerating + return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable); + }).thenApplyAsync(this::postLoadChunk, this.world); + } else { + chunkState.flags &= -2; + chunkState.future = chunkState.future + .thenCompose( + reference -> reference != null + ? CompletableFuture.completedFuture((Ref) reference) + : this.loader.loadHolder(x, z).thenApplyAsync(holder -> { + if (holder != null && !this.store.isShutdown()) { + this.totalLoadedChunksCount.getAndIncrement(); + return this.preLoadChunkAsync(index, (Holder) holder, false); + } else { + return null; + } + }).exceptionallyCompose(throwable -> { + // Corruption detected during load - recover by regenerating + return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable); + }).thenApplyAsync(this::postLoadChunk, this.world) + ); + } + } + + if ((isNew || (chunkState.flags & 2) != 0) && (flags & 2) == 0) { + if (chunkState.future == null) { + long readStamp = this.generatorLock.readLock(); + + CompletableFuture future; + try { + if (this.generator == null) { + future = this.generatorLoaded + .thenCompose(aVoid -> this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null)); + } else { + future = this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null); + } + } finally { + this.generatorLock.unlockRead(readStamp); + } + + chunkState.future = future.>thenApplyAsync(generatedChunk -> { + if (generatedChunk != null && !this.store.isShutdown()) { + this.totalGeneratedChunksCount.getAndIncrement(); + return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true); + } else { + return null; + } + }).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> { + LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z); + chunkState.fail(throwable); + throw SneakyThrow.sneakyThrow(throwable); + }); + } else { + chunkState.flags &= -3; + chunkState.future = chunkState.future.thenCompose(reference -> { + if (reference != null) { + return CompletableFuture.completedFuture((Ref) reference); + } else { + long readStampx = this.generatorLock.readLock(); + + CompletableFuture future; + try { + if (this.generator == null) { + future = this.generatorLoaded.thenCompose(aVoid -> this.generator.generate(seed, index, x, z, null)); + } else { + future = this.generator.generate(seed, index, x, z, null); + } + } finally { + this.generatorLock.unlockRead(readStampx); + } + + return future.>thenApplyAsync(generatedChunk -> { + if (generatedChunk != null && !this.store.isShutdown()) { + this.totalGeneratedChunksCount.getAndIncrement(); + return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true); + } else { + return null; + } + }).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> { + LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z); + chunkState.fail(throwable); + throw SneakyThrow.sneakyThrow(throwable); + }); + } + }); + } + } + + if ((isNew || (chunkState.flags & 4) == 0) && (flags & 4) != 0) { + chunkState.flags |= 4; + if (chunkState.future != null) { + chunkState.future = chunkState.future.>thenApplyAsync(reference -> { + if (reference != null) { + WorldChunk worldChunkComponent = this.store.getComponent((Ref) reference, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + worldChunkComponent.setFlag(ChunkFlag.TICKING, true); + } + + return reference; + }, this.world).exceptionally(throwable -> { + LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to set chunk ticking! %s, %s", x, z); + chunkState.fail(throwable); + throw SneakyThrow.sneakyThrow(throwable); + }); + } + } + + return chunkState.future != null ? chunkState.future : CompletableFuture.completedFuture(null); + } finally { + chunkState.lock.unlockWrite(stamp); + } + } + } + } + + private boolean isChunkStillNeeded(long index) { + for (PlayerRef playerRef : this.world.getPlayerRefs()) { + if (playerRef.getChunkTracker().shouldBeVisible(index)) { + return true; + } + } + + return false; + } + + /** + * Handles corruption recovery by logging the corruption, marking the region file as corrupted, + * and triggering regeneration. + */ + @Nonnull + private CompletableFuture> handleCorruptionAndRegenerate( + long index, int x, int z, int seed, Throwable corruptionCause) { + + // Log corruption to file + CorruptedChunkLogger.logCorruption(this.world.getName(), x, z, corruptionCause); + + LOGGER.at(Level.WARNING).log( + "Corrupted chunk detected at (%d, %d) in world '%s' - marking region as corrupted and regenerating. See logs/corrupted_chunks.log for details.", + x, z, this.world.getName() + ); + + // Mark the entire region file as corrupted (renames to .corrupted suffix) + CompletableFuture removeFuture; + if (this.saver != null) { + removeFuture = this.saver.deleteRegionFile(x, z).exceptionally(removeError -> { + LOGGER.at(Level.WARNING).withCause(removeError).log( + "Failed to mark region as corrupted for chunk (%d, %d)", x, z + ); + return null; + }); + } else { + removeFuture = CompletableFuture.completedFuture(null); + } + + // After removal, regenerate the chunk + return removeFuture.thenCompose(ignored -> { + long readStamp = this.generatorLock.readLock(); + try { + CompletableFuture genFuture; + if (this.generator == null) { + genFuture = this.generatorLoaded.thenCompose( + aVoid -> this.generator.generate(seed, index, x, z, null) + ); + } else { + genFuture = this.generator.generate(seed, index, x, z, null); + } + return genFuture.thenApplyAsync(generatedChunk -> { + if (generatedChunk != null && !this.store.isShutdown()) { + this.totalGeneratedChunksCount.getAndIncrement(); + return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true); + } else { + return null; + } + }); + } finally { + this.generatorLock.unlockRead(readStamp); + } + }); + } + + public boolean isChunkOnBackoff(long index, long maxFailureBackoffNanos) { + ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); + if (chunkState == null) { + return false; + } else { + long stamp = chunkState.lock.readLock(); + + boolean nanosSince; + try { + if (chunkState.throwable != null) { + long nanosSincex = System.nanoTime() - chunkState.failedWhen; + int count = chunkState.failedCounter; + return nanosSincex < Math.min(maxFailureBackoffNanos, count * count * FAILURE_BACKOFF_NANOS); + } + + nanosSince = false; + } finally { + chunkState.lock.unlockRead(stamp); + } + + return nanosSince; + } + } + + @Nonnull + private Holder preLoadChunkAsync(long index, @Nonnull Holder holder, boolean newlyGenerated) { + WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType()); + if (worldChunkComponent == null) { + throw new IllegalStateException( + String.format("Holder missing WorldChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index)) + ); + } else if (worldChunkComponent.getIndex() != index) { + throw new IllegalStateException( + String.format( + "Incorrect chunk index! Got (%d, %d) expected (%d, %d)", + worldChunkComponent.getX(), + worldChunkComponent.getZ(), + ChunkUtil.xOfChunkIndex(index), + ChunkUtil.zOfChunkIndex(index) + ) + ); + } else { + BlockChunk blockChunk = holder.getComponent(BlockChunk.getComponentType()); + if (blockChunk == null) { + throw new IllegalStateException( + String.format("Holder missing BlockChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index)) + ); + } else { + blockChunk.loadFromHolder(holder); + worldChunkComponent.setFlag(ChunkFlag.NEWLY_GENERATED, newlyGenerated); + worldChunkComponent.setLightingUpdatesEnabled(false); + if (newlyGenerated && this.world.getWorldConfig().shouldSaveNewChunks()) { + worldChunkComponent.markNeedsSaving(); + } + + try { + long start = System.nanoTime(); + IEventDispatcher dispatcher = HytaleServer.get() + .getEventBus() + .dispatchFor(ChunkPreLoadProcessEvent.class, this.world.getName()); + if (dispatcher.hasListener()) { + ChunkPreLoadProcessEvent event = dispatcher.dispatch(new ChunkPreLoadProcessEvent(holder, worldChunkComponent, newlyGenerated, start)); + if (!event.didLog()) { + long end = System.nanoTime(); + long diff = end - start; + if (diff > this.world.getTickStepNanos()) { + LOGGER.at(Level.SEVERE) + .log( + "Took too long to pre-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s", + FormatUtil.nanosToString(diff), + this.world.consumeGCHasRun(), + worldChunkComponent + ); + } + } + } + } finally { + worldChunkComponent.setLightingUpdatesEnabled(true); + } + + return holder; + } + } + } + + @Nullable + private Ref postLoadChunk(@Nullable Holder holder) { + this.world.debugAssertInTickingThread(); + if (holder != null && !this.store.isShutdown()) { + long start = System.nanoTime(); + WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + worldChunkComponent.setFlag(ChunkFlag.START_INIT, true); + if (worldChunkComponent.is(ChunkFlag.TICKING)) { + holder.tryRemoveComponent(REGISTRY.getNonTickingComponentType()); + } else { + holder.ensureComponent(REGISTRY.getNonTickingComponentType()); + } + + Ref reference = this.add(holder); + worldChunkComponent.initFlags(); + this.world.getChunkLighting().init(worldChunkComponent); + long end = System.nanoTime(); + long diff = end - start; + if (diff > this.world.getTickStepNanos()) { + LOGGER.at(Level.SEVERE) + .log( + "Took too long to post-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s", + FormatUtil.nanosToString(diff), + this.world.consumeGCHasRun(), + worldChunkComponent + ); + } + + return reference; + } else { + return null; + } + } + + static { + CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec); + REGISTRY.registerSystem(new ChunkStore.ChunkLoaderSaverSetupSystem()); + REGISTRY.registerSystem(new ChunkUnloadingSystem()); + REGISTRY.registerSystem(new ChunkSavingSystems.WorldRemoved()); + REGISTRY.registerSystem(new ChunkSavingSystems.Ticking()); + } + + private static class ChunkLoadState { + private final StampedLock lock = new StampedLock(); + private int flags = 0; + @Nullable + private CompletableFuture> future; + @Nullable + private Ref reference; + @Nullable + private Throwable throwable; + private long failedWhen; + private int failedCounter; + + private ChunkLoadState() { + } + + private void fail(Throwable throwable) { + long stamp = this.lock.writeLock(); try { - long start = System.nanoTime(); - IEventDispatcher dispatcher = HytaleServer.get() - .getEventBus() - .dispatchFor(ChunkPreLoadProcessEvent.class, this.world.getName()); - if (dispatcher.hasListener()) { - ChunkPreLoadProcessEvent event = dispatcher.dispatch(new ChunkPreLoadProcessEvent(holder, worldChunkComponent, newlyGenerated, start)); - if (!event.didLog()) { - long end = System.nanoTime(); - long diff = end - start; - if (diff > this.world.getTickStepNanos()) { - LOGGER.at(Level.SEVERE) - .log( - "Took too long to pre-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s", - FormatUtil.nanosToString(diff), - this.world.consumeGCHasRun(), - worldChunkComponent - ); - } - } - } + this.flags = 0; + this.future = null; + this.throwable = throwable; + this.failedWhen = System.nanoTime(); + this.failedCounter++; } finally { - worldChunkComponent.setLightingUpdatesEnabled(true); + this.lock.unlockWrite(stamp); } + } + } - return holder; - } - } - } + /** + * Utility class for logging corrupted chunk information to logs/corrupted_chunks.log + */ + private static class CorruptedChunkLogger { + private static final Path LOG_PATH = Paths.get("logs", "corrupted_chunks.log"); + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") + .withZone(ZoneId.systemDefault()); + private static final Object WRITE_LOCK = new Object(); - @Nullable - private Ref postLoadChunk(@Nullable Holder holder) { - this.world.debugAssertInTickingThread(); - if (holder != null && !this.store.isShutdown()) { - long start = System.nanoTime(); - WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType()); + static void logCorruption(String worldName, int chunkX, int chunkZ, Throwable cause) { + try { + synchronized (WRITE_LOCK) { + Files.createDirectories(LOG_PATH.getParent()); + try (BufferedWriter writer = Files.newBufferedWriter(LOG_PATH, + StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { + String timestamp = FORMATTER.format(Instant.now()); + writer.write(String.format("[%s] CORRUPTED CHUNK DETECTED - Regenerating%n", timestamp)); + writer.write(String.format(" World: %s%n", worldName)); + writer.write(String.format(" Chunk: (%d, %d)%n", chunkX, chunkZ)); + writer.write(String.format(" Block coords: (%d, %d) to (%d, %d)%n", + chunkX * 16, chunkZ * 16, chunkX * 16 + 15, chunkZ * 16 + 15)); + writer.write(String.format(" Cause: %s%n", cause.getClass().getSimpleName())); + writer.write(String.format(" Message: %s%n", cause.getMessage())); - assert worldChunkComponent != null; - - worldChunkComponent.setFlag(ChunkFlag.START_INIT, true); - if (worldChunkComponent.is(ChunkFlag.TICKING)) { - holder.tryRemoveComponent(REGISTRY.getNonTickingComponentType()); - } else { - holder.ensureComponent(REGISTRY.getNonTickingComponentType()); - } - - Ref reference = this.add(holder); - worldChunkComponent.initFlags(); - this.world.getChunkLighting().init(worldChunkComponent); - long end = System.nanoTime(); - long diff = end - start; - if (diff > this.world.getTickStepNanos()) { - LOGGER.at(Level.SEVERE) - .log( - "Took too long to post-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s", - FormatUtil.nanosToString(diff), - this.world.consumeGCHasRun(), - worldChunkComponent - ); - } - - return reference; - } else { - return null; - } - } - - static { - CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec); - REGISTRY.registerSystem(new ChunkStore.ChunkLoaderSaverSetupSystem()); - REGISTRY.registerSystem(new ChunkUnloadingSystem()); - REGISTRY.registerSystem(new ChunkSavingSystems.WorldRemoved()); - REGISTRY.registerSystem(new ChunkSavingSystems.Ticking()); - } - - private static class ChunkLoadState { - private final StampedLock lock = new StampedLock(); - private int flags = 0; - @Nullable - private CompletableFuture> future; - @Nullable - private Ref reference; - @Nullable - private Throwable throwable; - private long failedWhen; - private int failedCounter; - - private ChunkLoadState() { - } - - private void fail(Throwable throwable) { - long stamp = this.lock.writeLock(); - - try { - this.flags = 0; - this.future = null; - this.throwable = throwable; - this.failedWhen = System.nanoTime(); - this.failedCounter++; - } finally { - this.lock.unlockWrite(stamp); - } - } - } - - /** - * Utility class for logging corrupted chunk information to logs/corrupted_chunks.log - */ - private static class CorruptedChunkLogger { - private static final Path LOG_PATH = Paths.get("logs", "corrupted_chunks.log"); - private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") - .withZone(ZoneId.systemDefault()); - private static final Object WRITE_LOCK = new Object(); - - static void logCorruption(String worldName, int chunkX, int chunkZ, Throwable cause) { - try { - synchronized (WRITE_LOCK) { - Files.createDirectories(LOG_PATH.getParent()); - try (BufferedWriter writer = Files.newBufferedWriter(LOG_PATH, - StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { - String timestamp = FORMATTER.format(Instant.now()); - writer.write(String.format("[%s] CORRUPTED CHUNK DETECTED - Regenerating%n", timestamp)); - writer.write(String.format(" World: %s%n", worldName)); - writer.write(String.format(" Chunk: (%d, %d)%n", chunkX, chunkZ)); - writer.write(String.format(" Block coords: (%d, %d) to (%d, %d)%n", - chunkX * 16, chunkZ * 16, chunkX * 16 + 15, chunkZ * 16 + 15)); - writer.write(String.format(" Cause: %s%n", cause.getClass().getSimpleName())); - writer.write(String.format(" Message: %s%n", cause.getMessage())); - - // Write stack trace - StringWriter sw = new StringWriter(); - cause.printStackTrace(new PrintWriter(sw)); - String stackTrace = sw.toString(); - // Indent stack trace - for (String line : stackTrace.split("\n")) { - writer.write(String.format(" %s%n", line)); - } - writer.write(String.format("---%n%n")); - } + // Write stack trace + StringWriter sw = new StringWriter(); + cause.printStackTrace(new PrintWriter(sw)); + String stackTrace = sw.toString(); + // Indent stack trace + for (String line : stackTrace.split("\n")) { + writer.write(String.format(" %s%n", line)); + } + writer.write(String.format("---%n%n")); + } + } + } catch (IOException e) { + LOGGER.at(Level.WARNING).withCause(e).log("Failed to write to corrupted_chunks.log"); } - } catch (IOException e) { - LOGGER.at(Level.WARNING).withCause(e).log("Failed to write to corrupted_chunks.log"); - } - } - } + } + } - public static class ChunkLoaderSaverSetupSystem extends StoreSystem { - public ChunkLoaderSaverSetupSystem() { - } + public static class ChunkLoaderSaverSetupSystem extends StoreSystem { + public ChunkLoaderSaverSetupSystem() { + } - @Nullable - @Override - public SystemGroup getGroup() { - return ChunkStore.INIT_GROUP; - } + @Nullable + @Override + public SystemGroup getGroup() { + return ChunkStore.INIT_GROUP; + } - @Override - public void onSystemAddedToStore(@Nonnull Store store) { - ChunkStore data = store.getExternalData(); - World world = data.getWorld(); - IChunkStorageProvider chunkStorageProvider = world.getWorldConfig().getChunkStorageProvider(); + @Override + public void onSystemAddedToStore(@Nonnull Store store) { + ChunkStore data = store.getExternalData(); + World world = data.getWorld(); + IChunkStorageProvider chunkStorageProvider = world.getWorldConfig().getChunkStorageProvider(); - try { - data.loader = chunkStorageProvider.getLoader(store); - data.saver = chunkStorageProvider.getSaver(store); - } catch (IOException var6) { - throw SneakyThrow.sneakyThrow(var6); - } - } - - @Override - public void onSystemRemovedFromStore(@Nonnull Store store) { - ChunkStore data = store.getExternalData(); - - try { - if (data.loader != null) { - IChunkLoader oldLoader = data.loader; - data.loader = null; - oldLoader.close(); + try { + data.loader = chunkStorageProvider.getLoader(store); + data.saver = chunkStorageProvider.getSaver(store); + } catch (IOException var6) { + throw SneakyThrow.sneakyThrow(var6); } + } - if (data.saver != null) { - IChunkSaver oldSaver = data.saver; - data.saver = null; - oldSaver.close(); + @Override + public void onSystemRemovedFromStore(@Nonnull Store store) { + ChunkStore data = store.getExternalData(); + + try { + if (data.loader != null) { + IChunkLoader oldLoader = data.loader; + data.loader = null; + oldLoader.close(); + } + + if (data.saver != null) { + IChunkSaver oldSaver = data.saver; + data.saver = null; + oldSaver.close(); + } + } catch (IOException var4) { + ChunkStore.LOGGER.at(Level.SEVERE).withCause(var4).log("Failed to close storage!"); } - } catch (IOException var4) { - ChunkStore.LOGGER.at(Level.SEVERE).withCause(var4).log("Failed to close storage!"); - } - } - } + } + } - public abstract static class LoadFuturePacketDataQuerySystem extends EntityDataSystem> { - public LoadFuturePacketDataQuerySystem() { - } - } + public abstract static class LoadFuturePacketDataQuerySystem extends EntityDataSystem> { + public LoadFuturePacketDataQuerySystem() { + } + } - public abstract static class LoadPacketDataQuerySystem extends EntityDataSystem { - public LoadPacketDataQuerySystem() { - } - } + public abstract static class LoadPacketDataQuerySystem extends EntityDataSystem { + public LoadPacketDataQuerySystem() { + } + } - public abstract static class UnloadPacketDataQuerySystem extends EntityDataSystem { - public UnloadPacketDataQuerySystem() { - } - } + public abstract static class UnloadPacketDataQuerySystem extends EntityDataSystem { + public UnloadPacketDataQuerySystem() { + } + } } diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/IChunkSaver.java b/src/com/hypixel/hytale/server/core/universe/world/storage/IChunkSaver.java new file mode 100644 index 00000000..d45ac2a8 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/IChunkSaver.java @@ -0,0 +1,31 @@ +package com.hypixel.hytale.server.core.universe.world.storage; + +import com.hypixel.hytale.component.Holder; +import it.unimi.dsi.fastutil.longs.LongSet; + +import javax.annotation.Nonnull; +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public interface IChunkSaver extends Closeable { + @Nonnull + CompletableFuture saveHolder(int var1, int var2, @Nonnull Holder var3); + + @Nonnull + CompletableFuture removeHolder(int var1, int var2); + + /** + * Deletes the entire region file containing the specified chunk coordinates. + * Used for corruption recovery when the region file is unrecoverable. + */ + @Nonnull + default CompletableFuture deleteRegionFile(int x, int z) { + return CompletableFuture.completedFuture(null); + } + + @Nonnull + LongSet getIndexes() throws IOException; + + void flush() throws IOException; +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java b/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java new file mode 100644 index 00000000..21989e8e --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/provider/IndexedStorageChunkStorageProvider.java @@ -0,0 +1,474 @@ +package com.hypixel.hytale.server.core.universe.world.storage.provider; + +import com.hypixel.fastutil.longs.Long2ObjectConcurrentHashMap; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.array.ArrayCodec; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.SystemGroup; +import com.hypixel.hytale.component.system.StoreSystem; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.metrics.MetricProvider; +import com.hypixel.hytale.metrics.MetricResults; +import com.hypixel.hytale.metrics.MetricsRegistry; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.BufferChunkLoader; +import com.hypixel.hytale.server.core.universe.world.storage.BufferChunkSaver; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; +import com.hypixel.hytale.server.core.universe.world.storage.IChunkLoader; +import com.hypixel.hytale.server.core.universe.world.storage.IChunkSaver; +import com.hypixel.hytale.sneakythrow.SneakyThrow; +import com.hypixel.hytale.storage.IndexedStorageFile; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.ints.IntListIterator; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.longs.LongSets; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.Closeable; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Iterator; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +public class IndexedStorageChunkStorageProvider implements IChunkStorageProvider { + public static final String ID = "IndexedStorage"; + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + IndexedStorageChunkStorageProvider.class, IndexedStorageChunkStorageProvider::new + ) + .documentation("Uses the indexed storage file format to store chunks.") + .appendInherited( + new KeyedCodec<>("FlushOnWrite", Codec.BOOLEAN), (o, i) -> o.flushOnWrite = i, o -> o.flushOnWrite, (o, p) -> o.flushOnWrite = p.flushOnWrite + ) + .documentation( + "Controls whether the indexed storage flushes during writes.\nRecommended to be enabled to prevent corruption of chunks during unclean shutdowns." + ) + .add() + .build(); + private boolean flushOnWrite = true; + + public IndexedStorageChunkStorageProvider() { + } + + @Nonnull + @Override + public IChunkLoader getLoader(@Nonnull Store store) { + return new IndexedStorageChunkStorageProvider.IndexedStorageChunkLoader(store, this.flushOnWrite); + } + + @Nonnull + @Override + public IChunkSaver getSaver(@Nonnull Store store) { + return new IndexedStorageChunkStorageProvider.IndexedStorageChunkSaver(store, this.flushOnWrite); + } + + @Nonnull + @Override + public String toString() { + return "IndexedStorageChunkStorageProvider{}"; + } + + @Nonnull + private static String toFileName(int regionX, int regionZ) { + return regionX + "." + regionZ + ".region.bin"; + } + + private static long fromFileName(@Nonnull String fileName) { + String[] split = fileName.split("\\."); + if (split.length != 4) { + throw new IllegalArgumentException("Unexpected file name format!"); + } else if (!"region".equals(split[2])) { + throw new IllegalArgumentException("Unexpected file name format!"); + } else if (!"bin".equals(split[3])) { + throw new IllegalArgumentException("Unexpected file extension!"); + } else { + int regionX = Integer.parseInt(split[0]); + int regionZ = Integer.parseInt(split[1]); + return ChunkUtil.indexChunk(regionX, regionZ); + } + } + + public static class IndexedStorageCache implements Closeable, MetricProvider, Resource { + @Nonnull + public static final MetricsRegistry METRICS_REGISTRY = new MetricsRegistry() + .register( + "Files", + cache -> cache.cache + .long2ObjectEntrySet() + .stream() + .map(IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData::new) + .toArray(IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData[]::new), + new ArrayCodec<>( + IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData.CODEC, + IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData[]::new + ) + ); + private final Long2ObjectConcurrentHashMap cache = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND); + private Path path; + + public IndexedStorageCache() { + } + + public static ResourceType getResourceType() { + return Universe.get().getIndexedStorageCacheResourceType(); + } + + @Nonnull + public Long2ObjectConcurrentHashMap getCache() { + return this.cache; + } + + @Override + public void close() throws IOException { + IOException exception = null; + Iterator iterator = this.cache.values().iterator(); + + while (iterator.hasNext()) { + try { + iterator.next().close(); + iterator.remove(); + } catch (Exception var4) { + if (exception == null) { + exception = new IOException("Failed to close one or more loaders!"); + } + + exception.addSuppressed(var4); + } + } + + if (exception != null) { + throw exception; + } + } + + @Nullable + public IndexedStorageFile getOrTryOpen(int regionX, int regionZ, boolean flushOnWrite) { + return this.cache.computeIfAbsent(ChunkUtil.indexChunk(regionX, regionZ), k -> { + Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ)); + if (!Files.exists(regionFile)) { + return null; + } else { + try { + IndexedStorageFile open = IndexedStorageFile.open(regionFile, StandardOpenOption.READ, StandardOpenOption.WRITE); + open.setFlushOnWrite(flushOnWrite); + return open; + } catch (FileNotFoundException var8) { + return null; + } catch (Exception var9) { + // Corruption detected - rename file and return null to trigger regeneration + System.err.println("[IndexedStorageCache] Corrupted region file detected: " + regionFile + " - " + var9.getMessage()); + try { + Path corruptedPath = regionFile.resolveSibling(regionFile.getFileName() + ".corrupted"); + Files.move(regionFile, corruptedPath, StandardCopyOption.REPLACE_EXISTING); + System.err.println("[IndexedStorageCache] Renamed to: " + corruptedPath); + } catch (IOException moveErr) { + System.err.println("[IndexedStorageCache] Failed to rename corrupted file: " + moveErr.getMessage()); + } + return null; + } + } + }); + } + + /** + * Marks a region file as corrupted by renaming it with .corrupted suffix. + * The file will be regenerated on next access. + */ + public void markRegionCorrupted(int regionX, int regionZ) { + long key = ChunkUtil.indexChunk(regionX, regionZ); + IndexedStorageFile file = this.cache.remove(key); + if (file != null) { + try { + file.close(); + } catch (IOException e) { + // Ignore close errors + } + } + Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ)); + if (Files.exists(regionFile)) { + try { + Path corruptedPath = regionFile.resolveSibling(regionFile.getFileName() + ".corrupted"); + Files.move(regionFile, corruptedPath, StandardCopyOption.REPLACE_EXISTING); + System.err.println("[IndexedStorageCache] Marked region as corrupted: " + corruptedPath); + } catch (IOException e) { + System.err.println("[IndexedStorageCache] Failed to mark region as corrupted: " + e.getMessage()); + } + } + } + + @Nonnull + public IndexedStorageFile getOrCreate(int regionX, int regionZ, boolean flushOnWrite) { + return this.cache.computeIfAbsent(ChunkUtil.indexChunk(regionX, regionZ), k -> { + try { + if (!Files.exists(this.path)) { + try { + Files.createDirectory(this.path); + } catch (FileAlreadyExistsException var8) { + } + } + + Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ)); + IndexedStorageFile open = IndexedStorageFile.open(regionFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); + open.setFlushOnWrite(flushOnWrite); + return open; + } catch (IOException var9) { + throw SneakyThrow.sneakyThrow(var9); + } + }); + } + + @Nonnull + public LongSet getIndexes() throws IOException { + if (!Files.exists(this.path)) { + return LongSets.EMPTY_SET; + } else { + LongOpenHashSet chunkIndexes = new LongOpenHashSet(); + + try (Stream stream = Files.list(this.path)) { + stream.forEach(path -> { + if (!Files.isDirectory(path)) { + long regionIndex; + try { + regionIndex = IndexedStorageChunkStorageProvider.fromFileName(path.getFileName().toString()); + } catch (IllegalArgumentException var15) { + return; + } + + int regionX = ChunkUtil.xOfChunkIndex(regionIndex); + int regionZ = ChunkUtil.zOfChunkIndex(regionIndex); + IndexedStorageFile regionFile = this.getOrTryOpen(regionX, regionZ, true); + if (regionFile != null) { + IntList blobIndexes = regionFile.keys(); + IntListIterator iterator = blobIndexes.iterator(); + + while (iterator.hasNext()) { + int blobIndex = iterator.nextInt(); + int localX = ChunkUtil.xFromColumn(blobIndex); + int localZ = ChunkUtil.zFromColumn(blobIndex); + int chunkX = regionX << 5 | localX; + int chunkZ = regionZ << 5 | localZ; + chunkIndexes.add(ChunkUtil.indexChunk(chunkX, chunkZ)); + } + } + } + }); + } + + return chunkIndexes; + } + } + + public void flush() throws IOException { + IOException exception = null; + + for (IndexedStorageFile indexedStorageFile : this.cache.values()) { + try { + indexedStorageFile.force(false); + } catch (Exception var5) { + if (exception == null) { + exception = new IOException("Failed to close one or more loaders!"); + } + + exception.addSuppressed(var5); + } + } + + if (exception != null) { + throw exception; + } + } + + @Nonnull + @Override + public MetricResults toMetricResults() { + return METRICS_REGISTRY.toMetricResults(this); + } + + @Nonnull + @Override + public Resource clone() { + return new IndexedStorageChunkStorageProvider.IndexedStorageCache(); + } + + private static class CacheEntryMetricData { + @Nonnull + private static final Codec CODEC = BuilderCodec.builder( + IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData.class, + IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData::new + ) + .append(new KeyedCodec<>("Key", Codec.LONG), (entry, o) -> entry.key = o, entry -> entry.key) + .add() + .append(new KeyedCodec<>("File", IndexedStorageFile.METRICS_REGISTRY), (entry, o) -> entry.value = o, entry -> entry.value) + .add() + .build(); + private long key; + private IndexedStorageFile value; + + public CacheEntryMetricData() { + } + + public CacheEntryMetricData(@Nonnull Entry entry) { + this.key = entry.getLongKey(); + this.value = entry.getValue(); + } + } + } + + public static class IndexedStorageCacheSetupSystem extends StoreSystem { + public IndexedStorageCacheSetupSystem() { + } + + @Nullable + @Override + public SystemGroup getGroup() { + return ChunkStore.INIT_GROUP; + } + + @Override + public void onSystemAddedToStore(@Nonnull Store store) { + World world = store.getExternalData().getWorld(); + store.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).path = world.getSavePath().resolve("chunks"); + } + + @Override + public void onSystemRemovedFromStore(@Nonnull Store store) { + } + } + + public static class IndexedStorageChunkLoader extends BufferChunkLoader implements MetricProvider { + private final boolean flushOnWrite; + + public IndexedStorageChunkLoader(@Nonnull Store store, boolean flushOnWrite) { + super(store); + this.flushOnWrite = flushOnWrite; + } + + @Override + public void close() throws IOException { + this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).close(); + } + + @Nonnull + @Override + public CompletableFuture loadBuffer(int x, int z) { + int regionX = x >> 5; + int regionZ = z >> 5; + int localX = x & 31; + int localZ = z & 31; + int index = ChunkUtil.indexColumn(localX, localZ); + IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore() + .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()); + return CompletableFuture.supplyAsync(SneakyThrow.sneakySupplier(() -> { + IndexedStorageFile chunks = indexedStorageCache.getOrTryOpen(regionX, regionZ, this.flushOnWrite); + return chunks == null ? null : chunks.readBlob(index); + })); + } + + @Nonnull + @Override + public LongSet getIndexes() throws IOException { + return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).getIndexes(); + } + + @Nullable + @Override + public MetricResults toMetricResults() { + return this.getStore().getExternalData().getSaver() instanceof IndexedStorageChunkStorageProvider.IndexedStorageChunkSaver + ? null + : this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).toMetricResults(); + } + } + + public static class IndexedStorageChunkSaver extends BufferChunkSaver implements MetricProvider { + private final boolean flushOnWrite; + + protected IndexedStorageChunkSaver(@Nonnull Store store, boolean flushOnWrite) { + super(store); + this.flushOnWrite = flushOnWrite; + } + + @Override + public void close() throws IOException { + IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore() + .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()); + indexedStorageCache.close(); + } + + @Nonnull + @Override + public CompletableFuture saveBuffer(int x, int z, @Nonnull ByteBuffer buffer) { + int regionX = x >> 5; + int regionZ = z >> 5; + int localX = x & 31; + int localZ = z & 31; + int index = ChunkUtil.indexColumn(localX, localZ); + IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore() + .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()); + return CompletableFuture.runAsync(SneakyThrow.sneakyRunnable(() -> { + IndexedStorageFile chunks = indexedStorageCache.getOrCreate(regionX, regionZ, this.flushOnWrite); + chunks.writeBlob(index, buffer); + })); + } + + @Nonnull + @Override + public CompletableFuture removeBuffer(int x, int z) { + int regionX = x >> 5; + int regionZ = z >> 5; + int localX = x & 31; + int localZ = z & 31; + int index = ChunkUtil.indexColumn(localX, localZ); + IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore() + .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()); + return CompletableFuture.runAsync(SneakyThrow.sneakyRunnable(() -> { + IndexedStorageFile chunks = indexedStorageCache.getOrTryOpen(regionX, regionZ, this.flushOnWrite); + if (chunks != null) { + chunks.removeBlob(index); + } + })); + } + + @Nonnull + @Override + public CompletableFuture deleteRegionFile(int x, int z) { + int regionX = x >> 5; + int regionZ = z >> 5; + IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore() + .getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()); + return CompletableFuture.runAsync(() -> { + indexedStorageCache.markRegionCorrupted(regionX, regionZ); + }); + } + + @Nonnull + @Override + public LongSet getIndexes() throws IOException { + return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).getIndexes(); + } + + @Override + public void flush() throws IOException { + this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).flush(); + } + + @Override + public MetricResults toMetricResults() { + return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).toMetricResults(); + } + } +} diff --git a/src/com/hypixel/hytale/storage/IndexedStorageFile.java b/src/com/hypixel/hytale/storage/IndexedStorageFile.java index 218abc5c..8aaa313e 100644 --- a/src/com/hypixel/hytale/storage/IndexedStorageFile.java +++ b/src/com/hypixel/hytale/storage/IndexedStorageFile.java @@ -126,12 +126,21 @@ public class IndexedStorageFile implements Closeable { throw new IOException("file channel is empty"); } - storageFile.readHeader(); - storageFile.memoryMapBlobIndexes(); - if (storageFile.version == 0) { - storageFile = migrateV0(path, blobCount, segmentSize, options, attrs, storageFile); - } else { - storageFile.readUsedSegments(); + try { + storageFile.readHeader(); + storageFile.memoryMapBlobIndexes(); + if (storageFile.version == 0) { + storageFile = migrateV0(path, blobCount, segmentSize, options, attrs, storageFile); + } else { + storageFile.readUsedSegments(); + } + } catch (CorruptedStorageException e) { + // Corruption detected - delete and recreate the file + System.err.println("[IndexedStorageFile] Corruption detected in " + path + ": " + e.getMessage() + " - deleting and recreating"); + storageFile.close(); + Files.delete(path); + storageFile = new IndexedStorageFile(path, FileChannel.open(path, options, attrs)); + storageFile.create(blobCount, segmentSize); } } @@ -139,6 +148,15 @@ public class IndexedStorageFile implements Closeable { } } + /** + * Exception thrown when storage file corruption is detected. + */ + public static class CorruptedStorageException extends IOException { + public CorruptedStorageException(String message) { + super(message); + } + } + private static IndexedStorageFile migrateV0( Path path, int blobCount, int segmentSize, Set options, FileAttribute[] attrs, IndexedStorageFile storageFile ) throws IOException { @@ -262,6 +280,9 @@ public class IndexedStorageFile implements Closeable { this.mappedBlobIndexes = this.fileChannel.map(MapMode.READ_WRITE, HEADER_LENGTH, this.blobCount * 4L); } + private static final int MAX_COMPRESSED_LENGTH_LIMIT = 256 * 1024 * 1024; // 256MB + private static final int MAX_SEGMENT_INDEX = 10_000_000; // ~40GB at 4KB segments + protected void readUsedSegments() throws IOException { long stamp = this.usedSegmentsLock.writeLock(); @@ -276,25 +297,25 @@ public class IndexedStorageFile implements Closeable { firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos); if (firstSegmentIndex == 0) { compressedLength = 0; + } else if (firstSegmentIndex < 0 || firstSegmentIndex > MAX_SEGMENT_INDEX) { + // Corrupted segment index - file is corrupt + throw new CorruptedStorageException( + "Invalid segment index " + firstSegmentIndex + " for blob " + blobIndex); } else { - try { - ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex); - compressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET); - } catch (Exception e) { - // Corrupted blob header - skip this blob - compressedLength = 0; + ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex); + compressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET); + if (compressedLength < 0 || compressedLength > MAX_COMPRESSED_LENGTH_LIMIT) { + throw new CorruptedStorageException( + "Invalid compressed length " + compressedLength + " for blob " + blobIndex); } } } finally { this.indexLocks[blobIndex].unlockRead(segmentStamp); } - // Validate to prevent OOM from corrupted compressedLength values - // Max reasonable compressed length ~256MB, prevents BitSet from allocating huge arrays - if (compressedLength > 0 && compressedLength < 256 * 1024 * 1024 && firstSegmentIndex > 0) { + if (compressedLength > 0 && firstSegmentIndex > 0) { int segmentsCount = this.requiredSegments(BLOB_HEADER_LENGTH + compressedLength); int toIndex = firstSegmentIndex + segmentsCount; - // Ensure no overflow and reasonable range if (segmentsCount > 0 && toIndex > firstSegmentIndex) { this.usedSegments.set(firstSegmentIndex, toIndex); } @@ -509,11 +530,9 @@ public class IndexedStorageFile implements Closeable { } } - private static final int MAX_COMPRESSED_LENGTH = 256 * 1024 * 1024; // 256MB max to prevent OOM - @Nonnull protected ByteBuffer readSegments(int firstSegmentIndex, int compressedLength) throws IOException { - if (compressedLength <= 0 || compressedLength > MAX_COMPRESSED_LENGTH) { + if (compressedLength <= 0 || compressedLength > MAX_COMPRESSED_LENGTH_LIMIT) { throw new IOException("Invalid compressed length: " + compressedLength); } if (firstSegmentIndex <= 0) { @@ -558,52 +577,52 @@ public class IndexedStorageFile implements Closeable { dest.limit(dest.position()); dest.position(0); int indexPos = blobIndex * 4; - long stamp = this.indexLocks[blobIndex].writeLock(); - - try { - int oldSegmentLength = 0; - int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos); - if (oldFirstSegmentIndex != 0) { - try { - ByteBuffer blobHeaderBuffer = this.readBlobHeader(oldFirstSegmentIndex); - int oldCompressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET); - // Validate to prevent corruption from causing invalid segment ranges - if (oldCompressedLength >= 0) { - oldSegmentLength = this.requiredSegments(BLOB_HEADER_LENGTH + oldCompressedLength); - } - } catch (Exception e) { - // Old blob header is corrupted/unreadable - skip clearing old segments - // This leaks segments but prevents corruption from propagating - oldSegmentLength = 0; - } - } - - int firstSegmentIndex = this.writeSegments(dest); - if (this.flushOnWrite) { - this.fileChannel.force(false); - } - - this.mappedBlobIndexes.putInt(indexPos, firstSegmentIndex); - if (this.flushOnWrite) { - this.mappedBlobIndexes.force(indexPos, 4); - } - - if (oldSegmentLength > 0 && oldFirstSegmentIndex > 0) { - int toIndex = oldFirstSegmentIndex + oldSegmentLength; - // Validate range to prevent IndexOutOfBoundsException - if (toIndex > oldFirstSegmentIndex) { - long usedSegmentsStamp = this.usedSegmentsLock.writeLock(); + long stamp = this.indexLocks[blobIndex].writeLock(); + try { + int oldSegmentLength = 0; + int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos); + if (oldFirstSegmentIndex != 0) { try { - this.usedSegments.clear(oldFirstSegmentIndex, toIndex); - } finally { - this.usedSegmentsLock.unlockWrite(usedSegmentsStamp); + ByteBuffer blobHeaderBuffer = this.readBlobHeader(oldFirstSegmentIndex); + int oldCompressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET); + // Validate to prevent corruption from causing invalid segment ranges + if (oldCompressedLength >= 0) { + oldSegmentLength = this.requiredSegments(BLOB_HEADER_LENGTH + oldCompressedLength); + } + } catch (Exception e) { + // Old blob header is corrupted/unreadable - skip clearing old segments + // This leaks segments but prevents corruption from propagating + oldSegmentLength = 0; } } + + int firstSegmentIndex = this.writeSegments(dest); + if (this.flushOnWrite) { + this.fileChannel.force(false); + } + + this.mappedBlobIndexes.putInt(indexPos, firstSegmentIndex); + if (this.flushOnWrite) { + this.mappedBlobIndexes.force(indexPos, 4); + } + + if (oldSegmentLength > 0 && oldFirstSegmentIndex > 0) { + int toIndex = oldFirstSegmentIndex + oldSegmentLength; + // Validate range to prevent IndexOutOfBoundsException + if (toIndex > oldFirstSegmentIndex) { + long usedSegmentsStamp = this.usedSegmentsLock.writeLock(); + + try { + this.usedSegments.clear(oldFirstSegmentIndex, toIndex); + } finally { + this.usedSegmentsLock.unlockWrite(usedSegmentsStamp); + } + } + } + } finally { + this.indexLocks[blobIndex].unlockWrite(stamp); } - } finally { - this.indexLocks[blobIndex].unlockWrite(stamp); - } } else { throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount); }