445 lines
		
	
	
	
		
			15 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			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));
 | |
| 	}
 | |
| 
 | |
| 	public 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;
 | |
| 	}
 | |
| }
 |