package net.minecraft.server.level; import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet; import it.unimi.dsi.fastutil.shorts.ShortSet; import java.util.BitSet; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.IntConsumer; import java.util.function.IntSupplier; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; import net.minecraft.network.protocol.game.ClientboundLightUpdatePacket; import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.lighting.LevelLightEngine; import org.jetbrains.annotations.Nullable; public class ChunkHolder extends GenerationChunkHolder { public static final ChunkResult UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk"); private static final CompletableFuture> UNLOADED_LEVEL_CHUNK_FUTURE = CompletableFuture.completedFuture(UNLOADED_LEVEL_CHUNK); private final LevelHeightAccessor levelHeightAccessor; /** * A future that returns the chunk if it is a border chunk, {@link net.minecraft.world.server.ChunkHolder.ChunkLoadingFailure#UNLOADED} otherwise. */ private volatile CompletableFuture> fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; /** * A future that returns the chunk if it is a ticking chunk, {@link net.minecraft.world.server.ChunkHolder.ChunkLoadingFailure#UNLOADED} otherwise. */ private volatile CompletableFuture> tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; /** * A future that returns the chunk if it is an entity ticking chunk, {@link net.minecraft.world.server.ChunkHolder.ChunkLoadingFailure#UNLOADED} otherwise. */ private volatile CompletableFuture> entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; private int oldTicketLevel; private int ticketLevel; private int queueLevel; private boolean hasChangedSections; private final ShortSet[] changedBlocksPerSection; private final BitSet blockChangedLightSectionFilter = new BitSet(); private final BitSet skyChangedLightSectionFilter = new BitSet(); private final LevelLightEngine lightEngine; private final ChunkHolder.LevelChangeListener onLevelChange; private final ChunkHolder.PlayerProvider playerProvider; private boolean wasAccessibleSinceLastSave; private CompletableFuture pendingFullStateConfirmation = CompletableFuture.completedFuture(null); private CompletableFuture sendSync = CompletableFuture.completedFuture(null); private CompletableFuture saveSync = CompletableFuture.completedFuture(null); public ChunkHolder( ChunkPos pos, int ticketLevel, LevelHeightAccessor levelHeightAccessor, LevelLightEngine lightEngine, ChunkHolder.LevelChangeListener onLevelChange, ChunkHolder.PlayerProvider playerProvider ) { super(pos); this.levelHeightAccessor = levelHeightAccessor; this.lightEngine = lightEngine; this.onLevelChange = onLevelChange; this.playerProvider = playerProvider; this.oldTicketLevel = ChunkLevel.MAX_LEVEL + 1; this.ticketLevel = this.oldTicketLevel; this.queueLevel = this.oldTicketLevel; this.setTicketLevel(ticketLevel); this.changedBlocksPerSection = new ShortSet[levelHeightAccessor.getSectionsCount()]; } public CompletableFuture> getTickingChunkFuture() { return this.tickingChunkFuture; } public CompletableFuture> getEntityTickingChunkFuture() { return this.entityTickingChunkFuture; } public CompletableFuture> getFullChunkFuture() { return this.fullChunkFuture; } @Nullable public LevelChunk getTickingChunk() { return (LevelChunk)((ChunkResult)this.getTickingChunkFuture().getNow(UNLOADED_LEVEL_CHUNK)).orElse(null); } @Nullable public LevelChunk getChunkToSend() { return !this.sendSync.isDone() ? null : this.getTickingChunk(); } public CompletableFuture getSendSyncFuture() { return this.sendSync; } public void addSendDependency(CompletableFuture dependency) { if (this.sendSync.isDone()) { this.sendSync = dependency; } else { this.sendSync = this.sendSync.thenCombine(dependency, (object, object2) -> null); } } public CompletableFuture getSaveSyncFuture() { return this.saveSync; } public boolean isReadyForSaving() { return this.saveSync.isDone(); } @Override protected void addSaveDependency(CompletableFuture saveDependency) { if (this.saveSync.isDone()) { this.saveSync = saveDependency; } else { this.saveSync = this.saveSync.thenCombine(saveDependency, (object, object2) -> null); } } public boolean blockChanged(BlockPos pos) { LevelChunk levelChunk = this.getTickingChunk(); if (levelChunk == null) { return false; } else { boolean bl = this.hasChangedSections; int i = this.levelHeightAccessor.getSectionIndex(pos.getY()); if (this.changedBlocksPerSection[i] == null) { this.hasChangedSections = true; this.changedBlocksPerSection[i] = new ShortOpenHashSet(); } this.changedBlocksPerSection[i].add(SectionPos.sectionRelativePos(pos)); return !bl; } } public boolean sectionLightChanged(LightLayer lightLayer, int y) { ChunkAccess chunkAccess = this.getChunkIfPresent(ChunkStatus.INITIALIZE_LIGHT); if (chunkAccess == null) { return false; } else { chunkAccess.markUnsaved(); LevelChunk levelChunk = this.getTickingChunk(); if (levelChunk == null) { return false; } else { int i = this.lightEngine.getMinLightSection(); int j = this.lightEngine.getMaxLightSection(); if (y >= i && y <= j) { BitSet bitSet = lightLayer == LightLayer.SKY ? this.skyChangedLightSectionFilter : this.blockChangedLightSectionFilter; int k = y - i; if (!bitSet.get(k)) { bitSet.set(k); return true; } else { return false; } } else { return false; } } } } public boolean hasChangesToBroadcast() { return this.hasChangedSections || !this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty(); } public void broadcastChanges(LevelChunk chunk) { if (this.hasChangesToBroadcast()) { Level level = chunk.getLevel(); if (!this.skyChangedLightSectionFilter.isEmpty() || !this.blockChangedLightSectionFilter.isEmpty()) { List list = this.playerProvider.getPlayers(this.pos, true); if (!list.isEmpty()) { ClientboundLightUpdatePacket clientboundLightUpdatePacket = new ClientboundLightUpdatePacket( chunk.getPos(), this.lightEngine, this.skyChangedLightSectionFilter, this.blockChangedLightSectionFilter ); this.broadcast(list, clientboundLightUpdatePacket); } this.skyChangedLightSectionFilter.clear(); this.blockChangedLightSectionFilter.clear(); } if (this.hasChangedSections) { List list = this.playerProvider.getPlayers(this.pos, false); for (int i = 0; i < this.changedBlocksPerSection.length; i++) { ShortSet shortSet = this.changedBlocksPerSection[i]; if (shortSet != null) { this.changedBlocksPerSection[i] = null; if (!list.isEmpty()) { int j = this.levelHeightAccessor.getSectionYFromSectionIndex(i); SectionPos sectionPos = SectionPos.of(chunk.getPos(), j); if (shortSet.size() == 1) { BlockPos blockPos = sectionPos.relativeToBlockPos(shortSet.iterator().nextShort()); BlockState blockState = level.getBlockState(blockPos); this.broadcast(list, new ClientboundBlockUpdatePacket(blockPos, blockState)); this.broadcastBlockEntityIfNeeded(list, level, blockPos, blockState); } else { LevelChunkSection levelChunkSection = chunk.getSection(i); ClientboundSectionBlocksUpdatePacket clientboundSectionBlocksUpdatePacket = new ClientboundSectionBlocksUpdatePacket( sectionPos, shortSet, levelChunkSection ); this.broadcast(list, clientboundSectionBlocksUpdatePacket); clientboundSectionBlocksUpdatePacket.runUpdates((blockPos, blockState) -> this.broadcastBlockEntityIfNeeded(list, level, blockPos, blockState)); } } } } this.hasChangedSections = false; } } } private void broadcastBlockEntityIfNeeded(List players, Level level, BlockPos pos, BlockState state) { if (state.hasBlockEntity()) { this.broadcastBlockEntity(players, level, pos); } } private void broadcastBlockEntity(List players, Level level, BlockPos pos) { BlockEntity blockEntity = level.getBlockEntity(pos); if (blockEntity != null) { Packet packet = blockEntity.getUpdatePacket(); if (packet != null) { this.broadcast(players, packet); } } } private void broadcast(List players, Packet packet) { players.forEach(serverPlayer -> serverPlayer.connection.send(packet)); } @Override public int getTicketLevel() { return this.ticketLevel; } @Override public int getQueueLevel() { return this.queueLevel; } private void setQueueLevel(int queueLevel) { this.queueLevel = queueLevel; } public void setTicketLevel(int level) { this.ticketLevel = level; } private void scheduleFullChunkPromotion( ChunkMap chunkMap, CompletableFuture> future, Executor executor, FullChunkStatus fullChunkStatus ) { this.pendingFullStateConfirmation.cancel(false); CompletableFuture completableFuture = new CompletableFuture(); completableFuture.thenRunAsync(() -> chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus), executor); this.pendingFullStateConfirmation = completableFuture; future.thenAccept(chunkResult -> chunkResult.ifSuccess(levelChunk -> completableFuture.complete(null))); } private void demoteFullChunk(ChunkMap chunkMap, FullChunkStatus fullChunkStatus) { this.pendingFullStateConfirmation.cancel(false); chunkMap.onFullChunkStatusChange(this.pos, fullChunkStatus); } protected void updateFutures(ChunkMap chunkMap, Executor executor) { FullChunkStatus fullChunkStatus = ChunkLevel.fullStatus(this.oldTicketLevel); FullChunkStatus fullChunkStatus2 = ChunkLevel.fullStatus(this.ticketLevel); boolean bl = fullChunkStatus.isOrAfter(FullChunkStatus.FULL); boolean bl2 = fullChunkStatus2.isOrAfter(FullChunkStatus.FULL); this.wasAccessibleSinceLastSave |= bl2; if (!bl && bl2) { this.fullChunkFuture = chunkMap.prepareAccessibleChunk(this); this.scheduleFullChunkPromotion(chunkMap, this.fullChunkFuture, executor, FullChunkStatus.FULL); this.addSaveDependency(this.fullChunkFuture); } if (bl && !bl2) { this.fullChunkFuture.complete(UNLOADED_LEVEL_CHUNK); this.fullChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; } boolean bl3 = fullChunkStatus.isOrAfter(FullChunkStatus.BLOCK_TICKING); boolean bl4 = fullChunkStatus2.isOrAfter(FullChunkStatus.BLOCK_TICKING); if (!bl3 && bl4) { this.tickingChunkFuture = chunkMap.prepareTickingChunk(this); this.scheduleFullChunkPromotion(chunkMap, this.tickingChunkFuture, executor, FullChunkStatus.BLOCK_TICKING); this.addSaveDependency(this.tickingChunkFuture); } if (bl3 && !bl4) { this.tickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK); this.tickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; } boolean bl5 = fullChunkStatus.isOrAfter(FullChunkStatus.ENTITY_TICKING); boolean bl6 = fullChunkStatus2.isOrAfter(FullChunkStatus.ENTITY_TICKING); if (!bl5 && bl6) { if (this.entityTickingChunkFuture != UNLOADED_LEVEL_CHUNK_FUTURE) { throw (IllegalStateException)Util.pauseInIde(new IllegalStateException()); } this.entityTickingChunkFuture = chunkMap.prepareEntityTickingChunk(this); this.scheduleFullChunkPromotion(chunkMap, this.entityTickingChunkFuture, executor, FullChunkStatus.ENTITY_TICKING); this.addSaveDependency(this.entityTickingChunkFuture); } if (bl5 && !bl6) { this.entityTickingChunkFuture.complete(UNLOADED_LEVEL_CHUNK); this.entityTickingChunkFuture = UNLOADED_LEVEL_CHUNK_FUTURE; } if (!fullChunkStatus2.isOrAfter(fullChunkStatus)) { this.demoteFullChunk(chunkMap, fullChunkStatus2); } this.onLevelChange.onLevelChange(this.pos, this::getQueueLevel, this.ticketLevel, this::setQueueLevel); this.oldTicketLevel = this.ticketLevel; } public boolean wasAccessibleSinceLastSave() { return this.wasAccessibleSinceLastSave; } public void refreshAccessibility() { this.wasAccessibleSinceLastSave = ChunkLevel.fullStatus(this.ticketLevel).isOrAfter(FullChunkStatus.FULL); } @FunctionalInterface public interface LevelChangeListener { void onLevelChange(ChunkPos chunkPos, IntSupplier queueLevelGetter, int ticketLevel, IntConsumer queueLevelSetter); } public interface PlayerProvider { /** * Returns the players tracking the given chunk. */ List getPlayers(ChunkPos pos, boolean boundaryOnly); } }