/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.google.common.annotations.VisibleForTesting * com.mojang.datafixers.DataFixer * com.mojang.logging.LogUtils * it.unimi.dsi.fastutil.longs.LongSet * it.unimi.dsi.fastutil.objects.ObjectArrayList * it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet * org.jspecify.annotations.Nullable * org.slf4j.Logger */ 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.Objects; 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 net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; import net.minecraft.network.protocol.Packet; import net.minecraft.network.protocol.game.ClientGamePacketListener; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ChunkHolder; import net.minecraft.server.level.ChunkLevel; import net.minecraft.server.level.ChunkMap; import net.minecraft.server.level.ChunkResult; import net.minecraft.server.level.DistanceManager; import net.minecraft.server.level.GenerationChunkHolder; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ThreadedLevelLightEngine; import net.minecraft.server.level.Ticket; import net.minecraft.server.level.TicketType; import net.minecraft.util.FileUtil; import net.minecraft.util.Util; 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.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.gamerules.GameRules; import net.minecraft.world.level.levelgen.RandomState; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; import net.minecraft.world.level.storage.DimensionDataStorage; import net.minecraft.world.level.storage.LevelStorageSource; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class ServerChunkCache extends ChunkSource { private static final Logger LOGGER = LogUtils.getLogger(); private final DistanceManager distanceManager; private final ServerLevel level; private final Thread mainThread; private final ThreadedLevelLightEngine lightEngine; private final MainThreadExecutor mainThreadProcessor; public final ChunkMap chunkMap; private final DimensionDataStorage dataStorage; private final TicketStorage ticketStorage; private long lastInhabitedUpdate; private boolean spawnEnemies = true; private static final int CACHE_SIZE = 4; private final long[] lastChunkPos = new long[4]; private final @Nullable ChunkStatus[] lastChunkStatus = new ChunkStatus[4]; private final @Nullable ChunkAccess[] lastChunk = new ChunkAccess[4]; private final List spawningChunks = new ObjectArrayList(); private final Set chunkHoldersToBroadcast = new ReferenceOpenHashSet(); @VisibleForDebug private @Nullable NaturalSpawner.SpawnState lastSpawnState; public ServerChunkCache(ServerLevel level, LevelStorageSource.LevelStorageAccess levelStorage, DataFixer fixerUpper, StructureTemplateManager structureTemplateManager, Executor executor, ChunkGenerator generator, int viewDistance, int simulationDistance, boolean syncWrites, ChunkStatusUpdateListener chunkStatusListener, Supplier overworldDataStorage) { this.level = level; this.mainThreadProcessor = new MainThreadExecutor(level); this.mainThread = Thread.currentThread(); Path dataFolder = levelStorage.getDimensionPath(level.dimension()).resolve("data"); try { FileUtil.createDirectoriesSafe(dataFolder); } catch (IOException e) { LOGGER.error("Failed to create dimension data storage directory", (Throwable)e); } this.dataStorage = new DimensionDataStorage(dataFolder, fixerUpper, level.registryAccess()); this.ticketStorage = this.dataStorage.computeIfAbsent(TicketStorage.TYPE); this.chunkMap = new ChunkMap(level, levelStorage, fixerUpper, structureTemplateManager, executor, this.mainThreadProcessor, this, generator, chunkStatusListener, overworldDataStorage, this.ticketStorage, viewDistance, syncWrites); this.lightEngine = this.chunkMap.getLightEngine(); this.distanceManager = this.chunkMap.getDistanceManager(); this.distanceManager.updateSimulationDistance(simulationDistance); this.clearCache(); } @Override public ThreadedLevelLightEngine getLightEngine() { return this.lightEngine; } private @Nullable ChunkHolder getVisibleChunkIfPresent(long key) { return this.chunkMap.getVisibleChunkIfPresent(key); } private void storeInCache(long pos, @Nullable ChunkAccess chunk, ChunkStatus status) { 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] = pos; this.lastChunkStatus[0] = status; this.lastChunk[0] = chunk; } @Override public @Nullable ChunkAccess getChunk(int x, int z, ChunkStatus targetStatus, boolean loadOrGenerate) { if (Thread.currentThread() != this.mainThread) { return CompletableFuture.supplyAsync(() -> this.getChunk(x, z, targetStatus, loadOrGenerate), this.mainThreadProcessor).join(); } ProfilerFiller profiler = Profiler.get(); profiler.incrementCounter("getChunk"); long pos = ChunkPos.asLong(x, z); for (int i = 0; i < 4; ++i) { ChunkAccess chunkAccess; if (pos != this.lastChunkPos[i] || targetStatus != this.lastChunkStatus[i] || (chunkAccess = this.lastChunk[i]) == null && loadOrGenerate) continue; return chunkAccess; } profiler.incrementCounter("getChunkCacheMiss"); CompletableFuture> serverFuture = this.getChunkFutureMainThread(x, z, targetStatus, loadOrGenerate); this.mainThreadProcessor.managedBlock(serverFuture::isDone); ChunkResult chunkResult = serverFuture.join(); ChunkAccess chunk = chunkResult.orElse(null); if (chunk == null && loadOrGenerate) { throw Util.pauseInIde(new IllegalStateException("Chunk not there when requested: " + chunkResult.getError())); } this.storeInCache(pos, chunk, targetStatus); return chunk; } @Override public @Nullable LevelChunk getChunkNow(int x, int z) { if (Thread.currentThread() != this.mainThread) { return null; } Profiler.get().incrementCounter("getChunkNow"); long pos = ChunkPos.asLong(x, z); for (int i = 0; i < 4; ++i) { if (pos != this.lastChunkPos[i] || this.lastChunkStatus[i] != ChunkStatus.FULL) continue; ChunkAccess chunkAccess = this.lastChunk[i]; return chunkAccess instanceof LevelChunk ? (LevelChunk)chunkAccess : null; } ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(pos); if (chunkHolder == null) { return null; } ChunkAccess chunk = chunkHolder.getChunkIfPresent(ChunkStatus.FULL); if (chunk != null) { this.storeInCache(pos, chunk, ChunkStatus.FULL); if (chunk instanceof LevelChunk) { return (LevelChunk)chunk; } } 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 x, int z, ChunkStatus targetStatus, boolean loadOrGenerate) { CompletionStage> serverFuture; boolean isMainThread; boolean bl = isMainThread = Thread.currentThread() == this.mainThread; if (isMainThread) { serverFuture = this.getChunkFutureMainThread(x, z, targetStatus, loadOrGenerate); this.mainThreadProcessor.managedBlock(() -> serverFuture.isDone()); } else { serverFuture = CompletableFuture.supplyAsync(() -> this.getChunkFutureMainThread(x, z, targetStatus, loadOrGenerate), this.mainThreadProcessor).thenCompose(chunk -> chunk); } return serverFuture; } private CompletableFuture> getChunkFutureMainThread(int x, int z, ChunkStatus targetStatus, boolean loadOrGenerate) { ChunkPos pos = new ChunkPos(x, z); long key = pos.toLong(); int targetTicketLevel = ChunkLevel.byStatus(targetStatus); ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(key); if (loadOrGenerate) { this.addTicket(new Ticket(TicketType.UNKNOWN, targetTicketLevel), pos); if (this.chunkAbsent(chunkHolder, targetTicketLevel)) { ProfilerFiller profiler = Profiler.get(); profiler.push("chunkLoad"); this.runDistanceManagerUpdates(); chunkHolder = this.getVisibleChunkIfPresent(key); profiler.pop(); if (this.chunkAbsent(chunkHolder, targetTicketLevel)) { throw Util.pauseInIde(new IllegalStateException("No chunk holder after ticket has been added")); } } } if (this.chunkAbsent(chunkHolder, targetTicketLevel)) { return GenerationChunkHolder.UNLOADED_CHUNK_FUTURE; } return chunkHolder.scheduleChunkGenerationTask(targetStatus, this.chunkMap); } private boolean chunkAbsent(@Nullable ChunkHolder chunkHolder, int targetTicketLevel) { return chunkHolder == null || chunkHolder.getTicketLevel() > targetTicketLevel; } @Override public boolean hasChunk(int x, int z) { int targetTicketLevel; ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(new ChunkPos(x, z).toLong()); return !this.chunkAbsent(chunkHolder, targetTicketLevel = ChunkLevel.byStatus(ChunkStatus.FULL)); } @Override public @Nullable LightChunk getChunkForLighting(int x, int z) { long key = ChunkPos.asLong(x, z); ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(key); if (chunkHolder == null) { return null; } return chunkHolder.getChunkIfPresentUnchecked(ChunkStatus.INITIALIZE_LIGHT.getParent()); } @Override public Level getLevel() { return this.level; } public boolean pollTask() { return this.mainThreadProcessor.pollTask(); } boolean runDistanceManagerUpdates() { boolean updated = this.distanceManager.runAllUpdates(this.chunkMap); boolean promoted = this.chunkMap.promoteChunkMap(); this.chunkMap.runGenerationTasks(); if (updated || promoted) { this.clearCache(); return true; } return false; } public boolean isPositionTicking(long chunkKey) { if (!this.level.shouldTickBlocksAt(chunkKey)) { return false; } ChunkHolder holder = this.getVisibleChunkIfPresent(chunkKey); if (holder == null) { return false; } return holder.getTickingChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).isSuccess(); } public void save(boolean flushStorage) { this.runDistanceManagerUpdates(); this.chunkMap.saveAllChunks(flushStorage); } @Override public void close() throws IOException { this.save(true); this.dataStorage.close(); this.lightEngine.close(); this.chunkMap.close(); } @Override public void tick(BooleanSupplier haveTime, boolean tickChunks) { ProfilerFiller profiler = Profiler.get(); profiler.push("purge"); if (this.level.tickRateManager().runsNormally() || !tickChunks) { this.ticketStorage.purgeStaleTickets(this.chunkMap); } this.runDistanceManagerUpdates(); profiler.popPush("chunks"); if (tickChunks) { this.tickChunks(); this.chunkMap.tick(); } profiler.popPush("unload"); this.chunkMap.tick(haveTime); profiler.pop(); this.clearCache(); } private void tickChunks() { long time = this.level.getGameTime(); long timeDiff = time - this.lastInhabitedUpdate; this.lastInhabitedUpdate = time; if (this.level.isDebug()) { return; } ProfilerFiller profiler = Profiler.get(); profiler.push("pollingChunks"); if (this.level.tickRateManager().runsNormally()) { profiler.push("tickingChunks"); this.tickChunks(profiler, timeDiff); profiler.pop(); } this.broadcastChangedChunks(profiler); profiler.pop(); } private void broadcastChangedChunks(ProfilerFiller profiler) { profiler.push("broadcast"); for (ChunkHolder chunkHolder : this.chunkHoldersToBroadcast) { LevelChunk chunk = chunkHolder.getTickingChunk(); if (chunk == null) continue; chunkHolder.broadcastChanges(chunk); } this.chunkHoldersToBroadcast.clear(); profiler.pop(); } /* * WARNING - Removed try catching itself - possible behaviour change. */ private void tickChunks(ProfilerFiller profiler, long timeDiff) { List spawningCategories; NaturalSpawner.SpawnState spawnCookie; profiler.push("naturalSpawnCount"); int chunkCount = this.distanceManager.getNaturalSpawnChunkCount(); this.lastSpawnState = spawnCookie = NaturalSpawner.createState(chunkCount, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap)); boolean doMobSpawning = this.level.getGameRules().get(GameRules.SPAWN_MOBS); int tickSpeed = this.level.getGameRules().get(GameRules.RANDOM_TICK_SPEED); if (doMobSpawning) { boolean spawnPersistent = this.level.getGameTime() % 400L == 0L; spawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnCookie, true, this.spawnEnemies, spawnPersistent); } else { spawningCategories = List.of(); } List spawningChunks = this.spawningChunks; try { profiler.popPush("filteringSpawningChunks"); this.chunkMap.collectSpawningChunks(spawningChunks); profiler.popPush("shuffleSpawningChunks"); Util.shuffle(spawningChunks, this.level.random); profiler.popPush("tickSpawningChunks"); for (LevelChunk chunk2 : spawningChunks) { this.tickSpawningChunk(chunk2, timeDiff, spawningCategories, spawnCookie); } } finally { spawningChunks.clear(); } profiler.popPush("tickTickingChunks"); this.chunkMap.forEachBlockTickingChunk(chunk -> this.level.tickChunk((LevelChunk)chunk, tickSpeed)); if (doMobSpawning) { profiler.popPush("customSpawners"); this.level.tickCustomSpawners(this.spawnEnemies); } profiler.pop(); } private void tickSpawningChunk(LevelChunk chunk, long timeDiff, List spawningCategories, NaturalSpawner.SpawnState spawnCookie) { ChunkPos chunkPos = chunk.getPos(); chunk.incrementInhabitedTime(timeDiff); if (this.distanceManager.inEntityTickingRange(chunkPos.toLong())) { this.level.tickThunder(chunk); } if (spawningCategories.isEmpty()) { return; } if (this.level.canSpawnEntitiesInChunk(chunkPos)) { NaturalSpawner.spawnForChunk(this.level, chunk, spawnCookie, spawningCategories); } } private void getFullChunk(long chunkKey, Consumer output) { ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(chunkKey); if (chunkHolder != null) { chunkHolder.getFullChunkFuture().getNow(ChunkHolder.UNLOADED_LEVEL_CHUNK).ifSuccess(output); } } @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 pos) { int zc; int xc = SectionPos.blockToSectionCoord(pos.getX()); ChunkHolder chunk = this.getVisibleChunkIfPresent(ChunkPos.asLong(xc, zc = SectionPos.blockToSectionCoord(pos.getZ()))); if (chunk != null && chunk.blockChanged(pos)) { this.chunkHoldersToBroadcast.add(chunk); } } @Override public void onLightUpdate(LightLayer layer, SectionPos pos) { this.mainThreadProcessor.execute(() -> { ChunkHolder chunk = this.getVisibleChunkIfPresent(pos.chunk().toLong()); if (chunk != null && chunk.sectionLightChanged(layer, pos.y())) { this.chunkHoldersToBroadcast.add(chunk); } }); } public boolean hasActiveTickets() { return this.ticketStorage.shouldKeepDimensionActive(); } public void addTicket(Ticket ticket, ChunkPos pos) { this.ticketStorage.addTicket(ticket, pos); } public CompletableFuture addTicketAndLoadWithRadius(TicketType type, ChunkPos pos, int radius) { if (!type.doesLoad()) { throw new IllegalStateException("Ticket type " + String.valueOf(type) + " does not trigger chunk loading"); } if (type.canExpireIfUnloaded()) { throw new IllegalStateException("Ticket type " + String.valueOf(type) + " can expire before it loads, cannot fetch asynchronously"); } this.addTicketWithRadius(type, pos, radius); this.runDistanceManagerUpdates(); ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(pos.toLong()); Objects.requireNonNull(chunkHolder, "No chunk was scheduled for loading"); return this.chunkMap.getChunkRangeFuture(chunkHolder, radius, distance -> ChunkStatus.FULL); } public void addTicketWithRadius(TicketType type, ChunkPos pos, int radius) { this.ticketStorage.addTicketWithRadius(type, pos, radius); } public void removeTicketWithRadius(TicketType type, ChunkPos pos, int radius) { this.ticketStorage.removeTicketWithRadius(type, pos, radius); } @Override public boolean updateChunkForced(ChunkPos pos, boolean forced) { return this.ticketStorage.updateChunkForced(pos, forced); } @Override public LongSet getForceLoadedChunks() { return this.ticketStorage.getForceLoadedChunks(); } public void move(ServerPlayer player) { if (!player.isRemoved()) { this.chunkMap.move(player); if (player.isReceivingWaypoints()) { this.level.getWaypointManager().updatePlayer(player); } } } public void removeEntity(Entity entity) { this.chunkMap.removeEntity(entity); } public void addEntity(Entity entity) { this.chunkMap.addEntity(entity); } public void sendToTrackingPlayersAndSelf(Entity entity, Packet packet) { this.chunkMap.sendToTrackingPlayersAndSelf(entity, packet); } public void sendToTrackingPlayers(Entity entity, Packet packet) { this.chunkMap.sendToTrackingPlayers(entity, packet); } public void setViewDistance(int newDistance) { this.chunkMap.setServerViewDistance(newDistance); } public void setSimulationDistance(int simulationDistance) { this.distanceManager.updateSimulationDistance(simulationDistance); } @Override public void setSpawnSettings(boolean spawnEnemies) { this.spawnEnemies = spawnEnemies; } public String getChunkDebugData(ChunkPos pos) { return this.chunkMap.getChunkDebugData(pos); } public DimensionDataStorage getDataStorage() { return this.dataStorage; } public PoiManager getPoiManager() { return this.chunkMap.getPoiManager(); } public ChunkScanAccess chunkScanner() { return this.chunkMap.chunkScanner(); } @VisibleForDebug public @Nullable NaturalSpawner.SpawnState getLastSpawnState() { return this.lastSpawnState; } public void deactivateTicketsOnClosing() { this.ticketStorage.deactivateTicketsOnClosing(); } public void onChunkReadyToSend(ChunkHolder chunk) { if (chunk.hasChangesToBroadcast()) { this.chunkHoldersToBroadcast.add(chunk); } } private final class MainThreadExecutor extends BlockableEventLoop { private MainThreadExecutor(Level level) { super("Chunk source main thread executor for " + String.valueOf(level.dimension().identifier())); } @Override public void managedBlock(BooleanSupplier condition) { super.managedBlock(() -> MinecraftServer.throwIfFatalException() && condition.getAsBoolean()); } @Override public Runnable wrapRunnable(Runnable runnable) { return runnable; } @Override protected boolean shouldRun(Runnable task) { return true; } @Override protected boolean scheduleExecutables() { return true; } @Override protected Thread getRunningThread() { return ServerChunkCache.this.mainThread; } @Override protected void doRunTask(Runnable task) { Profiler.get().incrementCounter("runTask"); super.doRunTask(task); } @Override protected boolean pollTask() { if (ServerChunkCache.this.runDistanceManagerUpdates()) { return true; } ServerChunkCache.this.lightEngine.tryScheduleUpdate(); return super.pollTask(); } } }