1391 lines
50 KiB
Java
1391 lines
50 KiB
Java
package net.minecraft.server.level;
|
|
|
|
import com.google.common.collect.ImmutableList;
|
|
import com.google.common.collect.Iterables;
|
|
import com.google.common.collect.Lists;
|
|
import com.google.common.collect.Queues;
|
|
import com.google.common.collect.Sets;
|
|
import com.google.common.collect.ImmutableList.Builder;
|
|
import com.mojang.datafixers.DataFixer;
|
|
import com.mojang.logging.LogUtils;
|
|
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.LongIterator;
|
|
import it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet;
|
|
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
|
import it.unimi.dsi.fastutil.longs.LongSet;
|
|
import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
|
|
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.Map;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
import java.util.Queue;
|
|
import java.util.Set;
|
|
import java.util.UUID;
|
|
import java.util.concurrent.CancellationException;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.CompletionException;
|
|
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.Supplier;
|
|
import net.minecraft.CrashReport;
|
|
import net.minecraft.CrashReportCategory;
|
|
import net.minecraft.CrashReportDetail;
|
|
import net.minecraft.ReportedException;
|
|
import net.minecraft.Util;
|
|
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.ClientboundChunksBiomesPacket;
|
|
import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket;
|
|
import net.minecraft.server.level.ChunkHolder.PlayerProvider;
|
|
import net.minecraft.server.level.ChunkTrackingView.Positioned;
|
|
import net.minecraft.server.level.progress.ChunkProgressListener;
|
|
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.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.GameRules;
|
|
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.ChunkStorage;
|
|
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
|
import net.minecraft.world.level.chunk.storage.SerializableChunkData;
|
|
import net.minecraft.world.level.entity.ChunkStatusUpdateListener;
|
|
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.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.jetbrains.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public class ChunkMap extends ChunkStorage implements 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);
|
|
/**
|
|
* Chunks in memory. This should only ever be manipulated by the main thread.
|
|
*/
|
|
private final Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap = new Long2ObjectLinkedOpenHashMap<>();
|
|
/**
|
|
* Same as {@link #loadedChunks}, but immutable for access from other threads. <em>This should never be mutated.</em>
|
|
*/
|
|
private volatile Long2ObjectLinkedOpenHashMap<ChunkHolder> visibleChunkMap = this.updatingChunkMap.clone();
|
|
private final Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads = new Long2ObjectLinkedOpenHashMap<>();
|
|
private final List<ChunkGenerationTask> pendingGenerationTasks = new ArrayList();
|
|
final ServerLevel level;
|
|
private final ThreadedLevelLightEngine lightEngine;
|
|
private final BlockableEventLoop<Runnable> mainThreadExecutor;
|
|
private final RandomState randomState;
|
|
private final ChunkGeneratorStructureState chunkGeneratorState;
|
|
private final Supplier<DimensionDataStorage> overworldDataStorage;
|
|
private final TicketStorage ticketStorage;
|
|
private final PoiManager poiManager;
|
|
/**
|
|
* Chunks that have been requested to be unloaded, but haven't been unloaded yet.
|
|
*/
|
|
final LongSet toDrop = new LongOpenHashSet();
|
|
/**
|
|
* True if changes have been made to {@link #loadedChunks} and thus a new copy of the collection has to be made into {@link #immutableLoadedChunks}.
|
|
*/
|
|
private boolean modified;
|
|
private final ChunkTaskDispatcher worldgenTaskDispatcher;
|
|
private final ChunkTaskDispatcher lightTaskDispatcher;
|
|
private final ChunkProgressListener progressListener;
|
|
private final ChunkStatusUpdateListener chunkStatusListener;
|
|
private final ChunkMap.DistanceManager distanceManager;
|
|
private final AtomicInteger tickingGenerated = new AtomicInteger();
|
|
private final String storageName;
|
|
private final PlayerMap playerMap = new PlayerMap();
|
|
private final Int2ObjectMap<ChunkMap.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.<Runnable>newConcurrentLinkedQueue();
|
|
private final AtomicInteger activeChunkWrites = new AtomicInteger();
|
|
private int serverViewDistance;
|
|
private final WorldGenContext worldGenContext;
|
|
|
|
public ChunkMap(
|
|
ServerLevel level,
|
|
LevelStorageSource.LevelStorageAccess levelStorageAccess,
|
|
DataFixer fixerUpper,
|
|
StructureTemplateManager structureManager,
|
|
Executor dispatcher,
|
|
BlockableEventLoop<Runnable> mainThreadExecutor,
|
|
LightChunkGetter lightChunk,
|
|
ChunkGenerator generator,
|
|
ChunkProgressListener progressListener,
|
|
ChunkStatusUpdateListener chunkStatusListener,
|
|
Supplier<DimensionDataStorage> overworldDataStorage,
|
|
TicketStorage ticketStorage,
|
|
int serverViewDistance,
|
|
boolean sync
|
|
) {
|
|
super(
|
|
new RegionStorageInfo(levelStorageAccess.getLevelId(), level.dimension(), "chunk"),
|
|
levelStorageAccess.getDimensionPath(level.dimension()).resolve("region"),
|
|
fixerUpper,
|
|
sync
|
|
);
|
|
Path path = levelStorageAccess.getDimensionPath(level.dimension());
|
|
this.storageName = path.getFileName().toString();
|
|
this.level = level;
|
|
RegistryAccess registryAccess = level.registryAccess();
|
|
long l = level.getSeed();
|
|
if (generator instanceof NoiseBasedChunkGenerator noiseBasedChunkGenerator) {
|
|
this.randomState = RandomState.create(noiseBasedChunkGenerator.generatorSettings().value(), registryAccess.lookupOrThrow(Registries.NOISE), l);
|
|
} else {
|
|
this.randomState = RandomState.create(NoiseGeneratorSettings.dummy(), registryAccess.lookupOrThrow(Registries.NOISE), l);
|
|
}
|
|
|
|
this.chunkGeneratorState = generator.createState(registryAccess.lookupOrThrow(Registries.STRUCTURE_SET), this.randomState, l);
|
|
this.mainThreadExecutor = mainThreadExecutor;
|
|
ConsecutiveExecutor consecutiveExecutor = new ConsecutiveExecutor(dispatcher, "worldgen");
|
|
this.progressListener = progressListener;
|
|
this.chunkStatusListener = chunkStatusListener;
|
|
ConsecutiveExecutor consecutiveExecutor2 = new ConsecutiveExecutor(dispatcher, "light");
|
|
this.worldgenTaskDispatcher = new ChunkTaskDispatcher(consecutiveExecutor, dispatcher);
|
|
this.lightTaskDispatcher = new ChunkTaskDispatcher(consecutiveExecutor2, dispatcher);
|
|
this.lightEngine = new ThreadedLevelLightEngine(lightChunk, this, this.level.dimensionType().hasSkyLight(), consecutiveExecutor2, this.lightTaskDispatcher);
|
|
this.distanceManager = new ChunkMap.DistanceManager(ticketStorage, dispatcher, mainThreadExecutor);
|
|
this.overworldDataStorage = overworldDataStorage;
|
|
this.ticketStorage = ticketStorage;
|
|
this.poiManager = new PoiManager(
|
|
new RegionStorageInfo(levelStorageAccess.getLevelId(), level.dimension(), "poi"),
|
|
path.resolve("poi"),
|
|
fixerUpper,
|
|
sync,
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Checks if a chunk is within a player's view distance.
|
|
*/
|
|
boolean isChunkTracked(ServerPlayer player, int x, int z) {
|
|
return player.getChunkTrackingView().contains(x, z) && !player.connection.chunkSender.isPending(ChunkPos.asLong(x, z));
|
|
}
|
|
|
|
/**
|
|
* Checks if a chunk is on the edge of the player's view distance.
|
|
*/
|
|
private boolean isChunkOnTrackedBorder(ServerPlayer player, int x, int z) {
|
|
if (!this.isChunkTracked(player, x, z)) {
|
|
return false;
|
|
} else {
|
|
for (int i = -1; i <= 1; i++) {
|
|
for (int j = -1; j <= 1; j++) {
|
|
if ((i != 0 || j != 0) && !this.isChunkTracked(player, x + i, z + j)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
protected ThreadedLevelLightEngine getLightEngine() {
|
|
return this.lightEngine;
|
|
}
|
|
|
|
@Nullable
|
|
protected ChunkHolder getUpdatingChunkIfPresent(long chunkPos) {
|
|
return this.updatingChunkMap.get(chunkPos);
|
|
}
|
|
|
|
@Nullable
|
|
protected ChunkHolder getVisibleChunkIfPresent(long chunkPos) {
|
|
return this.visibleChunkMap.get(chunkPos);
|
|
}
|
|
|
|
protected IntSupplier getChunkQueueLevel(long chunkPos) {
|
|
return () -> {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(chunkPos);
|
|
return chunkHolder == null
|
|
? ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1
|
|
: Math.min(chunkHolder.getQueueLevel(), ChunkTaskPriorityQueue.PRIORITY_LEVEL_COUNT - 1);
|
|
};
|
|
}
|
|
|
|
public String getChunkDebugData(ChunkPos pos) {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(pos.toLong());
|
|
if (chunkHolder == null) {
|
|
return "null";
|
|
} else {
|
|
String string = chunkHolder.getTicketLevel() + "\n";
|
|
ChunkStatus chunkStatus = chunkHolder.getLatestStatus();
|
|
ChunkAccess chunkAccess = chunkHolder.getLatestChunk();
|
|
if (chunkStatus != null) {
|
|
string = string + "St: §" + chunkStatus.getIndex() + chunkStatus + "§r\n";
|
|
}
|
|
|
|
if (chunkAccess != null) {
|
|
string = string + "Ch: §" + chunkAccess.getPersistedStatus().getIndex() + chunkAccess.getPersistedStatus() + "§r\n";
|
|
}
|
|
|
|
FullChunkStatus fullChunkStatus = chunkHolder.getFullStatus();
|
|
string = string + '§' + fullChunkStatus.ordinal() + fullChunkStatus;
|
|
return string + "§r";
|
|
}
|
|
}
|
|
|
|
private CompletableFuture<ChunkResult<List<ChunkAccess>>> getChunkRangeFuture(ChunkHolder chunkHolder, int range, IntFunction<ChunkStatus> statusGetter) {
|
|
if (range == 0) {
|
|
ChunkStatus chunkStatus = (ChunkStatus)statusGetter.apply(0);
|
|
return chunkHolder.scheduleChunkGenerationTask(chunkStatus, this).thenApply(chunkResult -> chunkResult.map(List::of));
|
|
} else {
|
|
int i = Mth.square(range * 2 + 1);
|
|
List<CompletableFuture<ChunkResult<ChunkAccess>>> list = new ArrayList(i);
|
|
ChunkPos chunkPos = chunkHolder.getPos();
|
|
|
|
for (int j = -range; j <= range; j++) {
|
|
for (int k = -range; k <= range; k++) {
|
|
int l = Math.max(Math.abs(k), Math.abs(j));
|
|
long m = ChunkPos.asLong(chunkPos.x + k, chunkPos.z + j);
|
|
ChunkHolder chunkHolder2 = this.getUpdatingChunkIfPresent(m);
|
|
if (chunkHolder2 == null) {
|
|
return UNLOADED_CHUNK_LIST_FUTURE;
|
|
}
|
|
|
|
ChunkStatus chunkStatus2 = (ChunkStatus)statusGetter.apply(l);
|
|
list.add(chunkHolder2.scheduleChunkGenerationTask(chunkStatus2, this));
|
|
}
|
|
}
|
|
|
|
return Util.sequence(list).thenApply(listx -> {
|
|
List<ChunkAccess> list2 = new ArrayList(listx.size());
|
|
|
|
for (ChunkResult<ChunkAccess> chunkResult : listx) {
|
|
if (chunkResult == null) {
|
|
throw this.debugFuturesAndCreateReportedException(new IllegalStateException("At least one of the chunk futures were null"), "n/a");
|
|
}
|
|
|
|
ChunkAccess chunkAccess = chunkResult.orElse(null);
|
|
if (chunkAccess == null) {
|
|
return UNLOADED_CHUNK_LIST_RESULT;
|
|
}
|
|
|
|
list2.add(chunkAccess);
|
|
}
|
|
|
|
return ChunkResult.of(list2);
|
|
});
|
|
}
|
|
}
|
|
|
|
public ReportedException debugFuturesAndCreateReportedException(IllegalStateException exception, String details) {
|
|
StringBuilder stringBuilder = new StringBuilder();
|
|
Consumer<ChunkHolder> consumer = chunkHolder -> chunkHolder.getAllFutures()
|
|
.forEach(
|
|
pair -> {
|
|
ChunkStatus chunkStatus = (ChunkStatus)pair.getFirst();
|
|
CompletableFuture<ChunkResult<ChunkAccess>> completableFuture = (CompletableFuture<ChunkResult<ChunkAccess>>)pair.getSecond();
|
|
if (completableFuture != null && completableFuture.isDone() && completableFuture.join() == null) {
|
|
stringBuilder.append(chunkHolder.getPos())
|
|
.append(" - status: ")
|
|
.append(chunkStatus)
|
|
.append(" future: ")
|
|
.append(completableFuture)
|
|
.append(System.lineSeparator());
|
|
}
|
|
}
|
|
);
|
|
stringBuilder.append("Updating:").append(System.lineSeparator());
|
|
this.updatingChunkMap.values().forEach(consumer);
|
|
stringBuilder.append("Visible:").append(System.lineSeparator());
|
|
this.visibleChunkMap.values().forEach(consumer);
|
|
CrashReport crashReport = CrashReport.forThrowable(exception, "Chunk loading");
|
|
CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk loading");
|
|
crashReportCategory.setDetail("Details", details);
|
|
crashReportCategory.setDetail("Futures", stringBuilder);
|
|
return new ReportedException(crashReport);
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> prepareEntityTickingChunk(ChunkHolder chunk) {
|
|
return this.getChunkRangeFuture(chunk, 2, i -> ChunkStatus.FULL).thenApply(chunkResult -> chunkResult.map(list -> (LevelChunk)list.get(list.size() / 2)));
|
|
}
|
|
|
|
/**
|
|
* Sets level and loads/unloads chunk.
|
|
*
|
|
* @param holder The {@link net.minecraft.server.level.ChunkHolder} of the chunk if it is loaded, and null otherwise.
|
|
*/
|
|
@Nullable
|
|
ChunkHolder updateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel) {
|
|
if (!ChunkLevel.isLoaded(oldLevel) && !ChunkLevel.isLoaded(newLevel)) {
|
|
return holder;
|
|
} else {
|
|
if (holder != null) {
|
|
holder.setTicketLevel(newLevel);
|
|
}
|
|
|
|
if (holder != null) {
|
|
if (!ChunkLevel.isLoaded(newLevel)) {
|
|
this.toDrop.add(chunkPos);
|
|
} else {
|
|
this.toDrop.remove(chunkPos);
|
|
}
|
|
}
|
|
|
|
if (ChunkLevel.isLoaded(newLevel) && holder == null) {
|
|
holder = this.pendingUnloads.remove(chunkPos);
|
|
if (holder != null) {
|
|
holder.setTicketLevel(newLevel);
|
|
} else {
|
|
holder = new ChunkHolder(new ChunkPos(chunkPos), newLevel, this.level, this.lightEngine, this::onLevelChange, this);
|
|
}
|
|
|
|
this.updatingChunkMap.put(chunkPos, holder);
|
|
this.modified = true;
|
|
}
|
|
|
|
return holder;
|
|
}
|
|
}
|
|
|
|
private void onLevelChange(ChunkPos chunkPos, IntSupplier queueLevelGetter, int ticketLevel, IntConsumer queueLevelSetter) {
|
|
this.worldgenTaskDispatcher.onLevelChange(chunkPos, queueLevelGetter, ticketLevel, queueLevelSetter);
|
|
this.lightTaskDispatcher.onLevelChange(chunkPos, queueLevelGetter, ticketLevel, queueLevelSetter);
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
try {
|
|
this.worldgenTaskDispatcher.close();
|
|
this.lightTaskDispatcher.close();
|
|
this.poiManager.close();
|
|
} finally {
|
|
super.close();
|
|
}
|
|
}
|
|
|
|
protected void saveAllChunks(boolean flush) {
|
|
if (flush) {
|
|
List<ChunkHolder> list = this.visibleChunkMap
|
|
.values()
|
|
.stream()
|
|
.filter(ChunkHolder::wasAccessibleSinceLastSave)
|
|
.peek(ChunkHolder::refreshAccessibility)
|
|
.toList();
|
|
MutableBoolean mutableBoolean = new MutableBoolean();
|
|
|
|
do {
|
|
mutableBoolean.setFalse();
|
|
list.stream()
|
|
.map(chunkHolderx -> {
|
|
this.mainThreadExecutor.managedBlock(chunkHolderx::isReadyForSaving);
|
|
return chunkHolderx.getLatestChunk();
|
|
})
|
|
.filter(chunkAccess -> chunkAccess instanceof ImposterProtoChunk || chunkAccess instanceof LevelChunk)
|
|
.filter(this::save)
|
|
.forEach(chunkAccess -> mutableBoolean.setTrue());
|
|
} while (mutableBoolean.isTrue());
|
|
|
|
this.poiManager.flushAll();
|
|
this.processUnloads(() -> true);
|
|
this.flushWorker();
|
|
} else {
|
|
this.nextChunkSaveTime.clear();
|
|
long l = Util.getMillis();
|
|
|
|
for (ChunkHolder chunkHolder : this.visibleChunkMap.values()) {
|
|
this.saveChunkIfNeeded(chunkHolder, l);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void tick(BooleanSupplier hasMoreTime) {
|
|
ProfilerFiller profilerFiller = Profiler.get();
|
|
profilerFiller.push("poi");
|
|
this.poiManager.tick(hasMoreTime);
|
|
profilerFiller.popPush("chunk_unload");
|
|
if (!this.level.noSave()) {
|
|
this.processUnloads(hasMoreTime);
|
|
}
|
|
|
|
profilerFiller.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 hasMoreTime) {
|
|
for (LongIterator longIterator = this.toDrop.iterator(); longIterator.hasNext(); longIterator.remove()) {
|
|
long l = longIterator.nextLong();
|
|
ChunkHolder chunkHolder = this.updatingChunkMap.get(l);
|
|
if (chunkHolder != null) {
|
|
this.updatingChunkMap.remove(l);
|
|
this.pendingUnloads.put(l, chunkHolder);
|
|
this.modified = true;
|
|
this.scheduleUnload(l, chunkHolder);
|
|
}
|
|
}
|
|
|
|
int i = Math.max(0, this.unloadQueue.size() - 2000);
|
|
|
|
Runnable runnable;
|
|
while ((i > 0 || hasMoreTime.getAsBoolean()) && (runnable = (Runnable)this.unloadQueue.poll()) != null) {
|
|
i--;
|
|
runnable.run();
|
|
}
|
|
|
|
this.saveChunksEagerly(hasMoreTime);
|
|
}
|
|
|
|
private void saveChunksEagerly(BooleanSupplier hasMoreTime) {
|
|
long l = Util.getMillis();
|
|
int i = 0;
|
|
LongIterator longIterator = this.chunksToEagerlySave.iterator();
|
|
|
|
while (i < 20 && this.activeChunkWrites.get() < 128 && hasMoreTime.getAsBoolean() && longIterator.hasNext()) {
|
|
long m = longIterator.nextLong();
|
|
ChunkHolder chunkHolder = this.visibleChunkMap.get(m);
|
|
ChunkAccess chunkAccess = chunkHolder != null ? chunkHolder.getLatestChunk() : null;
|
|
if (chunkAccess == null || !chunkAccess.isUnsaved()) {
|
|
longIterator.remove();
|
|
} else if (this.saveChunkIfNeeded(chunkHolder, l)) {
|
|
i++;
|
|
longIterator.remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void scheduleUnload(long chunkPos, ChunkHolder chunkHolder) {
|
|
CompletableFuture<?> completableFuture = chunkHolder.getSaveSyncFuture();
|
|
completableFuture.thenRunAsync(() -> {
|
|
CompletableFuture<?> completableFuture2 = chunkHolder.getSaveSyncFuture();
|
|
if (completableFuture2 != completableFuture) {
|
|
this.scheduleUnload(chunkPos, chunkHolder);
|
|
} else {
|
|
ChunkAccess chunkAccess = chunkHolder.getLatestChunk();
|
|
if (this.pendingUnloads.remove(chunkPos, chunkHolder) && chunkAccess != null) {
|
|
if (chunkAccess instanceof LevelChunk levelChunk) {
|
|
levelChunk.setLoaded(false);
|
|
}
|
|
|
|
this.save(chunkAccess);
|
|
if (chunkAccess instanceof LevelChunk levelChunk) {
|
|
this.level.unload(levelChunk);
|
|
}
|
|
|
|
this.lightEngine.updateChunkStatus(chunkAccess.getPos());
|
|
this.lightEngine.tryScheduleUpdate();
|
|
this.progressListener.onStatusChange(chunkAccess.getPos(), null);
|
|
this.nextChunkSaveTime.remove(chunkAccess.getPos().toLong());
|
|
}
|
|
}
|
|
}, this.unloadQueue::add).whenComplete((void_, throwable) -> {
|
|
if (throwable != null) {
|
|
LOGGER.error("Failed to save chunk {}", chunkHolder.getPos(), throwable);
|
|
}
|
|
});
|
|
}
|
|
|
|
protected boolean promoteChunkMap() {
|
|
if (!this.modified) {
|
|
return false;
|
|
} else {
|
|
this.visibleChunkMap = this.updatingChunkMap.clone();
|
|
this.modified = false;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
private CompletableFuture<ChunkAccess> scheduleChunkLoad(ChunkPos chunkPos) {
|
|
CompletableFuture<Optional<SerializableChunkData>> completableFuture = this.readChunk(chunkPos).thenApplyAsync(optional -> optional.map(compoundTag -> {
|
|
SerializableChunkData serializableChunkData = SerializableChunkData.parse(this.level, this.level.registryAccess(), compoundTag);
|
|
if (serializableChunkData == null) {
|
|
LOGGER.error("Chunk file at {} is missing level data, skipping", chunkPos);
|
|
}
|
|
|
|
return serializableChunkData;
|
|
}), Util.backgroundExecutor().forName("parseChunk"));
|
|
CompletableFuture<?> completableFuture2 = this.poiManager.prefetch(chunkPos);
|
|
return completableFuture.thenCombine(completableFuture2, (optional, object) -> optional).thenApplyAsync(optional -> {
|
|
Profiler.get().incrementCounter("chunkLoad");
|
|
if (optional.isPresent()) {
|
|
ChunkAccess chunkAccess = ((SerializableChunkData)optional.get()).read(this.level, this.poiManager, this.storageInfo(), chunkPos);
|
|
this.markPosition(chunkPos, chunkAccess.getPersistedStatus().getChunkType());
|
|
return chunkAccess;
|
|
} else {
|
|
return this.createEmptyChunk(chunkPos);
|
|
}
|
|
}, this.mainThreadExecutor).exceptionallyAsync(throwable -> this.handleChunkLoadFailure(throwable, chunkPos), this.mainThreadExecutor);
|
|
}
|
|
|
|
private ChunkAccess handleChunkLoadFailure(Throwable exception, ChunkPos chunkPos) {
|
|
Throwable throwable = exception instanceof CompletionException completionException ? completionException.getCause() : exception;
|
|
Throwable throwable2 = throwable instanceof ReportedException reportedException ? reportedException.getCause() : throwable;
|
|
boolean bl = throwable2 instanceof Error;
|
|
boolean bl2 = throwable2 instanceof IOException || throwable2 instanceof NbtException;
|
|
if (!bl) {
|
|
if (!bl2) {
|
|
}
|
|
|
|
this.level.getServer().reportChunkLoadFailure(throwable2, this.storageInfo(), chunkPos);
|
|
return this.createEmptyChunk(chunkPos);
|
|
} else {
|
|
CrashReport crashReport = CrashReport.forThrowable(exception, "Exception loading chunk");
|
|
CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk being loaded");
|
|
crashReportCategory.setDetail("pos", chunkPos);
|
|
this.markPositionReplaceable(chunkPos);
|
|
throw new ReportedException(crashReport);
|
|
}
|
|
}
|
|
|
|
private ChunkAccess createEmptyChunk(ChunkPos chunkPos) {
|
|
this.markPositionReplaceable(chunkPos);
|
|
return new ProtoChunk(chunkPos, UpgradeData.EMPTY, this.level, this.level.registryAccess().lookupOrThrow(Registries.BIOME), null);
|
|
}
|
|
|
|
private void markPositionReplaceable(ChunkPos chunkPos) {
|
|
this.chunkTypeCache.put(chunkPos.toLong(), (byte)-1);
|
|
}
|
|
|
|
private byte markPosition(ChunkPos chunkPos, ChunkType chunkType) {
|
|
return this.chunkTypeCache.put(chunkPos.toLong(), (byte)(chunkType == ChunkType.PROTOCHUNK ? -1 : 1));
|
|
}
|
|
|
|
@Override
|
|
public GenerationChunkHolder acquireGeneration(long chunkPos) {
|
|
ChunkHolder chunkHolder = this.updatingChunkMap.get(chunkPos);
|
|
chunkHolder.increaseGenerationRefCount();
|
|
return chunkHolder;
|
|
}
|
|
|
|
@Override
|
|
public void releaseGeneration(GenerationChunkHolder chunk) {
|
|
chunk.decreaseGenerationRefCount();
|
|
}
|
|
|
|
@Override
|
|
public CompletableFuture<ChunkAccess> applyStep(GenerationChunkHolder chunk, ChunkStep step, StaticCache2D<GenerationChunkHolder> cache) {
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
if (step.targetStatus() == ChunkStatus.EMPTY) {
|
|
return this.scheduleChunkLoad(chunkPos);
|
|
} else {
|
|
try {
|
|
GenerationChunkHolder generationChunkHolder = cache.get(chunkPos.x, chunkPos.z);
|
|
ChunkAccess chunkAccess = generationChunkHolder.getChunkIfPresentUnchecked(step.targetStatus().getParent());
|
|
if (chunkAccess == null) {
|
|
throw new IllegalStateException("Parent chunk missing");
|
|
} else {
|
|
CompletableFuture<ChunkAccess> completableFuture = step.apply(this.worldGenContext, cache, chunkAccess);
|
|
this.progressListener.onStatusChange(chunkPos, step.targetStatus());
|
|
return completableFuture;
|
|
}
|
|
} catch (Exception var8) {
|
|
var8.getStackTrace();
|
|
CrashReport crashReport = CrashReport.forThrowable(var8, "Exception generating new chunk");
|
|
CrashReportCategory crashReportCategory = crashReport.addCategory("Chunk to be generated");
|
|
crashReportCategory.setDetail("Status being generated", (CrashReportDetail<String>)(() -> step.targetStatus().getName()));
|
|
crashReportCategory.setDetail("Location", String.format(Locale.ROOT, "%d,%d", chunkPos.x, chunkPos.z));
|
|
crashReportCategory.setDetail("Position hash", ChunkPos.asLong(chunkPos.x, chunkPos.z));
|
|
crashReportCategory.setDetail("Generator", this.generator());
|
|
this.mainThreadExecutor.execute(() -> {
|
|
throw new ReportedException(crashReport);
|
|
});
|
|
throw new ReportedException(crashReport);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public ChunkGenerationTask scheduleGenerationTask(ChunkStatus targetStatus, ChunkPos pos) {
|
|
ChunkGenerationTask chunkGenerationTask = ChunkGenerationTask.create(this, targetStatus, pos);
|
|
this.pendingGenerationTasks.add(chunkGenerationTask);
|
|
return chunkGenerationTask;
|
|
}
|
|
|
|
private void runGenerationTask(ChunkGenerationTask task) {
|
|
GenerationChunkHolder generationChunkHolder = task.getCenter();
|
|
this.worldgenTaskDispatcher.submit(() -> {
|
|
CompletableFuture<?> completableFuture = task.runUntilWait();
|
|
if (completableFuture != null) {
|
|
completableFuture.thenRun(() -> this.runGenerationTask(task));
|
|
}
|
|
}, generationChunkHolder.getPos().toLong(), generationChunkHolder::getQueueLevel);
|
|
}
|
|
|
|
@Override
|
|
public void runGenerationTasks() {
|
|
this.pendingGenerationTasks.forEach(this::runGenerationTask);
|
|
this.pendingGenerationTasks.clear();
|
|
}
|
|
|
|
public CompletableFuture<ChunkResult<LevelChunk>> prepareTickingChunk(ChunkHolder holder) {
|
|
CompletableFuture<ChunkResult<List<ChunkAccess>>> completableFuture = this.getChunkRangeFuture(holder, 1, i -> ChunkStatus.FULL);
|
|
CompletableFuture<ChunkResult<LevelChunk>> completableFuture2 = completableFuture.thenApplyAsync(chunkResult -> chunkResult.map(list -> {
|
|
LevelChunk levelChunk = (LevelChunk)list.get(list.size() / 2);
|
|
levelChunk.postProcessGeneration(this.level);
|
|
this.level.startTickingChunk(levelChunk);
|
|
CompletableFuture<?> completableFuturex = holder.getSendSyncFuture();
|
|
if (completableFuturex.isDone()) {
|
|
this.onChunkReadyToSend(holder, levelChunk);
|
|
} else {
|
|
completableFuturex.thenAcceptAsync(object -> this.onChunkReadyToSend(holder, levelChunk), this.mainThreadExecutor);
|
|
}
|
|
|
|
return levelChunk;
|
|
}), this.mainThreadExecutor);
|
|
completableFuture2.handle((chunkResult, throwable) -> {
|
|
this.tickingGenerated.getAndIncrement();
|
|
return null;
|
|
});
|
|
return completableFuture2;
|
|
}
|
|
|
|
private void onChunkReadyToSend(ChunkHolder chunkHolder, LevelChunk chunk) {
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
|
|
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
|
|
if (serverPlayer.getChunkTrackingView().contains(chunkPos)) {
|
|
markChunkPendingToSend(serverPlayer, chunk);
|
|
}
|
|
}
|
|
|
|
this.level.getChunkSource().onChunkReadyToSend(chunkHolder);
|
|
}
|
|
|
|
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)));
|
|
}
|
|
|
|
public int getTickingGenerated() {
|
|
return this.tickingGenerated.get();
|
|
}
|
|
|
|
private boolean saveChunkIfNeeded(ChunkHolder chunk, long gametime) {
|
|
if (chunk.wasAccessibleSinceLastSave() && chunk.isReadyForSaving()) {
|
|
ChunkAccess chunkAccess = chunk.getLatestChunk();
|
|
if (!(chunkAccess instanceof ImposterProtoChunk) && !(chunkAccess instanceof LevelChunk)) {
|
|
return false;
|
|
} else if (!chunkAccess.isUnsaved()) {
|
|
return false;
|
|
} else {
|
|
long l = chunkAccess.getPos().toLong();
|
|
long m = this.nextChunkSaveTime.getOrDefault(l, -1L);
|
|
if (gametime < m) {
|
|
return false;
|
|
} else {
|
|
boolean bl = this.save(chunkAccess);
|
|
chunk.refreshAccessibility();
|
|
if (bl) {
|
|
this.nextChunkSaveTime.put(l, gametime + 10000L);
|
|
}
|
|
|
|
return bl;
|
|
}
|
|
}
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private boolean save(ChunkAccess chunk) {
|
|
this.poiManager.flush(chunk.getPos());
|
|
if (!chunk.tryMarkSaved()) {
|
|
return false;
|
|
} else {
|
|
ChunkPos chunkPos = chunk.getPos();
|
|
|
|
try {
|
|
ChunkStatus chunkStatus = chunk.getPersistedStatus();
|
|
if (chunkStatus.getChunkType() != ChunkType.LEVELCHUNK) {
|
|
if (this.isExistingChunkFull(chunkPos)) {
|
|
return false;
|
|
}
|
|
|
|
if (chunkStatus == ChunkStatus.EMPTY && chunk.getAllStarts().values().stream().noneMatch(StructureStart::isValid)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Profiler.get().incrementCounter("chunkSave");
|
|
this.activeChunkWrites.incrementAndGet();
|
|
SerializableChunkData serializableChunkData = SerializableChunkData.copyOf(this.level, chunk);
|
|
CompletableFuture<CompoundTag> completableFuture = CompletableFuture.supplyAsync(serializableChunkData::write, Util.backgroundExecutor());
|
|
this.write(chunkPos, completableFuture::join).handle((void_, throwable) -> {
|
|
if (throwable != null) {
|
|
this.level.getServer().reportChunkSaveFailure(throwable, this.storageInfo(), chunkPos);
|
|
}
|
|
|
|
this.activeChunkWrites.decrementAndGet();
|
|
return null;
|
|
});
|
|
this.markPosition(chunkPos, chunkStatus.getChunkType());
|
|
return true;
|
|
} catch (Exception var6) {
|
|
this.level.getServer().reportChunkSaveFailure(var6, this.storageInfo(), chunkPos);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean isExistingChunkFull(ChunkPos chunkPos) {
|
|
byte b = this.chunkTypeCache.get(chunkPos.toLong());
|
|
if (b != 0) {
|
|
return b == 1;
|
|
} else {
|
|
CompoundTag compoundTag;
|
|
try {
|
|
compoundTag = (CompoundTag)((Optional)this.readChunk(chunkPos).join()).orElse(null);
|
|
if (compoundTag == null) {
|
|
this.markPositionReplaceable(chunkPos);
|
|
return false;
|
|
}
|
|
} catch (Exception var5) {
|
|
LOGGER.error("Failed to read chunk {}", chunkPos, var5);
|
|
this.markPositionReplaceable(chunkPos);
|
|
return false;
|
|
}
|
|
|
|
ChunkType chunkType = SerializableChunkData.getChunkStatusFromTag(compoundTag).getChunkType();
|
|
return this.markPosition(chunkPos, chunkType) == 1;
|
|
}
|
|
}
|
|
|
|
protected void setServerViewDistance(int viewDistance) {
|
|
int i = Mth.clamp(viewDistance, 2, 32);
|
|
if (i != this.serverViewDistance) {
|
|
this.serverViewDistance = i;
|
|
this.distanceManager.updatePlayerTickets(this.serverViewDistance);
|
|
|
|
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
|
|
this.updateChunkTracking(serverPlayer);
|
|
}
|
|
}
|
|
}
|
|
|
|
int getPlayerViewDistance(ServerPlayer player) {
|
|
return Mth.clamp(player.requestedViewDistance(), 2, this.serverViewDistance);
|
|
}
|
|
|
|
private void markChunkPendingToSend(ServerPlayer player, ChunkPos chunkPos) {
|
|
LevelChunk levelChunk = this.getChunkToSend(chunkPos.toLong());
|
|
if (levelChunk != null) {
|
|
markChunkPendingToSend(player, levelChunk);
|
|
}
|
|
}
|
|
|
|
private static void markChunkPendingToSend(ServerPlayer player, LevelChunk chunk) {
|
|
player.connection.chunkSender.markChunkPendingToSend(chunk);
|
|
}
|
|
|
|
private static void dropChunk(ServerPlayer player, ChunkPos chunkPos) {
|
|
player.connection.chunkSender.dropChunk(player, chunkPos);
|
|
}
|
|
|
|
@Nullable
|
|
public LevelChunk getChunkToSend(long chunkPos) {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(chunkPos);
|
|
return chunkHolder == null ? null : chunkHolder.getChunkToSend();
|
|
}
|
|
|
|
public int size() {
|
|
return this.visibleChunkMap.size();
|
|
}
|
|
|
|
public net.minecraft.server.level.DistanceManager getDistanceManager() {
|
|
return this.distanceManager;
|
|
}
|
|
|
|
/**
|
|
* Gets an unmodifiable iterable of all loaded chunks in the chunk manager
|
|
*/
|
|
protected Iterable<ChunkHolder> getChunks() {
|
|
return Iterables.unmodifiableIterable(this.visibleChunkMap.values());
|
|
}
|
|
|
|
void dumpChunks(Writer writer) 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(writer);
|
|
|
|
for (Entry<ChunkHolder> entry : this.visibleChunkMap.long2ObjectEntrySet()) {
|
|
long l = entry.getLongKey();
|
|
ChunkPos chunkPos = new ChunkPos(l);
|
|
ChunkHolder chunkHolder = (ChunkHolder)entry.getValue();
|
|
Optional<ChunkAccess> optional = Optional.ofNullable(chunkHolder.getLatestChunk());
|
|
Optional<LevelChunk> optional2 = optional.flatMap(chunkAccess -> chunkAccess instanceof LevelChunk ? Optional.of((LevelChunk)chunkAccess) : Optional.empty());
|
|
csvOutput.writeRow(
|
|
chunkPos.x,
|
|
chunkPos.z,
|
|
chunkHolder.getTicketLevel(),
|
|
optional.isPresent(),
|
|
optional.map(ChunkAccess::getPersistedStatus).orElse(null),
|
|
optional2.map(LevelChunk::getFullStatus).orElse(null),
|
|
printFuture(chunkHolder.getFullChunkFuture()),
|
|
printFuture(chunkHolder.getTickingChunkFuture()),
|
|
printFuture(chunkHolder.getEntityTickingChunkFuture()),
|
|
this.ticketStorage.getTicketDebugString(l, false),
|
|
this.anyPlayerCloseEnoughForSpawning(chunkPos),
|
|
optional2.map(levelChunk -> levelChunk.getBlockEntities().size()).orElse(0),
|
|
this.ticketStorage.getTicketDebugString(l, true),
|
|
this.distanceManager.getChunkLevel(l, true),
|
|
optional2.map(levelChunk -> levelChunk.getBlockTicks().count()).orElse(0),
|
|
optional2.map(levelChunk -> levelChunk.getFluidTicks().count()).orElse(0)
|
|
);
|
|
}
|
|
}
|
|
|
|
private static String printFuture(CompletableFuture<ChunkResult<LevelChunk>> future) {
|
|
try {
|
|
ChunkResult<LevelChunk> chunkResult = (ChunkResult<LevelChunk>)future.getNow(null);
|
|
if (chunkResult != null) {
|
|
return chunkResult.isSuccess() ? "done" : "unloaded";
|
|
} else {
|
|
return "not completed";
|
|
}
|
|
} catch (CompletionException var2) {
|
|
return "failed " + var2.getCause().getMessage();
|
|
} catch (CancellationException var3) {
|
|
return "cancelled";
|
|
}
|
|
}
|
|
|
|
private CompletableFuture<Optional<CompoundTag>> readChunk(ChunkPos pos) {
|
|
return this.read(pos).thenApplyAsync(optional -> optional.map(this::upgradeChunkTag), Util.backgroundExecutor().forName("upgradeChunk"));
|
|
}
|
|
|
|
private CompoundTag upgradeChunkTag(CompoundTag tag) {
|
|
return this.upgradeChunkTag(this.level.dimension(), this.overworldDataStorage, tag, this.generator().getTypeNameForDataFixer());
|
|
}
|
|
|
|
void collectSpawningChunks(List<LevelChunk> output) {
|
|
LongIterator longIterator = this.distanceManager.getSpawnCandidateChunks();
|
|
|
|
while (longIterator.hasNext()) {
|
|
ChunkHolder chunkHolder = this.visibleChunkMap.get(longIterator.nextLong());
|
|
if (chunkHolder != null) {
|
|
LevelChunk levelChunk = chunkHolder.getTickingChunk();
|
|
if (levelChunk != null && this.anyPlayerCloseEnoughForSpawningInternal(chunkHolder.getPos())) {
|
|
output.add(levelChunk);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void forEachBlockTickingChunk(Consumer<LevelChunk> action) {
|
|
this.distanceManager.forEachEntityTickingChunk(l -> {
|
|
ChunkHolder chunkHolder = this.visibleChunkMap.get(l);
|
|
if (chunkHolder != null) {
|
|
LevelChunk levelChunk = chunkHolder.getTickingChunk();
|
|
if (levelChunk != null) {
|
|
action.accept(levelChunk);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
boolean anyPlayerCloseEnoughForSpawning(ChunkPos chunkPos) {
|
|
TriState triState = this.distanceManager.hasPlayersNearby(chunkPos.toLong());
|
|
return triState == TriState.DEFAULT ? this.anyPlayerCloseEnoughForSpawningInternal(chunkPos) : triState.toBoolean(true);
|
|
}
|
|
|
|
private boolean anyPlayerCloseEnoughForSpawningInternal(ChunkPos chunkPos) {
|
|
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
|
|
if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public List<ServerPlayer> getPlayersCloseForSpawning(ChunkPos chunkPos) {
|
|
long l = chunkPos.toLong();
|
|
if (!this.distanceManager.hasPlayersNearby(l).toBoolean(true)) {
|
|
return List.of();
|
|
} else {
|
|
Builder<ServerPlayer> builder = ImmutableList.builder();
|
|
|
|
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
|
|
if (this.playerIsCloseEnoughForSpawning(serverPlayer, chunkPos)) {
|
|
builder.add(serverPlayer);
|
|
}
|
|
}
|
|
|
|
return builder.build();
|
|
}
|
|
}
|
|
|
|
private boolean playerIsCloseEnoughForSpawning(ServerPlayer player, ChunkPos chunkPos) {
|
|
if (player.isSpectator()) {
|
|
return false;
|
|
} else {
|
|
double d = euclideanDistanceSquared(chunkPos, player.position());
|
|
return d < 16384.0;
|
|
}
|
|
}
|
|
|
|
private static double euclideanDistanceSquared(ChunkPos chunkPos, Vec3 pos) {
|
|
double d = SectionPos.sectionToBlockCoord(chunkPos.x, 8);
|
|
double e = SectionPos.sectionToBlockCoord(chunkPos.z, 8);
|
|
double f = d - pos.x;
|
|
double g = e - pos.z;
|
|
return f * f + g * g;
|
|
}
|
|
|
|
private boolean skipPlayer(ServerPlayer player) {
|
|
return player.isSpectator() && !this.level.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
|
|
}
|
|
|
|
void updatePlayerStatus(ServerPlayer player, boolean track) {
|
|
boolean bl = this.skipPlayer(player);
|
|
boolean bl2 = this.playerMap.ignoredOrUnknown(player);
|
|
if (track) {
|
|
this.playerMap.addPlayer(player, bl);
|
|
this.updatePlayerPos(player);
|
|
if (!bl) {
|
|
this.distanceManager.addPlayer(SectionPos.of(player), player);
|
|
}
|
|
|
|
player.setChunkTrackingView(ChunkTrackingView.EMPTY);
|
|
this.updateChunkTracking(player);
|
|
} else {
|
|
SectionPos sectionPos = player.getLastSectionPos();
|
|
this.playerMap.removePlayer(player);
|
|
if (!bl2) {
|
|
this.distanceManager.removePlayer(sectionPos, player);
|
|
}
|
|
|
|
this.applyChunkTrackingView(player, ChunkTrackingView.EMPTY);
|
|
}
|
|
}
|
|
|
|
private void updatePlayerPos(ServerPlayer player) {
|
|
SectionPos sectionPos = SectionPos.of(player);
|
|
player.setLastSectionPos(sectionPos);
|
|
}
|
|
|
|
public void move(ServerPlayer player) {
|
|
for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
if (trackedEntity.entity == player) {
|
|
trackedEntity.updatePlayers(this.level.players());
|
|
} else {
|
|
trackedEntity.updatePlayer(player);
|
|
}
|
|
}
|
|
|
|
SectionPos sectionPos = player.getLastSectionPos();
|
|
SectionPos sectionPos2 = SectionPos.of(player);
|
|
boolean bl = this.playerMap.ignored(player);
|
|
boolean bl2 = this.skipPlayer(player);
|
|
boolean bl3 = sectionPos.asLong() != sectionPos2.asLong();
|
|
if (bl3 || bl != bl2) {
|
|
this.updatePlayerPos(player);
|
|
if (!bl) {
|
|
this.distanceManager.removePlayer(sectionPos, player);
|
|
}
|
|
|
|
if (!bl2) {
|
|
this.distanceManager.addPlayer(sectionPos2, player);
|
|
}
|
|
|
|
if (!bl && bl2) {
|
|
this.playerMap.ignorePlayer(player);
|
|
}
|
|
|
|
if (bl && !bl2) {
|
|
this.playerMap.unIgnorePlayer(player);
|
|
}
|
|
|
|
this.updateChunkTracking(player);
|
|
}
|
|
}
|
|
|
|
private void updateChunkTracking(ServerPlayer player) {
|
|
ChunkPos chunkPos = player.chunkPosition();
|
|
int i = this.getPlayerViewDistance(player);
|
|
if (!(player.getChunkTrackingView() instanceof Positioned positioned && positioned.center().equals(chunkPos) && positioned.viewDistance() == i)) {
|
|
this.applyChunkTrackingView(player, ChunkTrackingView.of(chunkPos, i));
|
|
}
|
|
}
|
|
|
|
private void applyChunkTrackingView(ServerPlayer player, ChunkTrackingView chunkTrackingView) {
|
|
if (player.level() == this.level) {
|
|
ChunkTrackingView chunkTrackingView2 = player.getChunkTrackingView();
|
|
if (chunkTrackingView instanceof Positioned positioned
|
|
&& !(chunkTrackingView2 instanceof Positioned positioned2 && positioned2.center().equals(positioned.center()))) {
|
|
player.connection.send(new ClientboundSetChunkCacheCenterPacket(positioned.center().x, positioned.center().z));
|
|
}
|
|
|
|
ChunkTrackingView.difference(
|
|
chunkTrackingView2, chunkTrackingView, chunkPos -> this.markChunkPendingToSend(player, chunkPos), chunkPos -> dropChunk(player, chunkPos)
|
|
);
|
|
player.setChunkTrackingView(chunkTrackingView);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public List<ServerPlayer> getPlayers(ChunkPos pos, boolean boundaryOnly) {
|
|
Set<ServerPlayer> set = this.playerMap.getAllPlayers();
|
|
Builder<ServerPlayer> builder = ImmutableList.builder();
|
|
|
|
for (ServerPlayer serverPlayer : set) {
|
|
if (boundaryOnly && this.isChunkOnTrackedBorder(serverPlayer, pos.x, pos.z) || !boundaryOnly && this.isChunkTracked(serverPlayer, pos.x, pos.z)) {
|
|
builder.add(serverPlayer);
|
|
}
|
|
}
|
|
|
|
return builder.build();
|
|
}
|
|
|
|
protected void addEntity(Entity entity) {
|
|
if (!(entity instanceof EnderDragonPart)) {
|
|
EntityType<?> entityType = entity.getType();
|
|
int i = entityType.clientTrackingRange() * 16;
|
|
if (i != 0) {
|
|
int j = entityType.updateInterval();
|
|
if (this.entityMap.containsKey(entity.getId())) {
|
|
throw (IllegalStateException)Util.pauseInIde(new IllegalStateException("Entity is already tracked!"));
|
|
} else {
|
|
ChunkMap.TrackedEntity trackedEntity = new ChunkMap.TrackedEntity(entity, i, j, entityType.trackDeltas());
|
|
this.entityMap.put(entity.getId(), trackedEntity);
|
|
trackedEntity.updatePlayers(this.level.players());
|
|
if (entity instanceof ServerPlayer serverPlayer) {
|
|
this.updatePlayerStatus(serverPlayer, true);
|
|
|
|
for (ChunkMap.TrackedEntity trackedEntity2 : this.entityMap.values()) {
|
|
if (trackedEntity2.entity != serverPlayer) {
|
|
trackedEntity2.updatePlayer(serverPlayer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void removeEntity(Entity entity) {
|
|
if (entity instanceof ServerPlayer serverPlayer) {
|
|
this.updatePlayerStatus(serverPlayer, false);
|
|
|
|
for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
trackedEntity.removePlayer(serverPlayer);
|
|
}
|
|
}
|
|
|
|
ChunkMap.TrackedEntity trackedEntity2 = this.entityMap.remove(entity.getId());
|
|
if (trackedEntity2 != null) {
|
|
trackedEntity2.broadcastRemoved();
|
|
}
|
|
}
|
|
|
|
protected void tick() {
|
|
for (ServerPlayer serverPlayer : this.playerMap.getAllPlayers()) {
|
|
this.updateChunkTracking(serverPlayer);
|
|
}
|
|
|
|
List<ServerPlayer> list = Lists.<ServerPlayer>newArrayList();
|
|
List<ServerPlayer> list2 = this.level.players();
|
|
|
|
for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
SectionPos sectionPos = trackedEntity.lastSectionPos;
|
|
SectionPos sectionPos2 = SectionPos.of(trackedEntity.entity);
|
|
boolean bl = !Objects.equals(sectionPos, sectionPos2);
|
|
if (bl) {
|
|
trackedEntity.updatePlayers(list2);
|
|
Entity entity = trackedEntity.entity;
|
|
if (entity instanceof ServerPlayer) {
|
|
list.add((ServerPlayer)entity);
|
|
}
|
|
|
|
trackedEntity.lastSectionPos = sectionPos2;
|
|
}
|
|
|
|
if (bl || this.distanceManager.inEntityTickingRange(sectionPos2.chunk().toLong())) {
|
|
trackedEntity.serverEntity.sendChanges();
|
|
}
|
|
}
|
|
|
|
if (!list.isEmpty()) {
|
|
for (ChunkMap.TrackedEntity trackedEntity : this.entityMap.values()) {
|
|
trackedEntity.updatePlayers(list);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void broadcast(Entity entity, Packet<?> packet) {
|
|
ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId());
|
|
if (trackedEntity != null) {
|
|
trackedEntity.broadcast(packet);
|
|
}
|
|
}
|
|
|
|
protected void broadcastAndSend(Entity entity, Packet<?> packet) {
|
|
ChunkMap.TrackedEntity trackedEntity = this.entityMap.get(entity.getId());
|
|
if (trackedEntity != null) {
|
|
trackedEntity.broadcastAndSend(packet);
|
|
}
|
|
}
|
|
|
|
public void resendBiomesForChunks(List<ChunkAccess> chunks) {
|
|
Map<ServerPlayer, List<LevelChunk>> map = new HashMap();
|
|
|
|
for (ChunkAccess chunkAccess : chunks) {
|
|
ChunkPos chunkPos = chunkAccess.getPos();
|
|
LevelChunk levelChunk2;
|
|
if (chunkAccess instanceof LevelChunk levelChunk) {
|
|
levelChunk2 = levelChunk;
|
|
} else {
|
|
levelChunk2 = this.level.getChunk(chunkPos.x, chunkPos.z);
|
|
}
|
|
|
|
for (ServerPlayer serverPlayer : this.getPlayers(chunkPos, false)) {
|
|
((List)map.computeIfAbsent(serverPlayer, serverPlayerx -> new ArrayList())).add(levelChunk2);
|
|
}
|
|
}
|
|
|
|
map.forEach((serverPlayerx, list) -> serverPlayerx.connection.send(ClientboundChunksBiomesPacket.forChunks(list)));
|
|
}
|
|
|
|
protected PoiManager getPoiManager() {
|
|
return this.poiManager;
|
|
}
|
|
|
|
public String getStorageName() {
|
|
return this.storageName;
|
|
}
|
|
|
|
void onFullChunkStatusChange(ChunkPos chunkPos, FullChunkStatus fullChunkStatus) {
|
|
this.chunkStatusListener.onChunkStatusChange(chunkPos, fullChunkStatus);
|
|
}
|
|
|
|
public void waitForLightBeforeSending(ChunkPos chunkPos, int range) {
|
|
int i = range + 1;
|
|
ChunkPos.rangeClosed(chunkPos, i).forEach(chunkPosx -> {
|
|
ChunkHolder chunkHolder = this.getVisibleChunkIfPresent(chunkPosx.toLong());
|
|
if (chunkHolder != null) {
|
|
chunkHolder.addSendDependency(this.lightEngine.waitForPendingTasks(chunkPosx.x, chunkPosx.z));
|
|
}
|
|
});
|
|
}
|
|
|
|
class DistanceManager extends net.minecraft.server.level.DistanceManager {
|
|
protected DistanceManager(final TicketStorage ticketStorage, final Executor dispatcher, final Executor mainThreadExecutor) {
|
|
super(ticketStorage, dispatcher, mainThreadExecutor);
|
|
}
|
|
|
|
@Override
|
|
protected boolean isChunkToRemove(long chunkPos) {
|
|
return ChunkMap.this.toDrop.contains(chunkPos);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
protected ChunkHolder getChunk(long chunkPos) {
|
|
return ChunkMap.this.getUpdatingChunkIfPresent(chunkPos);
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
protected ChunkHolder updateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel) {
|
|
return ChunkMap.this.updateChunkScheduling(chunkPos, newLevel, holder, oldLevel);
|
|
}
|
|
}
|
|
|
|
class TrackedEntity {
|
|
final ServerEntity serverEntity;
|
|
final Entity entity;
|
|
private final int range;
|
|
SectionPos lastSectionPos;
|
|
private final Set<ServerPlayerConnection> seenBy = Sets.newIdentityHashSet();
|
|
|
|
public TrackedEntity(final Entity entity, final int range, final int updateInterval, final boolean trackDelta) {
|
|
this.serverEntity = new ServerEntity(ChunkMap.this.level, entity, updateInterval, trackDelta, this::broadcast, this::broadcastIgnorePlayers);
|
|
this.entity = entity;
|
|
this.range = range;
|
|
this.lastSectionPos = SectionPos.of(entity);
|
|
}
|
|
|
|
public boolean equals(Object object) {
|
|
return object instanceof ChunkMap.TrackedEntity ? ((ChunkMap.TrackedEntity)object).entity.getId() == this.entity.getId() : false;
|
|
}
|
|
|
|
public int hashCode() {
|
|
return this.entity.getId();
|
|
}
|
|
|
|
public void broadcast(Packet<?> packet) {
|
|
for (ServerPlayerConnection serverPlayerConnection : this.seenBy) {
|
|
serverPlayerConnection.send(packet);
|
|
}
|
|
}
|
|
|
|
public void broadcastIgnorePlayers(Packet<?> packet, List<UUID> ignoredPlayers) {
|
|
for (ServerPlayerConnection serverPlayerConnection : this.seenBy) {
|
|
if (!ignoredPlayers.contains(serverPlayerConnection.getPlayer().getUUID())) {
|
|
serverPlayerConnection.send(packet);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void broadcastAndSend(Packet<?> packet) {
|
|
this.broadcast(packet);
|
|
if (this.entity instanceof ServerPlayer) {
|
|
((ServerPlayer)this.entity).connection.send(packet);
|
|
}
|
|
}
|
|
|
|
public void broadcastRemoved() {
|
|
for (ServerPlayerConnection serverPlayerConnection : this.seenBy) {
|
|
this.serverEntity.removePairing(serverPlayerConnection.getPlayer());
|
|
}
|
|
}
|
|
|
|
public void removePlayer(ServerPlayer player) {
|
|
if (this.seenBy.remove(player.connection)) {
|
|
this.serverEntity.removePairing(player);
|
|
}
|
|
}
|
|
|
|
public void updatePlayer(ServerPlayer player) {
|
|
if (player != this.entity) {
|
|
Vec3 vec3 = player.position().subtract(this.entity.position());
|
|
int i = ChunkMap.this.getPlayerViewDistance(player);
|
|
double d = Math.min(this.getEffectiveRange(), i * 16);
|
|
double e = vec3.x * vec3.x + vec3.z * vec3.z;
|
|
double f = d * d;
|
|
boolean bl = e <= f
|
|
&& this.entity.broadcastToPlayer(player)
|
|
&& ChunkMap.this.isChunkTracked(player, this.entity.chunkPosition().x, this.entity.chunkPosition().z);
|
|
if (bl) {
|
|
if (this.seenBy.add(player.connection)) {
|
|
this.serverEntity.addPairing(player);
|
|
}
|
|
} else if (this.seenBy.remove(player.connection)) {
|
|
this.serverEntity.removePairing(player);
|
|
}
|
|
}
|
|
}
|
|
|
|
private int scaledRange(int trackingDistance) {
|
|
return ChunkMap.this.level.getServer().getScaledTrackingDistance(trackingDistance);
|
|
}
|
|
|
|
private int getEffectiveRange() {
|
|
int i = this.range;
|
|
|
|
for (Entity entity : this.entity.getIndirectPassengers()) {
|
|
int j = entity.getType().clientTrackingRange() * 16;
|
|
if (j > i) {
|
|
i = j;
|
|
}
|
|
}
|
|
|
|
return this.scaledRange(i);
|
|
}
|
|
|
|
public void updatePlayers(List<ServerPlayer> playersList) {
|
|
for (ServerPlayer serverPlayer : playersList) {
|
|
this.updatePlayer(serverPlayer);
|
|
}
|
|
}
|
|
}
|
|
}
|