--- a/net/minecraft/server/level/PlayerChunk.java +++ b/net/minecraft/server/level/PlayerChunk.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 PlayerChunk extends GenerationChunkHolder { public static final ChunkResult UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); @@ -58,9 +62,9 @@ this.entityTickingChunkFuture = PlayerChunk.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 = levelheightaccessor; this.lightEngine = levellightengine; this.onLevelChange = playerchunk_a; @@ -72,6 +76,16 @@ this.changedBlocksPerSection = new ShortSet[levelheightaccessor.getSectionsCount()]; } + // CraftBukkit start + public Chunk getFullChunkNow() { + return (Chunk) this.getChunkIfPresent(ChunkStatus.FULL); + } + + public Chunk getFullChunkNowUnchecked() { + return (Chunk) this.getChunkIfPresentUnchecked(ChunkStatus.FULL); + } + // CraftBukkit end + public CompletableFuture> getTickingChunkFuture() { return this.tickingChunkFuture; } @@ -86,7 +100,7 @@ @Nullable public Chunk getTickingChunk() { - return (Chunk) ((ChunkResult) this.getTickingChunkFuture().getNow(PlayerChunk.UNLOADED_LEVEL_CHUNK)).orElse((Object) null); + return (Chunk) ((ChunkResult) this.getTickingChunkFuture().getNow(PlayerChunk.UNLOADED_LEVEL_CHUNK)).orElse(null); // CraftBukkit - decompile error } @Nullable @@ -134,6 +148,7 @@ if (chunk != null) { int i = this.levelHeightAccessor.getSectionIndex(blockposition.getY()); + if (i < 0 || i >= this.changedBlocksPerSection.length) return; // CraftBukkit - SPIGOT-6086, SPIGOT-6296 if (this.changedBlocksPerSection[i] == null) { this.hasChangedSections = true; this.changedBlocksPerSection[i] = new ShortOpenHashSet(); @@ -208,8 +223,11 @@ PacketPlayOutMultiBlockChange packetplayoutmultiblockchange = new PacketPlayOutMultiBlockChange(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 }); } } @@ -275,7 +293,7 @@ this.pendingFullStateConfirmation = completablefuture1; completablefuture.thenAccept((chunkresult) -> { chunkresult.ifSuccess((chunk) -> { - completablefuture1.complete((Object) null); + completablefuture1.complete(null); // CraftBukkit - decompile error }); }); } @@ -290,6 +308,30 @@ FullChunkStatus fullchunkstatus1 = ChunkLevel.fullStatus(this.ticketLevel); boolean flag = fullchunkstatus.isOrAfter(FullChunkStatus.FULL); boolean flag1 = fullchunkstatus1.isOrAfter(FullChunkStatus.FULL); + // CraftBukkit start + // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. + if (flag && !flag1) { + this.getFullChunkFuture().thenAccept((either) -> { + Chunk chunk = (Chunk) 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.setUnsaved(true); + 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 " + PlayerChunk.this.pos, throwable); + return null; + }); + + // Run callback right away if the future was already done + playerchunkmap.callbackExecutor.run(); + } + // CraftBukkit end this.wasAccessibleSinceLastSave |= flag1; if (!flag && flag1) { @@ -341,6 +383,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) -> { + Chunk chunk = (Chunk) either.orElse(null); + if (chunk != null) { + playerchunkmap.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 " + PlayerChunk.this.pos, throwable); + return null; + }); + + // Run callback right away if the future was already done + playerchunkmap.callbackExecutor.run(); + } + // CraftBukkit end } public boolean wasAccessibleSinceLastSave() {