--- a/net/minecraft/server/level/PlayerChunk.java +++ b/net/minecraft/server/level/PlayerChunk.java @@ -37,6 +37,10 @@ import net.minecraft.world.level.chunk.ProtoChunkExtension; import net.minecraft.world.level.lighting.LightEngine; +// CraftBukkit start +import net.minecraft.server.MinecraftServer; +// CraftBukkit end + public class PlayerChunk { public static final Either UNLOADED_CHUNK = Either.right(PlayerChunk.Failure.UNLOADED); @@ -75,11 +79,11 @@ this.fullChunkFuture = PlayerChunk.UNLOADED_LEVEL_CHUNK_FUTURE; this.tickingChunkFuture = PlayerChunk.UNLOADED_LEVEL_CHUNK_FUTURE; this.entityTickingChunkFuture = PlayerChunk.UNLOADED_LEVEL_CHUNK_FUTURE; - this.chunkToSave = CompletableFuture.completedFuture((Object) null); + this.chunkToSave = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error this.chunkToSaveHistory = null; this.blockChangedLightSectionFilter = new BitSet(); this.skyChangedLightSectionFilter = new BitSet(); - this.pendingFullStateConfirmation = CompletableFuture.completedFuture((Object) null); + this.pendingFullStateConfirmation = CompletableFuture.completedFuture(null); // CraftBukkit - decompile error this.pos = chunkcoordintpair; this.levelHeightAccessor = levelheightaccessor; this.lightEngine = lightengine; @@ -92,6 +96,20 @@ this.changedBlocksPerSection = new ShortSet[levelheightaccessor.getSectionsCount()]; } + // CraftBukkit start + public Chunk getFullChunkNow() { + // Note: We use the oldTicketLevel for isLoaded checks. + if (!getFullChunkStatus(this.oldTicketLevel).isOrAfter(PlayerChunk.State.BORDER)) return null; + return this.getFullChunkNowUnchecked(); + } + + public Chunk getFullChunkNowUnchecked() { + CompletableFuture> statusFuture = this.getFutureIfPresentUnchecked(ChunkStatus.FULL); + Either either = (Either) statusFuture.getNow(null); + return (either == null) ? null : (Chunk) either.left().orElse(null); + } + // CraftBukkit end + public CompletableFuture> getFutureIfPresentUnchecked(ChunkStatus chunkstatus) { CompletableFuture> completablefuture = (CompletableFuture) this.futures.get(chunkstatus.getIndex()); @@ -117,17 +135,17 @@ @Nullable public Chunk getTickingChunk() { CompletableFuture> completablefuture = this.getTickingChunkFuture(); - Either either = (Either) completablefuture.getNow((Object) null); + Either either = (Either) completablefuture.getNow(null); // CraftBukkit - decompile error - return either == null ? null : (Chunk) either.left().orElse((Object) null); + return either == null ? null : (Chunk) either.left().orElse(null); // CraftBukkit - decompile error } @Nullable public Chunk getFullChunk() { CompletableFuture> completablefuture = this.getFullChunkFuture(); - Either either = (Either) completablefuture.getNow((Object) null); + Either either = (Either) completablefuture.getNow(null); // CraftBukkit - decompile error - return either == null ? null : (Chunk) either.left().orElse((Object) null); + return either == null ? null : (Chunk) either.left().orElse(null); // CraftBukkit - decompile error } @Nullable @@ -172,6 +190,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(); @@ -182,10 +201,10 @@ } public void sectionLightChanged(EnumSkyBlock enumskyblock, int i) { - Either either = (Either) this.getFutureIfPresent(ChunkStatus.FEATURES).getNow((Object) null); + Either either = (Either) this.getFutureIfPresent(ChunkStatus.FEATURES).getNow(null); // CraftBukkit - decompile error if (either != null) { - IChunkAccess ichunkaccess = (IChunkAccess) either.left().orElse((Object) null); + IChunkAccess ichunkaccess = (IChunkAccess) either.left().orElse(null); // CraftBukkit - decompile error if (ichunkaccess != null) { ichunkaccess.setUnsaved(true); @@ -372,7 +391,7 @@ this.pendingFullStateConfirmation = completablefuture1; completablefuture.thenAccept((either) -> { either.ifLeft((chunk) -> { - completablefuture1.complete((Object) null); + completablefuture1.complete(null); // CraftBukkit - decompile error }); }); } @@ -389,6 +408,30 @@ boolean flag1 = this.ticketLevel <= PlayerChunkMap.MAX_CHUNK_DISTANCE; PlayerChunk.State playerchunk_state = getFullChunkStatus(this.oldTicketLevel); PlayerChunk.State playerchunk_state1 = getFullChunkStatus(this.ticketLevel); + // CraftBukkit start + // ChunkUnloadEvent: Called before the chunk is unloaded: isChunkLoaded is still true and chunk can still be modified by plugins. + if (playerchunk_state.isOrAfter(PlayerChunk.State.BORDER) && !playerchunk_state1.isOrAfter(PlayerChunk.State.BORDER)) { + this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { + Chunk chunk = (Chunk)either.left().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 if (flag) { Either either = Either.right(new PlayerChunk.Failure() { @@ -459,6 +502,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 (!playerchunk_state.isOrAfter(PlayerChunk.State.BORDER) && playerchunk_state1.isOrAfter(PlayerChunk.State.BORDER)) { + this.getFutureIfPresentUnchecked(ChunkStatus.FULL).thenAccept((either) -> { + Chunk chunk = (Chunk)either.left().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 static ChunkStatus getStatus(int i) {