--- a/net/minecraft/server/level/ChunkHolder.java +++ b/net/minecraft/server/level/ChunkHolder.java @@ -28,6 +28,10 @@ import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.lighting.LevelLightEngine; +// CraftBukkit start +import net.minecraft.server.MinecraftServer; +// CraftBukkit end + public class ChunkHolder extends GenerationChunkHolder { public static final ChunkResult UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); @@ -58,9 +62,9 @@ this.entityTickingChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE; this.blockChangedLightSectionFilter = new BitSet(); this.skyChangedLightSectionFilter = new BitSet(); - this.pendingFullStateConfirmation = CompletableFuture.completedFuture((Object) null); - this.sendSync = CompletableFuture.completedFuture((Object) null); - this.saveSync = CompletableFuture.completedFuture((Object) null); + this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error + this.sendSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error + this.saveSync = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error this.levelHeightAccessor = world; this.lightEngine = lightingProvider; this.onLevelChange = levelUpdateListener; @@ -70,7 +74,19 @@ this.queueLevel = this.oldTicketLevel; this.setTicketLevel(level); this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()]; + } + + // CraftBukkit start + public LevelChunk getFullChunkNow() { + // Note: We use the oldTicketLevel for isLoaded checks. + if (!ChunkLevel.fullStatus(this.oldTicketLevel).isOrAfter(FullChunkStatus.FULL)) return null; + return this.getFullChunkNowUnchecked(); + } + + public LevelChunk getFullChunkNowUnchecked() { + return (LevelChunk) this.getChunkIfPresentUnchecked(ChunkStatus.FULL); } + // CraftBukkit end public CompletableFuture> getTickingChunkFuture() { return this.tickingChunkFuture; @@ -86,7 +102,7 @@ @Nullable public LevelChunk getTickingChunk() { - return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse((Object) null); + return (LevelChunk) ((ChunkResult) this.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error } @Nullable @@ -138,6 +154,7 @@ boolean flag = this.hasChangedSections; int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); + if (i < 0 || i >= this.changedBlocksPerSection.length) return false; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 if (this.changedBlocksPerSection[i] == null) { this.hasChangedSections = true; this.changedBlocksPerSection[i] = new ShortOpenHashSet(); @@ -224,8 +241,11 @@ ClientboundSectionBlocksUpdatePacket packetplayoutmultiblockchange = new ClientboundSectionBlocksUpdatePacket(sectionposition, shortset, chunksection); this.broadcast(list, packetplayoutmultiblockchange); + // CraftBukkit start + List finalList = list; packetplayoutmultiblockchange.runUpdates((blockposition1, iblockdata1) -> { - this.broadcastBlockEntityIfNeeded(list, world, blockposition1, iblockdata1); + this.broadcastBlockEntityIfNeeded(finalList, world, blockposition1, iblockdata1); + // CraftBukkit end }); } } @@ -291,7 +311,7 @@ this.pendingFullStateConfirmation = completablefuture1; chunkFuture.thenAccept((chunkresult) -> { chunkresult.ifSuccess((chunk) -> { - completablefuture1.complete((Object) null); + completablefuture1.complete(null); // CraftBukkit - decompile error }); }); } @@ -299,7 +319,39 @@ private void demoteFullChunk(ChunkMap chunkLoadingManager, FullChunkStatus target) { this.pendingFullStateConfirmation.cancel(false); chunkLoadingManager.onFullChunkStatusChange(this.pos, target); + } + + // CraftBukkit start + // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. + // SPIGOT-7780: Moved out of updateFutures to call all chunk unload events before calling updateHighestAllowedStatus for all chunks + protected void callEventIfUnloading(ChunkMap playerchunkmap) { + FullChunkStatus oldFullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel); + FullChunkStatus newFullChunkStatus = ChunkLevel.fullStatus(this.ticketLevel); + boolean oldIsFull = oldFullChunkStatus.isOrAfter(FullChunkStatus.FULL); + boolean newIsFull = newFullChunkStatus.isOrAfter(FullChunkStatus.FULL); + if (oldIsFull && !newIsFull) { + this.getFullChunkFuture().thenAccept((either) -> { + LevelChunk chunk = (LevelChunk) either.orElse(null); + if (chunk != null) { + playerchunkmap.callbackExecutor.execute(() -> { + // Minecraft will apply the chunks tick lists to the world once the chunk got loaded, and then store the tick + // lists again inside the chunk once the chunk becomes inaccessible and set the chunk's needsSaving flag. + // These actions may however happen deferred, so we manually set the needsSaving flag already here. + chunk.markUnsaved(); + chunk.unloadCallback(); + }); + } + }).exceptionally((throwable) -> { + // ensure exceptions are printed, by default this is not the case + MinecraftServer.LOGGER.error("Failed to schedule unload callback for chunk " + ChunkHolder.this.pos, throwable); + return null; + }); + + // Run callback right away if the future was already done + playerchunkmap.callbackExecutor.run(); + } } + // CraftBukkit end protected void updateFutures(ChunkMap chunkLoadingManager, Executor executor) { FullChunkStatus fullchunkstatus = ChunkLevel.fullStatus(this.oldTicketLevel); @@ -357,6 +409,26 @@ this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); this.oldTicketLevel = this.ticketLevel; + // CraftBukkit start + // ChunkLoadEvent: Called after the chunk is loaded: isChunkLoaded returns true and chunk is ready to be modified by plugins. + if (!fullchunkstatus.isOrAfter(FullChunkStatus.FULL) && fullchunkstatus1.isOrAfter(FullChunkStatus.FULL)) { + this.getFullChunkFuture().thenAccept((either) -> { + LevelChunk chunk = (LevelChunk) either.orElse(null); + if (chunk != null) { + chunkLoadingManager.callbackExecutor.execute(() -> { + chunk.loadCallback(); + }); + } + }).exceptionally((throwable) -> { + // ensure exceptions are printed, by default this is not the case + MinecraftServer.LOGGER.error("Failed to schedule load callback for chunk " + ChunkHolder.this.pos, throwable); + return null; + }); + + // Run callback right away if the future was already done + chunkLoadingManager.callbackExecutor.run(); + } + // CraftBukkit end } public boolean wasAccessibleSinceLastSave() {