357 lines
13 KiB
Java
357 lines
13 KiB
Java
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<LevelChunk> UNLOADED_LEVEL_CHUNK = ChunkResult.error("Unloaded level chunk");
|
|
private static final CompletableFuture<ChunkResult<LevelChunk>> 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<ChunkResult<LevelChunk>> 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<ChunkResult<LevelChunk>> 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<ChunkResult<LevelChunk>> 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<ChunkResult<LevelChunk>> getTickingChunkFuture() {
|
|
return this.tickingChunkFuture;
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> getEntityTickingChunkFuture() {
|
|
return this.entityTickingChunkFuture;
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> players, Level level, BlockPos pos, BlockState state) {
|
|
if (state.hasBlockEntity()) {
|
|
this.broadcastBlockEntity(players, level, pos);
|
|
}
|
|
}
|
|
|
|
private void broadcastBlockEntity(List<ServerPlayer> 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<ServerPlayer> 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<ChunkResult<LevelChunk>> future, Executor executor, FullChunkStatus fullChunkStatus
|
|
) {
|
|
this.pendingFullStateConfirmation.cancel(false);
|
|
CompletableFuture<Void> 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<ServerPlayer> getPlayers(ChunkPos pos, boolean boundaryOnly);
|
|
}
|
|
}
|