minecraft-src/net/minecraft/world/level/entity/PersistentEntitySectionManager.java
2025-07-04 03:45:38 +03:00

445 lines
15 KiB
Java

package net.minecraft.world.level.entity;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Queues;
import com.google.common.collect.Sets;
import com.mojang.logging.LogUtils;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
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.UncheckedIOException;
import java.io.Writer;
import java.util.List;
import java.util.Queue;
import java.util.Set;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.util.CsvOutput;
import net.minecraft.util.VisibleForDebug;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.ChunkPos;
import org.slf4j.Logger;
public class PersistentEntitySectionManager<T extends EntityAccess> implements AutoCloseable {
static final Logger LOGGER = LogUtils.getLogger();
final Set<UUID> knownUuids = Sets.<UUID>newHashSet();
final LevelCallback<T> callbacks;
private final EntityPersistentStorage<T> permanentStorage;
private final EntityLookup<T> visibleEntityStorage;
final EntitySectionStorage<T> sectionStorage;
private final LevelEntityGetter<T> entityGetter;
private final Long2ObjectMap<Visibility> chunkVisibility = new Long2ObjectOpenHashMap<>();
private final Long2ObjectMap<PersistentEntitySectionManager.ChunkLoadStatus> chunkLoadStatuses = new Long2ObjectOpenHashMap<>();
private final LongSet chunksToUnload = new LongOpenHashSet();
private final Queue<ChunkEntities<T>> loadingInbox = Queues.<ChunkEntities<T>>newConcurrentLinkedQueue();
public PersistentEntitySectionManager(Class<T> entityClass, LevelCallback<T> callbacks, EntityPersistentStorage<T> permanentStorage) {
this.visibleEntityStorage = new EntityLookup<>();
this.sectionStorage = new EntitySectionStorage<>(entityClass, this.chunkVisibility);
this.chunkVisibility.defaultReturnValue(Visibility.HIDDEN);
this.chunkLoadStatuses.defaultReturnValue(PersistentEntitySectionManager.ChunkLoadStatus.FRESH);
this.callbacks = callbacks;
this.permanentStorage = permanentStorage;
this.entityGetter = new LevelEntityGetterAdapter<>(this.visibleEntityStorage, this.sectionStorage);
}
void removeSectionIfEmpty(long sectionKey, EntitySection<T> section) {
if (section.isEmpty()) {
this.sectionStorage.remove(sectionKey);
}
}
private boolean addEntityUuid(T entity) {
if (!this.knownUuids.add(entity.getUUID())) {
LOGGER.warn("UUID of added entity already exists: {}", entity);
return false;
} else {
return true;
}
}
public boolean addNewEntity(T entity) {
return this.addEntity(entity, false);
}
private boolean addEntity(T entity, boolean worldGenSpawned) {
if (!this.addEntityUuid(entity)) {
return false;
} else {
long l = SectionPos.asLong(entity.blockPosition());
EntitySection<T> entitySection = this.sectionStorage.getOrCreateSection(l);
entitySection.add(entity);
entity.setLevelCallback(new PersistentEntitySectionManager.Callback(entity, l, entitySection));
if (!worldGenSpawned) {
this.callbacks.onCreated(entity);
}
Visibility visibility = getEffectiveStatus(entity, entitySection.getStatus());
if (visibility.isAccessible()) {
this.startTracking(entity);
}
if (visibility.isTicking()) {
this.startTicking(entity);
}
return true;
}
}
static <T extends EntityAccess> Visibility getEffectiveStatus(T entity, Visibility visibility) {
return entity.isAlwaysTicking() ? Visibility.TICKING : visibility;
}
public boolean isTicking(ChunkPos chunkPos) {
return this.chunkVisibility.get(chunkPos.toLong()).isTicking();
}
public void addLegacyChunkEntities(Stream<T> entities) {
entities.forEach(entityAccess -> this.addEntity((T)entityAccess, true));
}
public void addWorldGenChunkEntities(Stream<T> entities) {
entities.forEach(entityAccess -> this.addEntity((T)entityAccess, false));
}
void startTicking(T entity) {
this.callbacks.onTickingStart(entity);
}
void stopTicking(T entity) {
this.callbacks.onTickingEnd(entity);
}
void startTracking(T entity) {
this.visibleEntityStorage.add(entity);
this.callbacks.onTrackingStart(entity);
}
void stopTracking(T entity) {
this.callbacks.onTrackingEnd(entity);
this.visibleEntityStorage.remove(entity);
}
public void updateChunkStatus(ChunkPos chunkPos, FullChunkStatus fullChunkStatus) {
Visibility visibility = Visibility.fromFullChunkStatus(fullChunkStatus);
this.updateChunkStatus(chunkPos, visibility);
}
public void updateChunkStatus(ChunkPos pos, Visibility visibility) {
long l = pos.toLong();
if (visibility == Visibility.HIDDEN) {
this.chunkVisibility.remove(l);
this.chunksToUnload.add(l);
} else {
this.chunkVisibility.put(l, visibility);
this.chunksToUnload.remove(l);
this.ensureChunkQueuedForLoad(l);
}
this.sectionStorage.getExistingSectionsInChunk(l).forEach(entitySection -> {
Visibility visibility2 = entitySection.updateChunkStatus(visibility);
boolean bl = visibility2.isAccessible();
boolean bl2 = visibility.isAccessible();
boolean bl3 = visibility2.isTicking();
boolean bl4 = visibility.isTicking();
if (bl3 && !bl4) {
entitySection.getEntities().filter(entityAccess -> !entityAccess.isAlwaysTicking()).forEach(this::stopTicking);
}
if (bl && !bl2) {
entitySection.getEntities().filter(entityAccess -> !entityAccess.isAlwaysTicking()).forEach(this::stopTracking);
} else if (!bl && bl2) {
entitySection.getEntities().filter(entityAccess -> !entityAccess.isAlwaysTicking()).forEach(this::startTracking);
}
if (!bl3 && bl4) {
entitySection.getEntities().filter(entityAccess -> !entityAccess.isAlwaysTicking()).forEach(this::startTicking);
}
});
}
private void ensureChunkQueuedForLoad(long chunkPosValue) {
PersistentEntitySectionManager.ChunkLoadStatus chunkLoadStatus = this.chunkLoadStatuses.get(chunkPosValue);
if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.FRESH) {
this.requestChunkLoad(chunkPosValue);
}
}
private boolean storeChunkSections(long chunkPosValue, Consumer<T> entityAction) {
PersistentEntitySectionManager.ChunkLoadStatus chunkLoadStatus = this.chunkLoadStatuses.get(chunkPosValue);
if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.PENDING) {
return false;
} else {
List<T> list = (List<T>)this.sectionStorage
.getExistingSectionsInChunk(chunkPosValue)
.flatMap(entitySection -> entitySection.getEntities().filter(EntityAccess::shouldBeSaved))
.collect(Collectors.toList());
if (list.isEmpty()) {
if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.LOADED) {
this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(chunkPosValue), ImmutableList.of()));
}
return true;
} else if (chunkLoadStatus == PersistentEntitySectionManager.ChunkLoadStatus.FRESH) {
this.requestChunkLoad(chunkPosValue);
return false;
} else {
this.permanentStorage.storeEntities(new ChunkEntities<>(new ChunkPos(chunkPosValue), list));
list.forEach(entityAction);
return true;
}
}
}
private void requestChunkLoad(long chunkPosValue) {
this.chunkLoadStatuses.put(chunkPosValue, PersistentEntitySectionManager.ChunkLoadStatus.PENDING);
ChunkPos chunkPos = new ChunkPos(chunkPosValue);
this.permanentStorage.loadEntities(chunkPos).thenAccept(this.loadingInbox::add).exceptionally(throwable -> {
LOGGER.error("Failed to read chunk {}", chunkPos, throwable);
return null;
});
}
private boolean processChunkUnload(long chunkPosValue) {
boolean bl = this.storeChunkSections(chunkPosValue, entityAccess -> entityAccess.getPassengersAndSelf().forEach(this::unloadEntity));
if (!bl) {
return false;
} else {
this.chunkLoadStatuses.remove(chunkPosValue);
return true;
}
}
private void unloadEntity(EntityAccess entity) {
entity.setRemoved(Entity.RemovalReason.UNLOADED_TO_CHUNK);
entity.setLevelCallback(EntityInLevelCallback.NULL);
}
private void processUnloads() {
this.chunksToUnload.removeIf(l -> this.chunkVisibility.get(l) != Visibility.HIDDEN ? true : this.processChunkUnload(l));
}
private void processPendingLoads() {
ChunkEntities<T> chunkEntities;
while ((chunkEntities = (ChunkEntities<T>)this.loadingInbox.poll()) != null) {
chunkEntities.getEntities().forEach(entityAccess -> this.addEntity((T)entityAccess, true));
this.chunkLoadStatuses.put(chunkEntities.getPos().toLong(), PersistentEntitySectionManager.ChunkLoadStatus.LOADED);
}
}
public void tick() {
this.processPendingLoads();
this.processUnloads();
}
private LongSet getAllChunksToSave() {
LongSet longSet = this.sectionStorage.getAllChunksWithExistingSections();
for (Entry<PersistentEntitySectionManager.ChunkLoadStatus> entry : Long2ObjectMaps.fastIterable(this.chunkLoadStatuses)) {
if (entry.getValue() == PersistentEntitySectionManager.ChunkLoadStatus.LOADED) {
longSet.add(entry.getLongKey());
}
}
return longSet;
}
public void autoSave() {
this.getAllChunksToSave().forEach(l -> {
boolean bl = this.chunkVisibility.get(l) == Visibility.HIDDEN;
if (bl) {
this.processChunkUnload(l);
} else {
this.storeChunkSections(l, entityAccess -> {});
}
});
}
public void saveAll() {
LongSet longSet = this.getAllChunksToSave();
while (!longSet.isEmpty()) {
this.permanentStorage.flush(false);
this.processPendingLoads();
longSet.removeIf(l -> {
boolean bl = this.chunkVisibility.get(l) == Visibility.HIDDEN;
return bl ? this.processChunkUnload(l) : this.storeChunkSections(l, entityAccess -> {});
});
}
this.permanentStorage.flush(true);
}
public void close() throws IOException {
this.saveAll();
this.permanentStorage.close();
}
public boolean isLoaded(UUID uuid) {
return this.knownUuids.contains(uuid);
}
public LevelEntityGetter<T> getEntityGetter() {
return this.entityGetter;
}
public boolean canPositionTick(BlockPos pos) {
return this.chunkVisibility.get(ChunkPos.asLong(pos)).isTicking();
}
public boolean canPositionTick(ChunkPos chunkPos) {
return this.chunkVisibility.get(chunkPos.toLong()).isTicking();
}
public boolean areEntitiesLoaded(long chunkPos) {
return this.chunkLoadStatuses.get(chunkPos) == PersistentEntitySectionManager.ChunkLoadStatus.LOADED;
}
public void dumpSections(Writer writer) throws IOException {
CsvOutput csvOutput = CsvOutput.builder()
.addColumn("x")
.addColumn("y")
.addColumn("z")
.addColumn("visibility")
.addColumn("load_status")
.addColumn("entity_count")
.build(writer);
this.sectionStorage.getAllChunksWithExistingSections().forEach(l -> {
PersistentEntitySectionManager.ChunkLoadStatus chunkLoadStatus = this.chunkLoadStatuses.get(l);
this.sectionStorage.getExistingSectionPositionsInChunk(l).forEach(lx -> {
EntitySection<T> entitySection = this.sectionStorage.getSection(lx);
if (entitySection != null) {
try {
csvOutput.writeRow(SectionPos.x(lx), SectionPos.y(lx), SectionPos.z(lx), entitySection.getStatus(), chunkLoadStatus, entitySection.size());
} catch (IOException var7) {
throw new UncheckedIOException(var7);
}
}
});
});
}
@VisibleForDebug
public String gatherStats() {
return this.knownUuids.size()
+ ","
+ this.visibleEntityStorage.count()
+ ","
+ this.sectionStorage.count()
+ ","
+ this.chunkLoadStatuses.size()
+ ","
+ this.chunkVisibility.size()
+ ","
+ this.loadingInbox.size()
+ ","
+ this.chunksToUnload.size();
}
@VisibleForDebug
public int count() {
return this.visibleEntityStorage.count();
}
class Callback implements EntityInLevelCallback {
private final T entity;
private long currentSectionKey;
private EntitySection<T> currentSection;
Callback(final T entity, final long currentSectionKey, final EntitySection<T> currentSection) {
this.entity = entity;
this.currentSectionKey = currentSectionKey;
this.currentSection = currentSection;
}
@Override
public void onMove() {
BlockPos blockPos = this.entity.blockPosition();
long l = SectionPos.asLong(blockPos);
if (l != this.currentSectionKey) {
Visibility visibility = this.currentSection.getStatus();
if (!this.currentSection.remove(this.entity)) {
PersistentEntitySectionManager.LOGGER.warn("Entity {} wasn't found in section {} (moving to {})", this.entity, SectionPos.of(this.currentSectionKey), l);
}
PersistentEntitySectionManager.this.removeSectionIfEmpty(this.currentSectionKey, this.currentSection);
EntitySection<T> entitySection = PersistentEntitySectionManager.this.sectionStorage.getOrCreateSection(l);
entitySection.add(this.entity);
this.currentSection = entitySection;
this.currentSectionKey = l;
this.updateStatus(visibility, entitySection.getStatus());
}
}
private void updateStatus(Visibility oldVisibility, Visibility newVisibility) {
Visibility visibility = PersistentEntitySectionManager.getEffectiveStatus(this.entity, oldVisibility);
Visibility visibility2 = PersistentEntitySectionManager.getEffectiveStatus(this.entity, newVisibility);
if (visibility == visibility2) {
if (visibility2.isAccessible()) {
PersistentEntitySectionManager.this.callbacks.onSectionChange(this.entity);
}
} else {
boolean bl = visibility.isAccessible();
boolean bl2 = visibility2.isAccessible();
if (bl && !bl2) {
PersistentEntitySectionManager.this.stopTracking(this.entity);
} else if (!bl && bl2) {
PersistentEntitySectionManager.this.startTracking(this.entity);
}
boolean bl3 = visibility.isTicking();
boolean bl4 = visibility2.isTicking();
if (bl3 && !bl4) {
PersistentEntitySectionManager.this.stopTicking(this.entity);
} else if (!bl3 && bl4) {
PersistentEntitySectionManager.this.startTicking(this.entity);
}
if (bl2) {
PersistentEntitySectionManager.this.callbacks.onSectionChange(this.entity);
}
}
}
@Override
public void onRemove(Entity.RemovalReason reason) {
if (!this.currentSection.remove(this.entity)) {
PersistentEntitySectionManager.LOGGER
.warn("Entity {} wasn't found in section {} (destroying due to {})", this.entity, SectionPos.of(this.currentSectionKey), reason);
}
Visibility visibility = PersistentEntitySectionManager.getEffectiveStatus(this.entity, this.currentSection.getStatus());
if (visibility.isTicking()) {
PersistentEntitySectionManager.this.stopTicking(this.entity);
}
if (visibility.isAccessible()) {
PersistentEntitySectionManager.this.stopTracking(this.entity);
}
if (reason.shouldDestroy()) {
PersistentEntitySectionManager.this.callbacks.onDestroyed(this.entity);
}
PersistentEntitySectionManager.this.knownUuids.remove(this.entity.getUUID());
this.entity.setLevelCallback(NULL);
PersistentEntitySectionManager.this.removeSectionIfEmpty(this.currentSectionKey, this.currentSection);
}
}
static enum ChunkLoadStatus {
FRESH,
PENDING,
LOADED;
}
}