1327 lines
60 KiB
Java
1327 lines
60 KiB
Java
/*
|
|
* Decompiled with CFR 0.152.
|
|
*
|
|
* Could not load the following classes:
|
|
* com.google.common.collect.ImmutableList
|
|
* com.google.common.collect.ImmutableList$Builder
|
|
* com.google.common.collect.Lists
|
|
* com.google.common.collect.Queues
|
|
* com.google.common.collect.Sets
|
|
* com.mojang.datafixers.DataFixer
|
|
* com.mojang.logging.LogUtils
|
|
* com.mojang.serialization.MapCodec
|
|
* it.unimi.dsi.fastutil.ints.Int2ObjectMap
|
|
* it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
|
|
* it.unimi.dsi.fastutil.longs.Long2ByteMap
|
|
* it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap
|
|
* it.unimi.dsi.fastutil.longs.Long2LongMap
|
|
* it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap
|
|
* it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap
|
|
* it.unimi.dsi.fastutil.longs.Long2ObjectMap$Entry
|
|
* it.unimi.dsi.fastutil.longs.LongIterator
|
|
* it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet
|
|
* it.unimi.dsi.fastutil.longs.LongOpenHashSet
|
|
* it.unimi.dsi.fastutil.longs.LongSet
|
|
* org.apache.commons.lang3.mutable.MutableBoolean
|
|
* org.jspecify.annotations.Nullable
|
|
* org.slf4j.Logger
|
|
*/
|
|
package net.minecraft.server.level;
|
|
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Queues;
|
|
import com.google.common.collect.Sets;
|
|
import com.mojang.datafixers.DataFixer;
|
|
import com.mojang.logging.LogUtils;
|
|
import com.mojang.serialization.MapCodec;
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
|
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ByteMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2LongMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
|
import it.unimi.dsi.fastutil.longs.LongIterator;
|
|
import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
|
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
|
import java.io.IOException;
|
|
import java.io.Writer;
|
|
import java.nio.file.Path;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Queue;
|
|
import java.util.Set;
|
|
import java.util.concurrent.CancellationException;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.CompletionException;
|
|
import java.util.concurrent.CompletionStage;
|
|
import java.util.concurrent.Executor;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
import java.util.function.BooleanSupplier;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.IntConsumer;
|
|
import java.util.function.IntFunction;
|
|
import java.util.function.IntSupplier;
|
|
import java.util.function.Predicate;
|
|
import java.util.function.Supplier;
|
|
import java.util.stream.Stream;
|
|
import net.minecraft.CrashReport;
|
|
import net.minecraft.CrashReportCategory;
|
|
import net.minecraft.ReportedException;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.RegistryAccess;
|
|
import net.minecraft.core.SectionPos;
|
|
import net.minecraft.core.registries.Registries;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.nbt.NbtException;
|
|
import net.minecraft.network.protocol.Packet;
|
|
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
|
import net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket;
|
|
import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
|
|
import net.minecraft.resources.ResourceKey;
|
|
import net.minecraft.server.level.ChunkGenerationTask;
|
|
import net.minecraft.server.level.ChunkHolder;
|
|
import net.minecraft.server.level.ChunkLevel;
|
|
import net.minecraft.server.level.ChunkResult;
|
|
import net.minecraft.server.level.ChunkTaskDispatcher;
|
|
import net.minecraft.server.level.ChunkTaskPriorityQueue;
|
|
import net.minecraft.server.level.ChunkTrackingView;
|
|
import net.minecraft.server.level.FullChunkStatus;
|
|
import net.minecraft.server.level.GeneratingChunkMap;
|
|
import net.minecraft.server.level.GenerationChunkHolder;
|
|
import net.minecraft.server.level.PlayerMap;
|
|
import net.minecraft.server.level.ServerEntity;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.server.level.ServerPlayer;
|
|
import net.minecraft.server.level.ThreadedLevelLightEngine;
|
|
import net.minecraft.server.network.ServerPlayerConnection;
|
|
import net.minecraft.util.CsvOutput;
|
|
import net.minecraft.util.Mth;
|
|
import net.minecraft.util.StaticCache2D;
|
|
import net.minecraft.util.TriState;
|
|
import net.minecraft.util.Util;
|
|
import net.minecraft.util.datafix.DataFixTypes;
|
|
import net.minecraft.util.profiling.Profiler;
|
|
import net.minecraft.util.profiling.ProfilerFiller;
|
|
import net.minecraft.util.thread.BlockableEventLoop;
|
|
import net.minecraft.util.thread.ConsecutiveExecutor;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.EntityType;
|
|
import net.minecraft.world.entity.ai.village.poi.PoiManager;
|
|
import net.minecraft.world.entity.boss.EnderDragonPart;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.Level;
|
|
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.ImposterProtoChunk;
|
|
import net.minecraft.world.level.chunk.LevelChunk;
|
|
import net.minecraft.world.level.chunk.LightChunkGetter;
|
|
import net.minecraft.world.level.chunk.ProtoChunk;
|
|
import net.minecraft.world.level.chunk.UpgradeData;
|
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
|
import net.minecraft.world.level.chunk.status.ChunkStep;
|
|
import net.minecraft.world.level.chunk.status.ChunkType;
|
|
import net.minecraft.world.level.chunk.status.WorldGenContext;
|
|
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
|
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
|
import net.minecraft.world.level.chunk.storage.SimpleRegionStorage;
|
|
import net.minecraft.world.level.entity.ChunkStatusUpdateListener;
|
|
import net.minecraft.world.level.gamerules.GameRules;
|
|
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
|
|
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
|
import net.minecraft.world.level.levelgen.RandomState;
|
|
import net.minecraft.world.level.levelgen.structure.LegacyStructureDataHandler;
|
|
import net.minecraft.world.level.levelgen.structure.StructureStart;
|
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
|
|
import net.minecraft.world.level.storage.DimensionDataStorage;
|
|
import net.minecraft.world.level.storage.LevelStorageSource;
|
|
import net.minecraft.world.phys.Vec3;
|
|
import org.apache.commons.lang3.mutable.MutableBoolean;
|
|
import org.jspecify.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public class ChunkMap
|
|
extends SimpleRegionStorage
|
|
implements ChunkHolder.PlayerProvider,
|
|
GeneratingChunkMap {
|
|
private static final ChunkResult<List<ChunkAccess>> UNLOADED_CHUNK_LIST_RESULT = ChunkResult.error("Unloaded chunks found in range");
|
|
private static final CompletableFuture<ChunkResult<List<ChunkAccess>>> UNLOADED_CHUNK_LIST_FUTURE = CompletableFuture.completedFuture(UNLOADED_CHUNK_LIST_RESULT);
|
|
private static final byte CHUNK_TYPE_REPLACEABLE = -1;
|
|
private static final byte CHUNK_TYPE_UNKNOWN = 0;
|
|
private static final byte CHUNK_TYPE_FULL = 1;
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
private static final int CHUNK_SAVED_PER_TICK = 200;
|
|
private static final int CHUNK_SAVED_EAGERLY_PER_TICK = 20;
|
|
private static final int EAGER_CHUNK_SAVE_COOLDOWN_IN_MILLIS = 10000;
|
|
private static final int MAX_ACTIVE_CHUNK_WRITES = 128;
|
|
public static final int MIN_VIEW_DISTANCE = 2;
|
|
public static final int MAX_VIEW_DISTANCE = 32;
|
|
public static final int FORCED_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
|
|
private final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap();
|
|
private volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap = this.updatingChunkMap.clone();
|
|
private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads = new Long2ObjectLinkedOpenHashMap();
|
|
private final List<ChunkGenerationTask> pendingGenerationTasks = new ArrayList<ChunkGenerationTask>();
|
|
private final ServerLevel level;
|
|
private final ThreadedLevelLightEngine lightEngine;
|
|
private final BlockableEventLoop<Runnable> mainThreadExecutor;
|
|
private final RandomState randomState;
|
|
private final ChunkGeneratorStructureState chunkGeneratorState;
|
|
private final TicketStorage ticketStorage;
|
|
private final PoiManager poiManager;
|
|
private final LongSet toDrop = new LongOpenHashSet();
|
|
private boolean modified;
|
|
private final ChunkTaskDispatcher worldgenTaskDispatcher;
|
|
private final ChunkTaskDispatcher lightTaskDispatcher;
|
|
private final ChunkStatusUpdateListener chunkStatusListener;
|
|
private final DistanceManager distanceManager;
|
|
private final String storageName;
|
|
private final PlayerMap playerMap = new PlayerMap();
|
|
private final Int2ObjectMap<TrackedEntity> entityMap = new Int2ObjectOpenHashMap();
|
|
private final Long2ByteMap chunkTypeCache = new Long2ByteOpenHashMap();
|
|
private final Long2LongMap nextChunkSaveTime = new Long2LongOpenHashMap();
|
|
private final LongSet chunksToEagerlySave = new LongLinkedOpenHashSet();
|
|
private final Queue<Runnable> unloadQueue = Queues.newConcurrentLinkedQueue();
|
|
private final AtomicInteger activeChunkWrites = new AtomicInteger();
|
|
private int serverViewDistance;
|
|
private final WorldGenContext worldGenContext;
|
|
|
|
public ChunkMap(ServerLevel level, LevelStorageSource.LevelStorageAccess levelStorage, DataFixer dataFixer, StructureTemplateManager structureManager, Executor executor, BlockableEventLoop<Runnable> mainThreadExecutor, LightChunkGetter chunkGetter, ChunkGenerator generator, ChunkStatusUpdateListener chunkStatusListener, Supplier<DimensionDataStorage> overworldDataStorage, TicketStorage ticketStorage, int serverViewDistance, boolean syncWrites) {
|
|
super(new RegionStorageInfo(levelStorage.getLevelId(), level.dimension(), "chunk"), levelStorage.getDimensionPath(level.dimension()).resolve("region"), dataFixer, syncWrites, DataFixTypes.CHUNK, LegacyStructureDataHandler.getLegacyTagFixer(level.dimension(), overworldDataStorage, dataFixer));
|
|
Path storageFolder = levelStorage.getDimensionPath(level.dimension());
|
|
this.storageName = storageFolder.getFileName().toString();
|
|
this.level = level;
|
|
RegistryAccess registryAccess = level.registryAccess();
|
|
long levelSeed = level.getSeed();
|
|
if (generator instanceof NoiseBasedChunkGenerator) {
|
|
NoiseBasedChunkGenerator noiseGenerator = (NoiseBasedChunkGenerator)generator;
|
|
this.randomState = RandomState.create(noiseGenerator.generatorSettings().value(), registryAccess.lookupOrThrow(Registries.NOISE), levelSeed);
|
|
} else {
|
|
this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), registryAccess.lookupOrThrow(Registries.NOISE), levelSeed);
|
|
}
|
|
this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, levelSeed);
|
|
this.mainThreadExecutor = mainThreadExecutor;
|
|
ConsecutiveExecutor worldgen = new ConsecutiveExecutor(executor, "worldgen");
|
|
this.chunkStatusListener = chunkStatusListener;
|
|
ConsecutiveExecutor light = new ConsecutiveExecutor(executor, "light");
|
|
this.worldgenTaskDispatcher = new ChunkTaskDispatcher(worldgen, executor);
|
|
this.lightTaskDispatcher = new ChunkTaskDispatcher(light, executor);
|
|
this.lightEngine = new ThreadedLevelLightEngine(chunkGetter, this, this.level.dimensionType().hasSkyLight(), light, this.lightTaskDispatcher);
|
|
this.distanceManager = new DistanceManager(ticketStorage, executor, mainThreadExecutor);
|
|
this.ticketStorage = ticketStorage;
|
|
this.poiManager = new PoiManager(new RegionStorageInfo(levelStorage.getLevelId(), level.dimension(), "poi"), storageFolder.resolve("poi"), dataFixer, syncWrites, registryAccess, level.getServer(), level);
|
|
this.setServerViewDistance(serverViewDistance);
|
|
this.worldGenContext = new WorldGenContext(level, generator, structureManager, this.lightEngine, mainThreadExecutor, this::setChunkUnsaved);
|
|
}
|
|
|
|
private void setChunkUnsaved(ChunkPos chunkPos) {
|
|
this.chunksToEagerlySave.add(chunkPos.toLong());
|
|
}
|
|
|
|
protected ChunkGenerator generator() {
|
|
return this.worldGenContext.generator();
|
|
}
|
|
|
|
protected ChunkGeneratorStructureState generatorState() {
|
|
return this.chunkGeneratorState;
|
|
}
|
|
|
|
protected RandomState randomState() {
|
|
return this.randomState;
|
|
}
|
|
|
|
public boolean isChunkTracked(ServerPlayer player, int chunkX, int chunkZ) {
|
|
return player.getChunkTrackingView().contains(chunkX, chunkZ) && !player.connection.chunkSender.isPending(ChunkPos.asLong(chunkX, chunkZ));
|
|
}
|
|
|
|
private boolean isChunkOnTrackedBorder(ServerPlayer player, int chunkX, int chunkZ) {
|
|
if (!this.isChunkTracked(player, chunkX, chunkZ)) {
|
|
return false;
|
|
}
|
|
for (int dx = -1; dx <= 1; ++dx) {
|
|
for (int dz = -1; dz <= 1; ++dz) {
|
|
if (dx == 0 && dz == 0 || this.isChunkTracked(player, chunkX + dx, chunkZ + dz)) continue;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
protected ThreadedLevelLightEngine getLightEngine() {
|
|
return this.lightEngine;
|
|
}
|
|
|
|
public @Nullable ChunkHolder getUpdatingChunkIfPresent(long key) {
|
|
return (ChunkHolder)this.updatingChunkMap.get(key);
|
|
}
|
|
|
|
protected @Nullable ChunkHolder getVisibleChunkIfPresent(long key) {
|
|
return (ChunkHolder)this.visibleChunkMap.get(key);
|
|
}
|
|
|
|
public @Nullable ChunkStatus getLatestStatus(long key) {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(key);
|
|
return chunkHolder != null ? chunkHolder.getLatestStatus() : null;
|
|
}
|
|
|
|
protected IntSupplier getChunkQueueLevel(long pos) {
|
|
return () -> {
|
|
ChunkHolder chunk = this.getVisibleChunkIfPresent(pos);
|
|
if (chunk == null) {
|
|
return ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1;
|
|
}
|
|
return Math.min(chunk.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
|
|
};
|
|
}
|
|
|
|
public String getChunkDebugData(ChunkPos pos) {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(pos.toLong());
|
|
if (chunkHolder == null) {
|
|
return "null";
|
|
}
|
|
String result = chunkHolder.getTicketLevel() + "\n";
|
|
ChunkStatus status = chunkHolder.getLatestStatus();
|
|
ChunkAccess chunk = chunkHolder.getLatestChunk();
|
|
if (status != null) {
|
|
result = result + "St: \u00a7" + status.getIndex() + String.valueOf(status) + "\u00a7r\n";
|
|
}
|
|
if (chunk != null) {
|
|
result = result + "Ch: \u00a7" + chunk.getPersistedStatus().getIndex() + String.valueOf(chunk.getPersistedStatus()) + "\u00a7r\n";
|
|
}
|
|
FullChunkStatus fullStatus = chunkHolder.getFullStatus();
|
|
result = result + String.valueOf('\u00a7') + fullStatus.ordinal() + String.valueOf((Object)fullStatus);
|
|
return result + "\u00a7r";
|
|
}
|
|
|
|
CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder centerChunk, int range, IntFunction<ChunkStatus> distanceToStatus) {
|
|
if (range == 0) {
|
|
ChunkStatus status = distanceToStatus.apply(0);
|
|
return centerChunk.scheduleChunkGenerationTask(status, this).thenApply(r -> r.map(List::of));
|
|
}
|
|
int chunkCount = Mth.square(range * 2 + 1);
|
|
ArrayList<CompletableFuture<ChunkResult<ChunkAccess>>> deps = new ArrayList<CompletableFuture<ChunkResult<ChunkAccess>>>(chunkCount);
|
|
ChunkPos centerPos = centerChunk.getPos();
|
|
for (int z = -range; z <= range; ++z) {
|
|
for (int x = -range; x <= range; ++x) {
|
|
int distance = Math.max(Math.abs(x), Math.abs(z));
|
|
long chunkNode = ChunkPos.asLong(centerPos.x + x, centerPos.z + z);
|
|
ChunkHolder chunk = this.getUpdatingChunkIfPresent(chunkNode);
|
|
if (chunk == null) {
|
|
return UNLOADED_CHUNK_LIST_FUTURE;
|
|
}
|
|
ChunkStatus depStatus = distanceToStatus.apply(distance);
|
|
deps.add(chunk.scheduleChunkGenerationTask(depStatus, this));
|
|
}
|
|
}
|
|
return Util.sequence(deps).thenApply(chunkResults -> {
|
|
ArrayList<ChunkAccess> chunks = new ArrayList<ChunkAccess>(chunkResults.size());
|
|
for (ChunkResult chunkResult : chunkResults) {
|
|
if (chunkResult == null) {
|
|
throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
|
|
}
|
|
ChunkAccess chunk = chunkResult.orElse(null);
|
|
if (chunk == null) {
|
|
return UNLOADED_CHUNK_LIST_RESULT;
|
|
}
|
|
chunks.add(chunk);
|
|
}
|
|
return ChunkResult.of(chunks);
|
|
});
|
|
}
|
|
|
|
public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
|
|
StringBuilder sb = new StringBuilder();
|
|
Consumer<ChunkHolder> addToDebug = holder -> holder.getAllFutures().forEach(pair -> {
|
|
ChunkStatus status = (ChunkStatus)pair.getFirst();
|
|
CompletableFuture future = (CompletableFuture)pair.getSecond();
|
|
if (future != null && future.isDone() && future.join() == null) {
|
|
sb.append(holder.getPos()).append(" - status: ").append(status).append(" future: ").append(future).append(System.lineSeparator());
|
|
}
|
|
});
|
|
sb.append("Updating:").append(System.lineSeparator());
|
|
this.updatingChunkMap.values().forEach(addToDebug);
|
|
sb.append("Visible:").append(System.lineSeparator());
|
|
this.visibleChunkMap.values().forEach(addToDebug);
|
|
CrashReport report = CrashReport.forThrowable(exception, "Chunk loading");
|
|
CrashReportCategory category = report.addCategory("Chunk loading");
|
|
category.setDetail("Details", details);
|
|
category.setDetail("Futures", sb);
|
|
return new ReportedException(report);
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder chunk) {
|
|
return this.getChunkRangeFuture(chunk, 2, distance -> ChunkStatus.FULL).thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2)));
|
|
}
|
|
|
|
private @Nullable ChunkHolder updateChunkScheduling(long node, int level, @Nullable ChunkHolder chunk, int oldLevel) {
|
|
if (!ChunkLevel.isLoaded(oldLevel) && !ChunkLevel.isLoaded(level)) {
|
|
return chunk;
|
|
}
|
|
if (chunk != null) {
|
|
chunk.setTicketLevel(level);
|
|
}
|
|
if (chunk != null) {
|
|
if (!ChunkLevel.isLoaded(level)) {
|
|
this.toDrop.add(node);
|
|
} else {
|
|
this.toDrop.remove(node);
|
|
}
|
|
}
|
|
if (ChunkLevel.isLoaded(level) && chunk == null) {
|
|
chunk = (ChunkHolder)this.pendingUnloads.remove(node);
|
|
if (chunk != null) {
|
|
chunk.setTicketLevel(level);
|
|
} else {
|
|
chunk = new ChunkHolder(new ChunkPos(node), level, this.level, this.lightEngine, this::onLevelChange, this);
|
|
}
|
|
this.updatingChunkMap.put(node, (Object)chunk);
|
|
this.modified = true;
|
|
}
|
|
return chunk;
|
|
}
|
|
|
|
private void onLevelChange(ChunkPos pos, IntSupplier oldLevel, int newLevel, IntConsumer setQueueLevel) {
|
|
this.worldgenTaskDispatcher.onLevelChange(pos, oldLevel, newLevel, setQueueLevel);
|
|
this.lightTaskDispatcher.onLevelChange(pos, oldLevel, newLevel, setQueueLevel);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
try {
|
|
this.worldgenTaskDispatcher.close();
|
|
this.lightTaskDispatcher.close();
|
|
this.poiManager.close();
|
|
}
|
|
finally {
|
|
super.close();
|
|
}
|
|
}
|
|
|
|
protected void saveAllChunks(boolean flushStorage) {
|
|
if (flushStorage) {
|
|
List<ChunkHolder> chunksToSave = this.visibleChunkMap.values().stream().filter(ChunkHolder::wasAccessibleSinceLastSave).peek(ChunkHolder::refreshAccessibility).toList();
|
|
MutableBoolean didWork = new MutableBoolean();
|
|
do {
|
|
didWork.setFalse();
|
|
chunksToSave.stream().map(chunk -> {
|
|
this.mainThreadExecutor.managedBlock(chunk::isReadyForSaving);
|
|
return chunk.getLatestChunk();
|
|
}).filter(chunkAccess -> chunkAccess instanceof ImposterProtoChunk || chunkAccess instanceof LevelChunk).filter(this::save).forEach(c -> didWork.setTrue());
|
|
} while (didWork.isTrue());
|
|
this.poiManager.flushAll();
|
|
this.processUnloads(() -> true);
|
|
this.synchronize(true).join();
|
|
} else {
|
|
this.nextChunkSaveTime.clear();
|
|
long now = Util.getMillis();
|
|
for (ChunkHolder chunk2 : this.visibleChunkMap.values()) {
|
|
this.saveChunkIfNeeded(chunk2, now);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void tick(BooleanSupplier haveTime) {
|
|
ProfilerFiller profiler = Profiler.get();
|
|
profiler.push("poi");
|
|
this.poiManager.tick(haveTime);
|
|
profiler.popPush("chunk_unload");
|
|
if (!this.level.noSave()) {
|
|
this.processUnloads(haveTime);
|
|
}
|
|
profiler.pop();
|
|
}
|
|
|
|
public boolean hasWork() {
|
|
return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.worldgenTaskDispatcher.hasWork() || this.lightTaskDispatcher.hasWork() || this.distanceManager.hasTickets();
|
|
}
|
|
|
|
private void processUnloads(BooleanSupplier haveTime) {
|
|
Runnable unloadTask;
|
|
LongIterator iterator = this.toDrop.iterator();
|
|
while (iterator.hasNext()) {
|
|
long pos = iterator.nextLong();
|
|
ChunkHolder chunkHolder = (ChunkHolder)this.updatingChunkMap.get(pos);
|
|
if (chunkHolder != null) {
|
|
this.updatingChunkMap.remove(pos);
|
|
this.pendingUnloads.put(pos, (Object)chunkHolder);
|
|
this.modified = true;
|
|
this.scheduleUnload(pos, chunkHolder);
|
|
}
|
|
iterator.remove();
|
|
}
|
|
for (int minimalNumberOfChunksToProcess = Math.max(0, this.unloadQueue.size() - 2000); (minimalNumberOfChunksToProcess > 0 || haveTime.getAsBoolean()) && (unloadTask = this.unloadQueue.poll()) != null; --minimalNumberOfChunksToProcess) {
|
|
unloadTask.run();
|
|
}
|
|
this.saveChunksEagerly(haveTime);
|
|
}
|
|
|
|
private void saveChunksEagerly(BooleanSupplier haveTime) {
|
|
long now = Util.getMillis();
|
|
int eagerlySavedCount = 0;
|
|
LongIterator iterator = this.chunksToEagerlySave.iterator();
|
|
while (eagerlySavedCount < 20 && this.activeChunkWrites.get() < 128 && haveTime.getAsBoolean() && iterator.hasNext()) {
|
|
ChunkAccess latestChunk;
|
|
long chunkPos = iterator.nextLong();
|
|
ChunkHolder chunkHolder = (ChunkHolder)this.visibleChunkMap.get(chunkPos);
|
|
ChunkAccess chunkAccess = latestChunk = chunkHolder != null ? chunkHolder.getLatestChunk() : null;
|
|
if (latestChunk == null || !latestChunk.isUnsaved()) {
|
|
iterator.remove();
|
|
continue;
|
|
}
|
|
if (!this.saveChunkIfNeeded(chunkHolder, now)) continue;
|
|
++eagerlySavedCount;
|
|
iterator.remove();
|
|
}
|
|
}
|
|
|
|
private void scheduleUnload(long pos, ChunkHolder chunkHolder) {
|
|
CompletableFuture<?> saveSyncFuture = chunkHolder.getSaveSyncFuture();
|
|
((CompletableFuture)saveSyncFuture.thenRunAsync(() -> {
|
|
CompletableFuture<?> currentFuture = chunkHolder.getSaveSyncFuture();
|
|
if (currentFuture != saveSyncFuture) {
|
|
this.scheduleUnload(pos, chunkHolder);
|
|
return;
|
|
}
|
|
ChunkAccess chunk = chunkHolder.getLatestChunk();
|
|
if (this.pendingUnloads.remove(pos, (Object)chunkHolder) && chunk != null) {
|
|
LevelChunk levelChunk;
|
|
if (chunk instanceof LevelChunk) {
|
|
levelChunk = (LevelChunk)chunk;
|
|
levelChunk.setLoaded(false);
|
|
}
|
|
this.save(chunk);
|
|
if (chunk instanceof LevelChunk) {
|
|
levelChunk = (LevelChunk)chunk;
|
|
this.level.unload(levelChunk);
|
|
}
|
|
this.lightEngine.updateChunkStatus(chunk.getPos());
|
|
this.lightEngine.tryScheduleUpdate();
|
|
this.nextChunkSaveTime.remove(chunk.getPos().toLong());
|
|
}
|
|
}, this.unloadQueue::add)).whenComplete((ignored, throwable) -> {
|
|
if (throwable != null) {
|
|
LOGGER.error("Failed to save chunk {}", (Object)chunkHolder.getPos(), throwable);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected boolean promoteChunkMap() {
|
|
if (!this.modified) {
|
|
return false;
|
|
}
|
|
this.visibleChunkMap = this.updatingChunkMap.clone();
|
|
this.modified = false;
|
|
return true;
|
|
}
|
|
|
|
private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos pos) {
|
|
CompletionStage chunkDataFuture = this.readChunk(pos).thenApplyAsync(chunkData -> chunkData.map(tag -> {
|
|
SerializableChunkData parsedData = SerializableChunkData.parse(this.level, this.level.palettedContainerFactory(), tag);
|
|
if (parsedData == null) {
|
|
LOGGER.error("Chunk file at {} is missing level data, skipping", (Object)pos);
|
|
}
|
|
return parsedData;
|
|
}), Util.backgroundExecutor().forName("parseChunk"));
|
|
CompletableFuture<?> poiFuture = this.poiManager.prefetch(pos);
|
|
return ((CompletableFuture)((CompletableFuture)((CompletableFuture)chunkDataFuture).thenCombine(poiFuture, (chunkData, ignored) -> chunkData)).thenApplyAsync(chunkData -> {
|
|
Profiler.get().incrementCounter("chunkLoad");
|
|
if (chunkData.isPresent()) {
|
|
ProtoChunk chunk = ((SerializableChunkData)chunkData.get()).read(this.level, this.poiManager, this.storageInfo(), pos);
|
|
this.markPosition(pos, ((ChunkAccess)chunk).getPersistedStatus().getChunkType());
|
|
return chunk;
|
|
}
|
|
return this.createEmptyChunk(pos);
|
|
}, (Executor)this.mainThreadExecutor)).exceptionallyAsync(throwable -> this.handleChunkLoadFailure((Throwable)throwable, pos), (Executor)this.mainThreadExecutor);
|
|
}
|
|
|
|
private ChunkAccess handleChunkLoadFailure(Throwable throwable, ChunkPos pos) {
|
|
boolean ioException;
|
|
Throwable throwable2;
|
|
Throwable unwrapped;
|
|
if (throwable instanceof CompletionException) {
|
|
CompletionException e = (CompletionException)throwable;
|
|
v0 = e.getCause();
|
|
} else {
|
|
v0 = unwrapped = throwable;
|
|
}
|
|
if (unwrapped instanceof ReportedException) {
|
|
ReportedException e = (ReportedException)unwrapped;
|
|
throwable2 = e.getCause();
|
|
} else {
|
|
throwable2 = unwrapped;
|
|
}
|
|
Throwable cause = throwable2;
|
|
boolean alwaysThrow = cause instanceof Error;
|
|
boolean bl = ioException = cause instanceof IOException || cause instanceof NbtException;
|
|
if (alwaysThrow || !ioException) {
|
|
CrashReport report = CrashReport.forThrowable(throwable, "Exception loading chunk");
|
|
CrashReportCategory chunkBeingLoaded = report.addCategory("Chunk being loaded");
|
|
chunkBeingLoaded.setDetail("pos", pos);
|
|
this.markPositionReplaceable(pos);
|
|
throw new ReportedException(report);
|
|
}
|
|
this.level.getServer().reportChunkLoadFailure(cause, this.storageInfo(), pos);
|
|
return this.createEmptyChunk(pos);
|
|
}
|
|
|
|
private ChunkAccess createEmptyChunk(ChunkPos pos) {
|
|
this.markPositionReplaceable(pos);
|
|
return new ProtoChunk(pos, UpgradeData.EMPTY, this.level, this.level.palettedContainerFactory(), null);
|
|
}
|
|
|
|
private void markPositionReplaceable(ChunkPos pos) {
|
|
this.chunkTypeCache.put(pos.toLong(), (byte)-1);
|
|
}
|
|
|
|
private byte markPosition(ChunkPos pos, ChunkType type) {
|
|
return this.chunkTypeCache.put(pos.toLong(), type == ChunkType.PROTOCHUNK ? (byte)-1 : 1);
|
|
}
|
|
|
|
@Override
|
|
public GenerationChunkHolder acquireGeneration(long chunkNode) {
|
|
ChunkHolder chunkHolder = (ChunkHolder)this.updatingChunkMap.get(chunkNode);
|
|
chunkHolder.increaseGenerationRefCount();
|
|
return chunkHolder;
|
|
}
|
|
|
|
@Override
|
|
public void releaseGeneration(GenerationChunkHolder chunkHolder) {
|
|
chunkHolder.decreaseGenerationRefCount();
|
|
}
|
|
|
|
@Override
|
|
public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunkHolder, ChunkStep step, StaticCache2D<GenerationChunkHolder> cache) {
|
|
ChunkPos pos = chunkHolder.getPos();
|
|
if (step.targetStatus() == ChunkStatus.EMPTY) {
|
|
return this.scheduleChunkLoad(pos);
|
|
}
|
|
try {
|
|
GenerationChunkHolder holder = cache.get(pos.x, pos.z);
|
|
ChunkAccess centerChunk = holder.getChunkIfPresentUnchecked(step.targetStatus().getParent());
|
|
if (centerChunk == null) {
|
|
throw new IllegalStateException("Parent chunk missing");
|
|
}
|
|
return step.apply(this.worldGenContext, cache, centerChunk);
|
|
}
|
|
catch (Exception e) {
|
|
e.getStackTrace();
|
|
CrashReport report = CrashReport.forThrowable(e, "Exception generating new chunk");
|
|
CrashReportCategory category = report.addCategory("Chunk to be generated");
|
|
category.setDetail("Status being generated", () -> step.targetStatus().getName());
|
|
category.setDetail("Location", String.format(Locale.ROOT, "%d,%d", pos.x, pos.z));
|
|
category.setDetail("Position hash", ChunkPos.asLong(pos.x, pos.z));
|
|
category.setDetail("Generator", this.generator());
|
|
this.mainThreadExecutor.execute(() -> {
|
|
throw new ReportedException(report);
|
|
});
|
|
throw new ReportedException(report);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public ChunkGenerationTask scheduleGenerationTask(ChunkStatus targetStatus, ChunkPos pos) {
|
|
ChunkGenerationTask task = ChunkGenerationTask.create(this, targetStatus, pos);
|
|
this.pendingGenerationTasks.add(task);
|
|
return task;
|
|
}
|
|
|
|
private void runGenerationTask(ChunkGenerationTask task) {
|
|
GenerationChunkHolder chunk = task.getCenter();
|
|
this.worldgenTaskDispatcher.submit(() -> {
|
|
CompletableFuture<?> future = task.runUntilWait();
|
|
if (future == null) {
|
|
return;
|
|
}
|
|
future.thenRun(() -> this.runGenerationTask(task));
|
|
}, chunk.getPos().toLong(), chunk::getQueueLevel);
|
|
}
|
|
|
|
@Override
|
|
public void runGenerationTasks() {
|
|
this.pendingGenerationTasks.forEach(this::runGenerationTask);
|
|
this.pendingGenerationTasks.clear();
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder chunk) {
|
|
CompletableFuture<ChunkResult<List<ChunkAccess>>> future = this.getChunkRangeFuture(chunk, 1, distance -> ChunkStatus.FULL);
|
|
return future.thenApplyAsync(listResult -> listResult.map(list -> {
|
|
LevelChunk levelChunk = (LevelChunk)list.get(list.size() / 2);
|
|
levelChunk.postProcessGeneration(this.level);
|
|
this.level.startTickingChunk(levelChunk);
|
|
CompletableFuture<?> sendSyncFuture = chunk.getSendSyncFuture();
|
|
if (sendSyncFuture.isDone()) {
|
|
this.onChunkReadyToSend(chunk, levelChunk);
|
|
} else {
|
|
sendSyncFuture.thenAcceptAsync(ignored -> this.onChunkReadyToSend(chunk, levelChunk), (Executor)this.mainThreadExecutor);
|
|
}
|
|
return levelChunk;
|
|
}), (Executor)this.mainThreadExecutor);
|
|
}
|
|
|
|
private void onChunkReadyToSend(ChunkHolder chunkHolder, LevelChunk chunk) {
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
for (ServerPlayer player : this.playerMap.getAllPlayers()) {
|
|
if (!player.getChunkTrackingView().contains(chunkPos)) continue;
|
|
ChunkMap.markChunkPendingToSend(player, chunk);
|
|
}
|
|
this.level.getChunkSource().onChunkReadyToSend(chunkHolder);
|
|
this.level.debugSynchronizers().registerChunk(chunk);
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> prepareAccessibleChunk(ChunkHolder chunk) {
|
|
return this.getChunkRangeFuture(chunk, 1, ChunkLevel::getStatusAroundFullChunk).thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2)));
|
|
}
|
|
|
|
Stream<ChunkHolder> allChunksWithAtLeastStatus(ChunkStatus status) {
|
|
int level = ChunkLevel.byStatus(status);
|
|
return this.visibleChunkMap.values().stream().filter(chunk -> chunk.getTicketLevel() <= level);
|
|
}
|
|
|
|
private boolean saveChunkIfNeeded(ChunkHolder chunk, long now) {
|
|
if (!chunk.wasAccessibleSinceLastSave() || !chunk.isReadyForSaving()) {
|
|
return false;
|
|
}
|
|
ChunkAccess chunkAccess = chunk.getLatestChunk();
|
|
if (chunkAccess instanceof ImposterProtoChunk || chunkAccess instanceof LevelChunk) {
|
|
if (!chunkAccess.isUnsaved()) {
|
|
return false;
|
|
}
|
|
long chunkPos = chunkAccess.getPos().toLong();
|
|
long nextSaveTime = this.nextChunkSaveTime.getOrDefault(chunkPos, -1L);
|
|
if (now < nextSaveTime) {
|
|
return false;
|
|
}
|
|
boolean saved = this.save(chunkAccess);
|
|
chunk.refreshAccessibility();
|
|
if (saved) {
|
|
this.nextChunkSaveTime.put(chunkPos, now + 10000L);
|
|
}
|
|
return saved;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean save(ChunkAccess chunk) {
|
|
this.poiManager.flush(chunk.getPos());
|
|
if (!chunk.tryMarkSaved()) {
|
|
return false;
|
|
}
|
|
ChunkPos pos = chunk.getPos();
|
|
try {
|
|
ChunkStatus status = chunk.getPersistedStatus();
|
|
if (status.getChunkType() != ChunkType.LEVELCHUNK) {
|
|
if (this.isExistingChunkFull(pos)) {
|
|
return false;
|
|
}
|
|
if (status == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
|
|
return false;
|
|
}
|
|
}
|
|
Profiler.get().incrementCounter("chunkSave");
|
|
this.activeChunkWrites.incrementAndGet();
|
|
SerializableChunkData data = SerializableChunkData.copyOf(this.level, chunk);
|
|
CompletableFuture<CompoundTag> encodedData = CompletableFuture.supplyAsync(data::write, Util.backgroundExecutor());
|
|
this.write(pos, encodedData::join).handle((ignored, throwable) -> {
|
|
if (throwable != null) {
|
|
this.level.getServer().reportChunkSaveFailure((Throwable)throwable, this.storageInfo(), pos);
|
|
}
|
|
this.activeChunkWrites.decrementAndGet();
|
|
return null;
|
|
});
|
|
this.markPosition(pos, status.getChunkType());
|
|
return true;
|
|
}
|
|
catch (Exception e) {
|
|
this.level.getServer().reportChunkSaveFailure(e, this.storageInfo(), pos);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean isExistingChunkFull(ChunkPos pos) {
|
|
CompoundTag currentTag;
|
|
byte cachedChunkType = this.chunkTypeCache.get(pos.toLong());
|
|
if (cachedChunkType != 0) {
|
|
return cachedChunkType == 1;
|
|
}
|
|
try {
|
|
currentTag = this.readChunk(pos).join().orElse(null);
|
|
if (currentTag == null) {
|
|
this.markPositionReplaceable(pos);
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception e) {
|
|
LOGGER.error("Failed to read chunk {}", (Object)pos, (Object)e);
|
|
this.markPositionReplaceable(pos);
|
|
return false;
|
|
}
|
|
ChunkType chunkType = SerializableChunkData.getChunkStatusFromTag(currentTag).getChunkType();
|
|
return this.markPosition(pos, chunkType) == 1;
|
|
}
|
|
|
|
protected void setServerViewDistance(int newViewDistance) {
|
|
int actualNewDistance = Mth.clamp(newViewDistance, 2, 32);
|
|
if (actualNewDistance != this.serverViewDistance) {
|
|
this.serverViewDistance = actualNewDistance;
|
|
this.distanceManager.updatePlayerTickets(this.serverViewDistance);
|
|
for (ServerPlayer player : this.playerMap.getAllPlayers()) {
|
|
this.updateChunkTracking(player);
|
|
}
|
|
}
|
|
}
|
|
|
|
private int getPlayerViewDistance(ServerPlayer player) {
|
|
return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
|
|
}
|
|
|
|
private void markChunkPendingToSend(ServerPlayer player, ChunkPos pos) {
|
|
LevelChunk chunk = this.getChunkToSend(pos.toLong());
|
|
if (chunk != null) {
|
|
ChunkMap.markChunkPendingToSend(player, chunk);
|
|
}
|
|
}
|
|
|
|
private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
|
|
player.connection.chunkSender.markChunkPendingToSend(chunk);
|
|
}
|
|
|
|
private static void dropChunk(ServerPlayer player, ChunkPos pos) {
|
|
player.connection.chunkSender.dropChunk(player, pos);
|
|
}
|
|
|
|
public @Nullable LevelChunk getChunkToSend(long key) {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(key);
|
|
if (chunkHolder == null) {
|
|
return null;
|
|
}
|
|
return chunkHolder.getChunkToSend();
|
|
}
|
|
|
|
public int size() {
|
|
return this.visibleChunkMap.size();
|
|
}
|
|
|
|
public net.minecraft.server.level.DistanceManager getDistanceManager() {
|
|
return this.distanceManager;
|
|
}
|
|
|
|
void dumpChunks(Writer output) throws IOException {
|
|
CsvOutput csvOutput = CsvOutput.builder().addColumn("x").addColumn("z").addColumn("level").addColumn("in_memory").addColumn("status").addColumn("full_status").addColumn("accessible_ready").addColumn("ticking_ready").addColumn("entity_ticking_ready").addColumn("ticket").addColumn("spawning").addColumn("block_entity_count").addColumn("ticking_ticket").addColumn("ticking_level").addColumn("block_ticks").addColumn("fluid_ticks").build(output);
|
|
for (Long2ObjectMap.Entry entry : this.visibleChunkMap.long2ObjectEntrySet()) {
|
|
long posKey = entry.getLongKey();
|
|
ChunkPos pos = new ChunkPos(posKey);
|
|
ChunkHolder holder = (ChunkHolder)entry.getValue();
|
|
Optional<ChunkAccess> chunk = Optional.ofNullable(holder.getLatestChunk());
|
|
Optional<Object> fullChunk = chunk.flatMap(chunkAccess -> chunkAccess instanceof LevelChunk ? Optional.of((LevelChunk)chunkAccess) : Optional.empty());
|
|
csvOutput.writeRow(pos.x, pos.z, holder.getTicketLevel(), chunk.isPresent(), chunk.map(ChunkAccess::getPersistedStatus).orElse(null), fullChunk.map(LevelChunk::getFullStatus).orElse(null), ChunkMap.printFuture(holder.getFullChunkFuture()), ChunkMap.printFuture(holder.getTickingChunkFuture()), ChunkMap.printFuture(holder.getEntityTickingChunkFuture()), this.ticketStorage.getTicketDebugString(posKey, false), this.anyPlayerCloseEnoughForSpawning(pos), fullChunk.map(c -> c.getBlockEntities().size()).orElse(0), this.ticketStorage.getTicketDebugString(posKey, true), this.distanceManager.getChunkLevel(posKey, true), fullChunk.map(levelChunk -> levelChunk.getBlockTicks().count()).orElse(0), fullChunk.map(levelChunk -> levelChunk.getFluidTicks().count()).orElse(0));
|
|
}
|
|
}
|
|
|
|
private static String printFuture(CompletableFuture<ChunkResult<LevelChunk>> future) {
|
|
try {
|
|
ChunkResult result = future.getNow(null);
|
|
if (result != null) {
|
|
return result.isSuccess() ? "done" : "unloaded";
|
|
}
|
|
return "not completed";
|
|
}
|
|
catch (CompletionException e) {
|
|
return "failed " + e.getCause().getMessage();
|
|
}
|
|
catch (CancellationException e) {
|
|
return "cancelled";
|
|
}
|
|
}
|
|
|
|
private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos pos) {
|
|
return this.read(pos).thenApplyAsync(chunkTag -> chunkTag.map(this::upgradeChunkTag), Util.backgroundExecutor().forName("upgradeChunk"));
|
|
}
|
|
|
|
private CompoundTag upgradeChunkTag(CompoundTag tag) {
|
|
return this.upgradeChunkTag(tag, -1, ChunkMap.getChunkDataFixContextTag(this.level.dimension(), this.generator().getTypeNameForDataFixer()));
|
|
}
|
|
|
|
public static CompoundTag getChunkDataFixContextTag(ResourceKey<Level> dimension, Optional<ResourceKey<MapCodec<? extends ChunkGenerator>>> generator) {
|
|
CompoundTag contextTag = new CompoundTag();
|
|
contextTag.putString("dimension", dimension.identifier().toString());
|
|
generator.ifPresent(k -> contextTag.putString("generator", k.identifier().toString()));
|
|
return contextTag;
|
|
}
|
|
|
|
void collectSpawningChunks(List<LevelChunk> output) {
|
|
LongIterator spawnCandidateChunks = this.distanceManager.getSpawnCandidateChunks();
|
|
while (spawnCandidateChunks.hasNext()) {
|
|
LevelChunk chunk;
|
|
ChunkHolder holder = (ChunkHolder)this.visibleChunkMap.get(spawnCandidateChunks.nextLong());
|
|
if (holder == null || (chunk = holder.getTickingChunk()) == null || !this.anyPlayerCloseEnoughForSpawningInternal(holder.getPos())) continue;
|
|
output.add(chunk);
|
|
}
|
|
}
|
|
|
|
void forEachBlockTickingChunk(Consumer<LevelChunk> tickingChunkConsumer) {
|
|
this.distanceManager.forEachEntityTickingChunk(chunkPos -> {
|
|
ChunkHolder holder = (ChunkHolder)this.visibleChunkMap.get(chunkPos);
|
|
if (holder == null) {
|
|
return;
|
|
}
|
|
LevelChunk chunk = holder.getTickingChunk();
|
|
if (chunk == null) {
|
|
return;
|
|
}
|
|
tickingChunkConsumer.accept(chunk);
|
|
});
|
|
}
|
|
|
|
boolean anyPlayerCloseEnoughForSpawning(ChunkPos pos) {
|
|
TriState triState = this.distanceManager.hasPlayersNearby(pos.toLong());
|
|
if (triState == TriState.DEFAULT) {
|
|
return this.anyPlayerCloseEnoughForSpawningInternal(pos);
|
|
}
|
|
return triState.toBoolean(true);
|
|
}
|
|
|
|
boolean anyPlayerCloseEnoughTo(BlockPos pos, int maxDistance) {
|
|
Vec3 target = new Vec3(pos);
|
|
for (ServerPlayer player : this.playerMap.getAllPlayers()) {
|
|
if (!this.playerIsCloseEnoughTo(player, target, maxDistance)) continue;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos pos) {
|
|
for (ServerPlayer player : this.playerMap.getAllPlayers()) {
|
|
if (!this.playerIsCloseEnoughForSpawning(player, pos)) continue;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos pos) {
|
|
long key = pos.toLong();
|
|
if (!this.distanceManager.hasPlayersNearby(key).toBoolean(true)) {
|
|
return List.of();
|
|
}
|
|
ImmutableList.Builder builder = ImmutableList.builder();
|
|
for (ServerPlayer player : this.playerMap.getAllPlayers()) {
|
|
if (!this.playerIsCloseEnoughForSpawning(player, pos)) continue;
|
|
builder.add((Object)player);
|
|
}
|
|
return builder.build();
|
|
}
|
|
|
|
private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos pos) {
|
|
if (player.isSpectator()) {
|
|
return false;
|
|
}
|
|
double distanceToChunk = ChunkMap.euclideanDistanceSquared(pos, player.position());
|
|
return distanceToChunk < 16384.0;
|
|
}
|
|
|
|
private boolean playerIsCloseEnoughTo(ServerPlayer player, Vec3 pos, int maxDistance) {
|
|
if (player.isSpectator()) {
|
|
return false;
|
|
}
|
|
double distanceToPos = player.position().distanceTo(pos);
|
|
return distanceToPos < (double)maxDistance;
|
|
}
|
|
|
|
private static double euclideanDistanceSquared(ChunkPos chunkPos, Vec3 pos) {
|
|
double xPos = SectionPos.sectionToBlockCoord(chunkPos.x, 8);
|
|
double zPos = SectionPos.sectionToBlockCoord(chunkPos.z, 8);
|
|
double xd = xPos - pos.x;
|
|
double zd = zPos - pos.z;
|
|
return xd * xd + zd * zd;
|
|
}
|
|
|
|
private boolean skipPlayer(ServerPlayer player) {
|
|
return player.isSpectator() && this.level.getGameRules().get(GameRules.SPECTATORS_GENERATE_CHUNKS) == false;
|
|
}
|
|
|
|
void updatePlayerStatus(ServerPlayer player, boolean added) {
|
|
boolean ignored = this.skipPlayer(player);
|
|
boolean wasIgnored = this.playerMap.ignoredOrUnknown(player);
|
|
if (added) {
|
|
this.playerMap.addPlayer(player, ignored);
|
|
this.updatePlayerPos(player);
|
|
if (!ignored) {
|
|
this.distanceManager.addPlayer(SectionPos.of(player), player);
|
|
}
|
|
player.setChunkTrackingView(ChunkTrackingView.EMPTY);
|
|
this.updateChunkTracking(player);
|
|
} else {
|
|
SectionPos lastPos = player.getLastSectionPos();
|
|
this.playerMap.removePlayer(player);
|
|
if (!wasIgnored) {
|
|
this.distanceManager.removePlayer(lastPos, player);
|
|
}
|
|
this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
|
|
}
|
|
}
|
|
|
|
private void updatePlayerPos(ServerPlayer player) {
|
|
SectionPos pos = SectionPos.of(player);
|
|
player.setLastSectionPos(pos);
|
|
}
|
|
|
|
public void move(ServerPlayer player) {
|
|
boolean positionChanged;
|
|
for (TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
if (trackedEntity.entity == player) {
|
|
trackedEntity.updatePlayers(this.level.players());
|
|
continue;
|
|
}
|
|
trackedEntity.updatePlayer(player);
|
|
}
|
|
SectionPos oldSection = player.getLastSectionPos();
|
|
SectionPos newSection = SectionPos.of(player);
|
|
boolean wasIgnored = this.playerMap.ignored(player);
|
|
boolean ignored = this.skipPlayer(player);
|
|
boolean bl = positionChanged = oldSection.asLong() != newSection.asLong();
|
|
if (positionChanged || wasIgnored != ignored) {
|
|
this.updatePlayerPos(player);
|
|
if (!wasIgnored) {
|
|
this.distanceManager.removePlayer(oldSection, player);
|
|
}
|
|
if (!ignored) {
|
|
this.distanceManager.addPlayer(newSection, player);
|
|
}
|
|
if (!wasIgnored && ignored) {
|
|
this.playerMap.ignorePlayer(player);
|
|
}
|
|
if (wasIgnored && !ignored) {
|
|
this.playerMap.unIgnorePlayer(player);
|
|
}
|
|
this.updateChunkTracking(player);
|
|
}
|
|
}
|
|
|
|
private void updateChunkTracking(ServerPlayer player) {
|
|
ChunkTrackingView.Positioned view;
|
|
ChunkPos chunkPos = player.chunkPosition();
|
|
int playerViewDistance = this.getPlayerViewDistance(player);
|
|
ChunkTrackingView chunkTrackingView = player.getChunkTrackingView();
|
|
if (chunkTrackingView instanceof ChunkTrackingView.Positioned && (view = (ChunkTrackingView.Positioned)chunkTrackingView).center().equals(chunkPos) && view.viewDistance() == playerViewDistance) {
|
|
return;
|
|
}
|
|
this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkPos, playerViewDistance));
|
|
}
|
|
|
|
private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView next) {
|
|
if (player.level() != this.level) {
|
|
return;
|
|
}
|
|
ChunkTrackingView previous = player.getChunkTrackingView();
|
|
if (next instanceof ChunkTrackingView.Positioned) {
|
|
ChunkTrackingView.Positioned from;
|
|
ChunkTrackingView.Positioned to = (ChunkTrackingView.Positioned)next;
|
|
if (!(previous instanceof ChunkTrackingView.Positioned) || !(from = (ChunkTrackingView.Positioned)previous).center().equals(to.center())) {
|
|
player.connection.send(new ClientboundSetChunkCacheCenterPacket(to.center().x, to.center().z));
|
|
}
|
|
}
|
|
ChunkTrackingView.difference(previous, next, pos -> this.markChunkPendingToSend(player, (ChunkPos)pos), pos -> ChunkMap.dropChunk(player, pos));
|
|
player.setChunkTrackingView(next);
|
|
}
|
|
|
|
@Override
|
|
public List<ServerPlayer> getPlayers(ChunkPos pos, boolean borderOnly) {
|
|
Set<ServerPlayer> allPlayers = this.playerMap.getAllPlayers();
|
|
ImmutableList.Builder result = ImmutableList.builder();
|
|
for (ServerPlayer player : allPlayers) {
|
|
if ((!borderOnly || !this.isChunkOnTrackedBorder(player, pos.x, pos.z)) && (borderOnly || !this.isChunkTracked(player, pos.x, pos.z))) continue;
|
|
result.add((Object)player);
|
|
}
|
|
return result.build();
|
|
}
|
|
|
|
protected void addEntity(Entity entity) {
|
|
if (entity instanceof EnderDragonPart) {
|
|
return;
|
|
}
|
|
EntityType<?> type = entity.getType();
|
|
int range = type.clientTrackingRange() * 16;
|
|
if (range == 0) {
|
|
return;
|
|
}
|
|
int updateInterval = type.updateInterval();
|
|
if (this.entityMap.containsKey(entity.getId())) {
|
|
throw Util.pauseInIde(new IllegalStateException("Entity is already tracked!"));
|
|
}
|
|
TrackedEntity trackedEntity = new TrackedEntity(entity, range, updateInterval, type.trackDeltas());
|
|
this.entityMap.put(entity.getId(), (Object)trackedEntity);
|
|
trackedEntity.updatePlayers(this.level.players());
|
|
if (entity instanceof ServerPlayer) {
|
|
ServerPlayer player = (ServerPlayer)entity;
|
|
this.updatePlayerStatus(player, true);
|
|
for (TrackedEntity e : this.entityMap.values()) {
|
|
if (e.entity == player) continue;
|
|
e.updatePlayer(player);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void removeEntity(Entity entity) {
|
|
TrackedEntity trackedEntity;
|
|
if (entity instanceof ServerPlayer) {
|
|
ServerPlayer player = (ServerPlayer)entity;
|
|
this.updatePlayerStatus(player, false);
|
|
for (TrackedEntity trackedEntity2 : this.entityMap.values()) {
|
|
trackedEntity2.removePlayer(player);
|
|
}
|
|
}
|
|
if ((trackedEntity = (TrackedEntity)this.entityMap.remove(entity.getId())) != null) {
|
|
trackedEntity.broadcastRemoved();
|
|
}
|
|
}
|
|
|
|
protected void tick() {
|
|
for (ServerPlayer player : this.playerMap.getAllPlayers()) {
|
|
this.updateChunkTracking(player);
|
|
}
|
|
ArrayList movedPlayers = Lists.newArrayList();
|
|
List<ServerPlayer> players = this.level.players();
|
|
for (TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
boolean sectionPosChanged;
|
|
SectionPos oldPos = trackedEntity.lastSectionPos;
|
|
SectionPos newPos = SectionPos.of(trackedEntity.entity);
|
|
boolean bl = sectionPosChanged = !Objects.equals(oldPos, newPos);
|
|
if (sectionPosChanged) {
|
|
trackedEntity.updatePlayers(players);
|
|
Entity entity = trackedEntity.entity;
|
|
if (entity instanceof ServerPlayer) {
|
|
movedPlayers.add((ServerPlayer)entity);
|
|
}
|
|
trackedEntity.lastSectionPos = newPos;
|
|
}
|
|
if (!sectionPosChanged && !trackedEntity.entity.needsSync && !this.distanceManager.inEntityTickingRange(newPos.chunk().toLong())) continue;
|
|
trackedEntity.serverEntity.sendChanges();
|
|
}
|
|
if (!movedPlayers.isEmpty()) {
|
|
for (TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
trackedEntity.updatePlayers(movedPlayers);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void sendToTrackingPlayers(Entity entity, Packet<? super ClientGamePacketListener> packet) {
|
|
TrackedEntity trackedEntity = (TrackedEntity)this.entityMap.get(entity.getId());
|
|
if (trackedEntity != null) {
|
|
trackedEntity.sendToTrackingPlayers(packet);
|
|
}
|
|
}
|
|
|
|
public void sendToTrackingPlayersFiltered(Entity entity, Packet<? super ClientGamePacketListener> packet, Predicate<ServerPlayer> targetPredicate) {
|
|
TrackedEntity trackedEntity = (TrackedEntity)this.entityMap.get(entity.getId());
|
|
if (trackedEntity != null) {
|
|
trackedEntity.sendToTrackingPlayersFiltered(packet, targetPredicate);
|
|
}
|
|
}
|
|
|
|
protected void sendToTrackingPlayersAndSelf(Entity entity, Packet<? super ClientGamePacketListener> packet) {
|
|
TrackedEntity trackedEntity = (TrackedEntity)this.entityMap.get(entity.getId());
|
|
if (trackedEntity != null) {
|
|
trackedEntity.sendToTrackingPlayersAndSelf(packet);
|
|
}
|
|
}
|
|
|
|
public boolean isTrackedByAnyPlayer(Entity entity) {
|
|
TrackedEntity trackedEntity = (TrackedEntity)this.entityMap.get(entity.getId());
|
|
if (trackedEntity != null) {
|
|
return !trackedEntity.seenBy.isEmpty();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void forEachEntityTrackedBy(ServerPlayer player, Consumer<Entity> consumer) {
|
|
for (TrackedEntity entity : this.entityMap.values()) {
|
|
if (!entity.seenBy.contains(player.connection)) continue;
|
|
consumer.accept(entity.entity);
|
|
}
|
|
}
|
|
|
|
public void resendBiomesForChunks(List<ChunkAccess> chunks) {
|
|
HashMap<ServerPlayer, List> chunksForPlayers = new HashMap<ServerPlayer, List>();
|
|
for (ChunkAccess chunkAccess : chunks) {
|
|
LevelChunk levelChunk;
|
|
ChunkPos pos = chunkAccess.getPos();
|
|
LevelChunk chunk = chunkAccess instanceof LevelChunk ? (levelChunk = (LevelChunk)chunkAccess) : this.level.getChunk(pos.x, pos.z);
|
|
for (ServerPlayer player2 : this.getPlayers(pos, false)) {
|
|
chunksForPlayers.computeIfAbsent(player2, p -> new ArrayList()).add(chunk);
|
|
}
|
|
}
|
|
chunksForPlayers.forEach((player, chunkList) -> player.connection.send(ClientboundChunksBiomesPacket.forChunks(chunkList)));
|
|
}
|
|
|
|
protected PoiManager getPoiManager() {
|
|
return this.poiManager;
|
|
}
|
|
|
|
public String getStorageName() {
|
|
return this.storageName;
|
|
}
|
|
|
|
void onFullChunkStatusChange(ChunkPos pos, FullChunkStatus status) {
|
|
this.chunkStatusListener.onChunkStatusChange(pos, status);
|
|
}
|
|
|
|
public void waitForLightBeforeSending(ChunkPos centerChunk, int chunkRadius) {
|
|
int affectedLightChunkRadius = chunkRadius + 1;
|
|
ChunkPos.rangeClosed(centerChunk, affectedLightChunkRadius).forEach(chunkPos -> {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(chunkPos.toLong());
|
|
if (chunkHolder != null) {
|
|
chunkHolder.addSendDependency(this.lightEngine.waitForPendingTasks(chunkPos.x, chunkPos.z));
|
|
}
|
|
});
|
|
}
|
|
|
|
public void forEachReadyToSendChunk(Consumer<LevelChunk> consumer) {
|
|
for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) {
|
|
LevelChunk chunk = chunkHolder.getChunkToSend();
|
|
if (chunk == null) continue;
|
|
consumer.accept(chunk);
|
|
}
|
|
}
|
|
|
|
private class DistanceManager
|
|
extends net.minecraft.server.level.DistanceManager {
|
|
protected DistanceManager(TicketStorage ticketStorage, Executor executor, Executor mainThreadExecutor) {
|
|
super(ticketStorage, executor, mainThreadExecutor);
|
|
}
|
|
|
|
@Override
|
|
protected boolean isChunkToRemove(long node) {
|
|
return ChunkMap.this.toDrop.contains(node);
|
|
}
|
|
|
|
@Override
|
|
protected @Nullable ChunkHolder getChunk(long node) {
|
|
return ChunkMap.this.getUpdatingChunkIfPresent(node);
|
|
}
|
|
|
|
@Override
|
|
protected @Nullable ChunkHolder updateChunkScheduling(long node, int level, @Nullable ChunkHolder chunk, int oldLevel) {
|
|
return ChunkMap.this.updateChunkScheduling(node, level, chunk, oldLevel);
|
|
}
|
|
}
|
|
|
|
private class TrackedEntity
|
|
implements ServerEntity.Synchronizer {
|
|
private final ServerEntity serverEntity;
|
|
private final Entity entity;
|
|
private final int range;
|
|
private SectionPos lastSectionPos;
|
|
private final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
|
|
|
|
public TrackedEntity(Entity entity, int range, int updateInterval, boolean trackDelta) {
|
|
this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this);
|
|
this.entity = entity;
|
|
this.range = range;
|
|
this.lastSectionPos = SectionPos.of(entity);
|
|
}
|
|
|
|
public boolean equals(Object obj) {
|
|
if (obj instanceof TrackedEntity) {
|
|
return ((TrackedEntity)obj).entity.getId() == this.entity.getId();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public int hashCode() {
|
|
return this.entity.getId();
|
|
}
|
|
|
|
@Override
|
|
public void sendToTrackingPlayers(Packet<? super ClientGamePacketListener> packet) {
|
|
for (ServerPlayerConnection connection : this.seenBy) {
|
|
connection.send(packet);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void sendToTrackingPlayersAndSelf(Packet<? super ClientGamePacketListener> packet) {
|
|
this.sendToTrackingPlayers(packet);
|
|
Entity entity = this.entity;
|
|
if (entity instanceof ServerPlayer) {
|
|
ServerPlayer player = (ServerPlayer)entity;
|
|
player.connection.send(packet);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void sendToTrackingPlayersFiltered(Packet<? super ClientGamePacketListener> packet, Predicate<ServerPlayer> targetPredicate) {
|
|
for (ServerPlayerConnection connection : this.seenBy) {
|
|
if (!targetPredicate.test(connection.getPlayer())) continue;
|
|
connection.send(packet);
|
|
}
|
|
}
|
|
|
|
public void broadcastRemoved() {
|
|
for (ServerPlayerConnection connection : this.seenBy) {
|
|
this.serverEntity.removePairing(connection.getPlayer());
|
|
}
|
|
}
|
|
|
|
public void removePlayer(ServerPlayer player) {
|
|
if (this.seenBy.remove(player.connection)) {
|
|
this.serverEntity.removePairing(player);
|
|
if (this.seenBy.isEmpty()) {
|
|
ChunkMap.this.level.debugSynchronizers().dropEntity(this.entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void updatePlayer(ServerPlayer player) {
|
|
boolean visibleToPlayer;
|
|
if (player == this.entity) {
|
|
return;
|
|
}
|
|
Vec3 deltaToPlayer = player.position().subtract(this.entity.position());
|
|
int playerViewDistance = ChunkMap.this.getPlayerViewDistance(player);
|
|
double distanceSquared = deltaToPlayer.x * deltaToPlayer.x + deltaToPlayer.z * deltaToPlayer.z;
|
|
double visibleRange = Math.min(this.getEffectiveRange(), playerViewDistance * 16);
|
|
double rangeSquared = visibleRange * visibleRange;
|
|
boolean bl = visibleToPlayer = distanceSquared <= rangeSquared && this.entity.broadcastToPlayer(player) && ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
|
|
if (visibleToPlayer) {
|
|
if (this.seenBy.add(player.connection)) {
|
|
this.serverEntity.addPairing(player);
|
|
if (this.seenBy.size() == 1) {
|
|
ChunkMap.this.level.debugSynchronizers().registerEntity(this.entity);
|
|
}
|
|
ChunkMap.this.level.debugSynchronizers().startTrackingEntity(player, this.entity);
|
|
}
|
|
} else {
|
|
this.removePlayer(player);
|
|
}
|
|
}
|
|
|
|
private int scaledRange(int range) {
|
|
return ChunkMap.this.level.getServer().getScaledTrackingDistance(range);
|
|
}
|
|
|
|
private int getEffectiveRange() {
|
|
int effectiveRange = this.range;
|
|
for (Entity passenger : this.entity.getIndirectPassengers()) {
|
|
int passengerRange = passenger.getType().clientTrackingRange() * 16;
|
|
if (passengerRange <= effectiveRange) continue;
|
|
effectiveRange = passengerRange;
|
|
}
|
|
return this.scaledRange(effectiveRange);
|
|
}
|
|
|
|
public void updatePlayers(List<ServerPlayer> players) {
|
|
for (ServerPlayer player : players) {
|
|
this.updatePlayer(player);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|