package net.minecraft.server.level; import com.google.common.annotations.VisibleForTesting; import com.mojang.datafixers.DataFixer; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.longs.LongSet; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; import java.util.function.BooleanSupplier; import java.util.function.Consumer; import java.util.function.Supplier; import javax.annotation.Nullable; import net.minecraft.FileUtil; import net.minecraft.Util; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.Packet; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.progress.ChunkProgressListener; import net.minecraft.util.VisibleForDebug; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.util.thread.BlockableEventLoop; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.MobCategory; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.LocalMobCapCalculator; import net.minecraft.world.level.NaturalSpawner; import net.minecraft.world.level.TicketStorage; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; import net.minecraft.world.level.chunk.ChunkSource; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LightChunk; import net.minecraft.world.level.chunk.status.ChunkStatus; import net.minecraft.world.level.chunk.storage.ChunkScanAccess; import net.minecraft.world.level.entity.ChunkStatusUpdateListener; import net.minecraft.world.level.levelgen.RandomState; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import net.minecraft.world.level.saveddata.SavedData; import net.minecraft.world.level.storage.DimensionDataStorage; import net.minecraft.world.level.storage.LevelStorageSource; import org.slf4j.Logger; public class ServerChunkCache extends ChunkSource { private static final Logger LOGGER = LogUtils.getLogger(); private final DistanceManager distanceManager; private final ServerLevel level; final Thread mainThread; final ThreadedLevelLightEngine lightEngine; private final ServerChunkCache.MainThreadExecutor mainThreadProcessor; public final ChunkMap chunkMap; private final DimensionDataStorage dataStorage; private final TicketStorage ticketStorage; private long lastInhabitedUpdate; private boolean spawnEnemies = true; private boolean spawnFriendlies = true; private static final int CACHE_SIZE = 4; private final long[] lastChunkPos = new long[4]; private final ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; private final ChunkAccess[] lastChunk = new ChunkAccess[4]; private final List spawningChunks = new ObjectArrayList<>(); private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet<>(); @Nullable @VisibleForDebug private NaturalSpawner.SpawnState lastSpawnState; public ServerChunkCache( ServerLevel p_214982_, LevelStorageSource.LevelStorageAccess p_214983_, DataFixer p_214984_, StructureTemplateManager p_214985_, Executor p_214986_, ChunkGenerator p_214987_, int p_214988_, int p_214989_, boolean p_214990_, ChunkProgressListener p_214991_, ChunkStatusUpdateListener p_214992_, Supplier p_214993_ ) { this.level = p_214982_; this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(p_214982_); this.mainThread = Thread.currentThread(); Path path = p_214983_.getDimensionPath(p_214982_.dimension()).resolve("data"); try { FileUtil.createDirectoriesSafe(path); } catch (IOException ioexception) { LOGGER.error("Failed to create dimension data storage directory", (Throwable)ioexception); } this.dataStorage = new DimensionDataStorage(new SavedData.Context(p_214982_), path, p_214984_, p_214982_.registryAccess()); this.ticketStorage = this.dataStorage.computeIfAbsent(TicketStorage.TYPE); this.chunkMap = new ChunkMap( p_214982_, p_214983_, p_214984_, p_214985_, p_214986_, this.mainThreadProcessor, this, p_214987_, p_214991_, p_214992_, p_214993_, this.ticketStorage, p_214988_, p_214990_ ); this.lightEngine = this.chunkMap.getLightEngine(); this.distanceManager = this.chunkMap.getDistanceManager(); this.distanceManager.updateSimulationDistance(p_214989_); this.clearCache(); } public ThreadedLevelLightEngine getLightEngine() { return this.lightEngine; } @Nullable private ChunkHolder getVisibleChunkIfPresent(long p_8365_) { return this.chunkMap.getVisibleChunkIfPresent(p_8365_); } public int getTickingGenerated() { return this.chunkMap.getTickingGenerated(); } private void storeInCache(long p_8367_, @Nullable ChunkAccess p_8368_, ChunkStatus p_333650_) { for (int i = 3; i > 0; i--) { this.lastChunkPos[i] = this.lastChunkPos[i - 1]; this.lastChunkStatus[i] = this.lastChunkStatus[i - 1]; this.lastChunk[i] = this.lastChunk[i - 1]; } this.lastChunkPos[0] = p_8367_; this.lastChunkStatus[0] = p_333650_; this.lastChunk[0] = p_8368_; } @Nullable @Override public ChunkAccess getChunk(int p_8360_, int p_8361_, ChunkStatus p_334940_, boolean p_8363_) { if (Thread.currentThread() != this.mainThread) { return CompletableFuture.supplyAsync(() -> this.getChunk(p_8360_, p_8361_, p_334940_, p_8363_), this.mainThreadProcessor).join(); } else { ProfilerFiller profilerfiller = Profiler.get(); profilerfiller.incrementCounter("getChunk"); long i = ChunkPos.asLong(p_8360_, p_8361_); for (int j = 0; j < 4; j++) { if (i == this.lastChunkPos[j] && p_334940_ == this.lastChunkStatus[j]) { ChunkAccess chunkaccess = this.lastChunk[j]; if (chunkaccess != null || !p_8363_) { return chunkaccess; } } } profilerfiller.incrementCounter("getChunkCacheMiss"); CompletableFuture> completablefuture = this.getChunkFutureMainThread(p_8360_, p_8361_, p_334940_, p_8363_); this.mainThreadProcessor.managedBlock(completablefuture::isDone); ChunkResult chunkresult = completablefuture.join(); ChunkAccess chunkaccess1 = chunkresult.orElse(null); if (chunkaccess1 == null && p_8363_) { throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkresult.getError())); } else { this.storeInCache(i, chunkaccess1, p_334940_); return chunkaccess1; } } } @Nullable @Override public LevelChunk getChunkNow(int p_8357_, int p_8358_) { if (Thread.currentThread() != this.mainThread) { return null; } else { Profiler.get().incrementCounter("getChunkNow"); long i = ChunkPos.asLong(p_8357_, p_8358_); for (int j = 0; j < 4; j++) { if (i == this.lastChunkPos[j] && this.lastChunkStatus[j] == ChunkStatus.FULL) { ChunkAccess chunkaccess = this.lastChunk[j]; return chunkaccess instanceof LevelChunk ? (LevelChunk)chunkaccess : null; } } ChunkHolder chunkholder = this.getVisibleChunkIfPresent(i); if (chunkholder == null) { return null; } else { ChunkAccess chunkaccess1 = chunkholder.getChunkIfPresent(ChunkStatus.FULL); if (chunkaccess1 != null) { this.storeInCache(i, chunkaccess1, ChunkStatus.FULL); if (chunkaccess1 instanceof LevelChunk) { return (LevelChunk)chunkaccess1; } } return null; } } } private void clearCache() { Arrays.fill(this.lastChunkPos, ChunkPos.INVALID_CHUNK_POS); Arrays.fill(this.lastChunkStatus, null); Arrays.fill(this.lastChunk, null); } public CompletableFuture> getChunkFuture(int p_8432_, int p_8433_, ChunkStatus p_329681_, boolean p_8435_) { boolean flag = Thread.currentThread() == this.mainThread; CompletableFuture> completablefuture; if (flag) { completablefuture = this.getChunkFutureMainThread(p_8432_, p_8433_, p_329681_, p_8435_); this.mainThreadProcessor.managedBlock(completablefuture::isDone); } else { completablefuture = CompletableFuture.>>supplyAsync( () -> this.getChunkFutureMainThread(p_8432_, p_8433_, p_329681_, p_8435_), this.mainThreadProcessor ) .thenCompose(p_333930_ -> (CompletionStage>)p_333930_); } return completablefuture; } private CompletableFuture> getChunkFutureMainThread(int p_8457_, int p_8458_, ChunkStatus p_334479_, boolean p_8460_) { ChunkPos chunkpos = new ChunkPos(p_8457_, p_8458_); long i = chunkpos.toLong(); int j = ChunkLevel.byStatus(p_334479_); ChunkHolder chunkholder = this.getVisibleChunkIfPresent(i); if (p_8460_) { this.addTicket(new Ticket(TicketType.UNKNOWN, j), chunkpos); if (this.chunkAbsent(chunkholder, j)) { ProfilerFiller profilerfiller = Profiler.get(); profilerfiller.push("chunkLoad"); this.runDistanceManagerUpdates(); chunkholder = this.getVisibleChunkIfPresent(i); profilerfiller.pop(); if (this.chunkAbsent(chunkholder, j)) { throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); } } } return this.chunkAbsent(chunkholder, j) ? GenerationChunkHolder.UNLOADED_CHUNK_FUTURE : chunkholder.scheduleChunkGenerationTask(p_334479_, this.chunkMap); } private boolean chunkAbsent(@Nullable ChunkHolder p_8417_, int p_8418_) { return p_8417_ == null || p_8417_.getTicketLevel() > p_8418_; } @Override public boolean hasChunk(int p_8429_, int p_8430_) { ChunkHolder chunkholder = this.getVisibleChunkIfPresent(new ChunkPos(p_8429_, p_8430_).toLong()); int i = ChunkLevel.byStatus(ChunkStatus.FULL); return !this.chunkAbsent(chunkholder, i); } @Nullable @Override public LightChunk getChunkForLighting(int p_8454_, int p_8455_) { long i = ChunkPos.asLong(p_8454_, p_8455_); ChunkHolder chunkholder = this.getVisibleChunkIfPresent(i); return chunkholder == null ? null : chunkholder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent()); } public Level getLevel() { return this.level; } public boolean pollTask() { return this.mainThreadProcessor.pollTask(); } boolean runDistanceManagerUpdates() { boolean flag = this.distanceManager.runAllUpdates(this.chunkMap); boolean flag1 = this.chunkMap.promoteChunkMap(); this.chunkMap.runGenerationTasks(); if (!flag && !flag1) { return false; } else { this.clearCache(); return true; } } public boolean isPositionTicking(long p_143240_) { if (!this.level.shouldTickBlocksAt(p_143240_)) { return false; } else { ChunkHolder chunkholder = this.getVisibleChunkIfPresent(p_143240_); return chunkholder == null ? false : chunkholder.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).isSuccess(); } } public void save(boolean p_8420_) { this.runDistanceManagerUpdates(); this.chunkMap.saveAllChunks(p_8420_); } @Override public void close() throws IOException { this.save(true); this.dataStorage.close(); this.lightEngine.close(); this.chunkMap.close(); } @Override public void tick(BooleanSupplier p_201913_, boolean p_201914_) { ProfilerFiller profilerfiller = Profiler.get(); profilerfiller.push("purge"); if (this.level.tickRateManager().runsNormally() || !p_201914_) { this.ticketStorage.purgeStaleTickets(); } this.runDistanceManagerUpdates(); profilerfiller.popPush("chunks"); if (p_201914_) { this.tickChunks(); this.chunkMap.tick(); } profilerfiller.popPush("unload"); this.chunkMap.tick(p_201913_); profilerfiller.pop(); this.clearCache(); } private void tickChunks() { long i = this.level.getGameTime(); long j = i - this.lastInhabitedUpdate; this.lastInhabitedUpdate = i; if (!this.level.isDebug()) { ProfilerFiller profilerfiller = Profiler.get(); profilerfiller.push("pollingChunks"); if (this.level.tickRateManager().runsNormally()) { profilerfiller.push("tickingChunks"); this.tickChunks(profilerfiller, j); profilerfiller.pop(); } this.broadcastChangedChunks(profilerfiller); profilerfiller.pop(); } } private void broadcastChangedChunks(ProfilerFiller p_369706_) { p_369706_.push("broadcast"); for (ChunkHolder chunkholder : this.chunkHoldersToBroadcast) { LevelChunk levelchunk = chunkholder.getTickingChunk(); if (levelchunk != null) { chunkholder.broadcastChanges(levelchunk); } } this.chunkHoldersToBroadcast.clear(); p_369706_.pop(); } private void tickChunks(ProfilerFiller p_368327_, long p_362313_) { p_368327_.popPush("naturalSpawnCount"); int i = this.distanceManager.getNaturalSpawnChunkCount(); NaturalSpawner.SpawnState naturalspawner$spawnstate = NaturalSpawner.createState( i, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap) ); this.lastSpawnState = naturalspawner$spawnstate; p_368327_.popPush("spawnAndTick"); boolean flag = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING); int j = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); List list; if (flag && (this.spawnEnemies || this.spawnFriendlies)) { boolean flag1 = this.level.getLevelData().getGameTime() % 400L == 0L; list = NaturalSpawner.getFilteredSpawningCategories(naturalspawner$spawnstate, this.spawnFriendlies, this.spawnEnemies, flag1); } else { list = List.of(); } List list1 = this.spawningChunks; try { p_368327_.push("filteringSpawningChunks"); this.chunkMap.collectSpawningChunks(list1); p_368327_.popPush("shuffleSpawningChunks"); Util.shuffle(list1, this.level.random); p_368327_.popPush("tickSpawningChunks"); for (LevelChunk levelchunk : list1) { this.tickSpawningChunk(levelchunk, p_362313_, list, naturalspawner$spawnstate); } } finally { list1.clear(); } p_368327_.popPush("tickTickingChunks"); this.chunkMap.forEachBlockTickingChunk(p_390143_ -> this.level.tickChunk(p_390143_, j)); p_368327_.pop(); p_368327_.popPush("customSpawners"); if (flag) { this.level.tickCustomSpawners(this.spawnEnemies, this.spawnFriendlies); } } private void tickSpawningChunk(LevelChunk p_394359_, long p_392468_, List p_396237_, NaturalSpawner.SpawnState p_392979_) { ChunkPos chunkpos = p_394359_.getPos(); p_394359_.incrementInhabitedTime(p_392468_); if (this.distanceManager.inEntityTickingRange(chunkpos.toLong())) { this.level.tickThunder(p_394359_); } if (!p_396237_.isEmpty()) { if (this.level.canSpawnEntitiesInChunk(chunkpos)) { NaturalSpawner.spawnForChunk(this.level, p_394359_, p_392979_, p_396237_); } } } private void getFullChunk(long p_8371_, Consumer p_8372_) { ChunkHolder chunkholder = this.getVisibleChunkIfPresent(p_8371_); if (chunkholder != null) { chunkholder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifSuccess(p_8372_); } } @Override public String gatherStats() { return Integer.toString(this.getLoadedChunksCount()); } @VisibleForTesting public int getPendingTasksCount() { return this.mainThreadProcessor.getPendingTasksCount(); } public ChunkGenerator getGenerator() { return this.chunkMap.generator(); } public ChunkGeneratorStructureState getGeneratorState() { return this.chunkMap.generatorState(); } public RandomState randomState() { return this.chunkMap.randomState(); } @Override public int getLoadedChunksCount() { return this.chunkMap.size(); } public void blockChanged(BlockPos p_8451_) { int i = SectionPos.blockToSectionCoord(p_8451_.getX()); int j = SectionPos.blockToSectionCoord(p_8451_.getZ()); ChunkHolder chunkholder = this.getVisibleChunkIfPresent(ChunkPos.asLong(i, j)); if (chunkholder != null && chunkholder.blockChanged(p_8451_)) { this.chunkHoldersToBroadcast.add(chunkholder); } } @Override public void onLightUpdate(LightLayer p_8403_, SectionPos p_8404_) { this.mainThreadProcessor.execute(() -> { ChunkHolder chunkholder = this.getVisibleChunkIfPresent(p_8404_.chunk().toLong()); if (chunkholder != null && chunkholder.sectionLightChanged(p_8403_, p_8404_.y())) { this.chunkHoldersToBroadcast.add(chunkholder); } }); } public void addTicket(Ticket p_392821_, ChunkPos p_393687_) { this.ticketStorage.addTicket(p_392821_, p_393687_); } public void addTicketWithRadius(TicketType p_391769_, ChunkPos p_392988_, int p_397576_) { this.ticketStorage.addTicketWithRadius(p_391769_, p_392988_, p_397576_); } public void removeTicketWithRadius(TicketType p_392479_, ChunkPos p_394338_, int p_393500_) { this.ticketStorage.removeTicketWithRadius(p_392479_, p_394338_, p_393500_); } @Override public boolean updateChunkForced(ChunkPos p_8400_, boolean p_8401_) { return this.ticketStorage.updateChunkForced(p_8400_, p_8401_); } @Override public LongSet getForceLoadedChunks() { return this.ticketStorage.getForceLoadedChunks(); } public void move(ServerPlayer p_8386_) { if (!p_8386_.isRemoved()) { this.chunkMap.move(p_8386_); } } public void removeEntity(Entity p_8444_) { this.chunkMap.removeEntity(p_8444_); } public void addEntity(Entity p_8464_) { this.chunkMap.addEntity(p_8464_); } public void broadcastAndSend(Entity p_8395_, Packet p_8396_) { this.chunkMap.broadcastAndSend(p_8395_, p_8396_); } public void broadcast(Entity p_8446_, Packet p_8447_) { this.chunkMap.broadcast(p_8446_, p_8447_); } public void setViewDistance(int p_8355_) { this.chunkMap.setServerViewDistance(p_8355_); } public void setSimulationDistance(int p_184027_) { this.distanceManager.updateSimulationDistance(p_184027_); } @Override public void setSpawnSettings(boolean p_8425_) { this.spawnEnemies = p_8425_; this.spawnFriendlies = this.spawnFriendlies; } public String getChunkDebugData(ChunkPos p_8449_) { return this.chunkMap.getChunkDebugData(p_8449_); } public DimensionDataStorage getDataStorage() { return this.dataStorage; } public PoiManager getPoiManager() { return this.chunkMap.getPoiManager(); } public ChunkScanAccess chunkScanner() { return this.chunkMap.chunkScanner(); } @Nullable @VisibleForDebug public NaturalSpawner.SpawnState getLastSpawnState() { return this.lastSpawnState; } public void deactivateTicketsOnClosing() { this.ticketStorage.deactivateTicketsOnClosing(); } public void onChunkReadyToSend(ChunkHolder p_370261_) { if (p_370261_.hasChangesToBroadcast()) { this.chunkHoldersToBroadcast.add(p_370261_); } } final class MainThreadExecutor extends BlockableEventLoop { MainThreadExecutor(final Level p_8494_) { super("Chunk source main thread executor for " + p_8494_.dimension().location()); } @Override public void managedBlock(BooleanSupplier p_344943_) { super.managedBlock(() -> MinecraftServer.throwIfFatalException() && p_344943_.getAsBoolean()); } @Override public Runnable wrapRunnable(Runnable p_8506_) { return p_8506_; } @Override protected boolean shouldRun(Runnable p_8504_) { return true; } @Override protected boolean scheduleExecutables() { return true; } @Override protected Thread getRunningThread() { return ServerChunkCache.this.mainThread; } @Override protected void doRunTask(Runnable p_8502_) { Profiler.get().incrementCounter("runTask"); super.doRunTask(p_8502_); } @Override public boolean pollTask() { if (ServerChunkCache.this.runDistanceManagerUpdates()) { return true; } else { ServerChunkCache.this.lightEngine.tryScheduleUpdate(); return super.pollTask(); } } } }