2025-11-24 22:52:51 +03:00

615 lines
24 KiB
Java

/*
* 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<LevelChunk> spawningChunks = new ObjectArrayList();
private final Set<ChunkHolder> 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<DimensionDataStorage> 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<ChunkResult<ChunkAccess>> serverFuture = this.getChunkFutureMainThread(x, z, targetStatus, loadOrGenerate);
this.mainThreadProcessor.managedBlock(serverFuture::isDone);
ChunkResult<ChunkAccess> 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<ChunkResult<ChunkAccess>> getChunkFuture(int x, int z, ChunkStatus targetStatus, boolean loadOrGenerate) {
CompletionStage<ChunkResult<ChunkAccess>> 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<ChunkResult<ChunkAccess>> 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<MobCategory> 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<LevelChunk> 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<MobCategory> 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<LevelChunk> 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<? super ClientGamePacketListener> packet) {
this.chunkMap.sendToTrackingPlayersAndSelf(entity, packet);
}
public void sendToTrackingPlayers(Entity entity, Packet<? super ClientGamePacketListener> 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<Runnable> {
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();
}
}
}