328 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			328 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| package net.minecraft.server.level;
 | |
| 
 | |
| import com.mojang.logging.LogUtils;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ByteMap;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ByteMaps;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap;
 | |
| import it.unimi.dsi.fastutil.longs.Long2IntMap;
 | |
| import it.unimi.dsi.fastutil.longs.Long2IntMaps;
 | |
| import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ObjectFunction;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
 | |
| import it.unimi.dsi.fastutil.longs.LongConsumer;
 | |
| import it.unimi.dsi.fastutil.longs.LongIterator;
 | |
| import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
 | |
| import it.unimi.dsi.fastutil.longs.LongSet;
 | |
| import it.unimi.dsi.fastutil.longs.Long2ByteMap.Entry;
 | |
| import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
 | |
| import it.unimi.dsi.fastutil.objects.ObjectSet;
 | |
| import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
 | |
| import java.util.Set;
 | |
| import java.util.concurrent.CompletableFuture;
 | |
| import java.util.concurrent.Executor;
 | |
| import net.minecraft.core.SectionPos;
 | |
| import net.minecraft.util.TriState;
 | |
| import net.minecraft.util.thread.TaskScheduler;
 | |
| import net.minecraft.world.level.ChunkPos;
 | |
| import net.minecraft.world.level.NaturalSpawner;
 | |
| import net.minecraft.world.level.TicketStorage;
 | |
| import net.minecraft.world.level.chunk.LevelChunk;
 | |
| import org.jetbrains.annotations.Nullable;
 | |
| import org.slf4j.Logger;
 | |
| 
 | |
| public abstract class DistanceManager {
 | |
| 	private static final Logger LOGGER = LogUtils.getLogger();
 | |
| 	static final int PLAYER_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING);
 | |
| 	final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap<>();
 | |
| 	private final LoadingChunkTracker loadingChunkTracker;
 | |
| 	private final SimulationChunkTracker simulationChunkTracker;
 | |
| 	final TicketStorage ticketStorage;
 | |
| 	private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
 | |
| 	private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(32);
 | |
| 	protected final Set<ChunkHolder> chunksToUpdateFutures = new ReferenceOpenHashSet<>();
 | |
| 	final ThrottlingChunkTaskDispatcher ticketDispatcher;
 | |
| 	final LongSet ticketsToRelease = new LongOpenHashSet();
 | |
| 	final Executor mainThreadExecutor;
 | |
| 	private int simulationDistance = 10;
 | |
| 
 | |
| 	protected DistanceManager(TicketStorage ticketStorage, Executor dispatcher, Executor mainThreadExecutor) {
 | |
| 		this.ticketStorage = ticketStorage;
 | |
| 		this.loadingChunkTracker = new LoadingChunkTracker(this, ticketStorage);
 | |
| 		this.simulationChunkTracker = new SimulationChunkTracker(ticketStorage);
 | |
| 		TaskScheduler<Runnable> taskScheduler = TaskScheduler.wrapExecutor("player ticket throttler", mainThreadExecutor);
 | |
| 		this.ticketDispatcher = new ThrottlingChunkTaskDispatcher(taskScheduler, dispatcher, 4);
 | |
| 		this.mainThreadExecutor = mainThreadExecutor;
 | |
| 	}
 | |
| 
 | |
| 	protected abstract boolean isChunkToRemove(long chunkPos);
 | |
| 
 | |
| 	@Nullable
 | |
| 	protected abstract ChunkHolder getChunk(long chunkPos);
 | |
| 
 | |
| 	@Nullable
 | |
| 	protected abstract ChunkHolder updateChunkScheduling(long chunkPos, int newLevel, @Nullable ChunkHolder holder, int oldLevel);
 | |
| 
 | |
| 	public boolean runAllUpdates(ChunkMap chunkMap) {
 | |
| 		this.naturalSpawnChunkCounter.runAllUpdates();
 | |
| 		this.simulationChunkTracker.runAllUpdates();
 | |
| 		this.playerTicketManager.runAllUpdates();
 | |
| 		int i = Integer.MAX_VALUE - this.loadingChunkTracker.runDistanceUpdates(Integer.MAX_VALUE);
 | |
| 		boolean bl = i != 0;
 | |
| 		if (bl) {
 | |
| 		}
 | |
| 
 | |
| 		if (!this.chunksToUpdateFutures.isEmpty()) {
 | |
| 			for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
 | |
| 				chunkHolder.updateHighestAllowedStatus(chunkMap);
 | |
| 			}
 | |
| 
 | |
| 			for (ChunkHolder chunkHolder : this.chunksToUpdateFutures) {
 | |
| 				chunkHolder.updateFutures(chunkMap, this.mainThreadExecutor);
 | |
| 			}
 | |
| 
 | |
| 			this.chunksToUpdateFutures.clear();
 | |
| 			return true;
 | |
| 		} else {
 | |
| 			if (!this.ticketsToRelease.isEmpty()) {
 | |
| 				LongIterator longIterator = this.ticketsToRelease.iterator();
 | |
| 
 | |
| 				while (longIterator.hasNext()) {
 | |
| 					long l = longIterator.nextLong();
 | |
| 					if (this.ticketStorage.getTickets(l).stream().anyMatch(ticket -> ticket.getType() == TicketType.PLAYER_LOADING)) {
 | |
| 						ChunkHolder chunkHolder2 = chunkMap.getUpdatingChunkIfPresent(l);
 | |
| 						if (chunkHolder2 == null) {
 | |
| 							throw new IllegalStateException();
 | |
| 						}
 | |
| 
 | |
| 						CompletableFuture<ChunkResult<LevelChunk>> completableFuture = chunkHolder2.getEntityTickingChunkFuture();
 | |
| 						completableFuture.thenAccept(chunkResult -> this.mainThreadExecutor.execute(() -> this.ticketDispatcher.release(l, () -> {}, false)));
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				this.ticketsToRelease.clear();
 | |
| 			}
 | |
| 
 | |
| 			return bl;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public void addPlayer(SectionPos sectionPos, ServerPlayer player) {
 | |
| 		ChunkPos chunkPos = sectionPos.chunk();
 | |
| 		long l = chunkPos.toLong();
 | |
| 		this.playersPerChunk.computeIfAbsent(l, (Long2ObjectFunction<? extends ObjectSet<ServerPlayer>>)(lx -> new ObjectOpenHashSet<>())).add(player);
 | |
| 		this.naturalSpawnChunkCounter.update(l, 0, true);
 | |
| 		this.playerTicketManager.update(l, 0, true);
 | |
| 		this.ticketStorage.addTicket(new Ticket(TicketType.PLAYER_SIMULATION, this.getPlayerTicketLevel()), chunkPos);
 | |
| 	}
 | |
| 
 | |
| 	public void removePlayer(SectionPos sectionPos, ServerPlayer player) {
 | |
| 		ChunkPos chunkPos = sectionPos.chunk();
 | |
| 		long l = chunkPos.toLong();
 | |
| 		ObjectSet<ServerPlayer> objectSet = this.playersPerChunk.get(l);
 | |
| 		objectSet.remove(player);
 | |
| 		if (objectSet.isEmpty()) {
 | |
| 			this.playersPerChunk.remove(l);
 | |
| 			this.naturalSpawnChunkCounter.update(l, Integer.MAX_VALUE, false);
 | |
| 			this.playerTicketManager.update(l, Integer.MAX_VALUE, false);
 | |
| 			this.ticketStorage.removeTicket(new Ticket(TicketType.PLAYER_SIMULATION, this.getPlayerTicketLevel()), chunkPos);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	private int getPlayerTicketLevel() {
 | |
| 		return Math.max(0, ChunkLevel.byStatus(FullChunkStatus.ENTITY_TICKING) - this.simulationDistance);
 | |
| 	}
 | |
| 
 | |
| 	public boolean inEntityTickingRange(long chunkPos) {
 | |
| 		return ChunkLevel.isEntityTicking(this.simulationChunkTracker.getLevel(chunkPos));
 | |
| 	}
 | |
| 
 | |
| 	public boolean inBlockTickingRange(long chunkPos) {
 | |
| 		return ChunkLevel.isBlockTicking(this.simulationChunkTracker.getLevel(chunkPos));
 | |
| 	}
 | |
| 
 | |
| 	public int getChunkLevel(long chunkPos, boolean simulate) {
 | |
| 		return simulate ? this.simulationChunkTracker.getLevel(chunkPos) : this.loadingChunkTracker.getLevel(chunkPos);
 | |
| 	}
 | |
| 
 | |
| 	protected void updatePlayerTickets(int viewDistance) {
 | |
| 		this.playerTicketManager.updateViewDistance(viewDistance);
 | |
| 	}
 | |
| 
 | |
| 	public void updateSimulationDistance(int simulationDistance) {
 | |
| 		if (simulationDistance != this.simulationDistance) {
 | |
| 			this.simulationDistance = simulationDistance;
 | |
| 			this.ticketStorage.replaceTicketLevelOfType(this.getPlayerTicketLevel(), TicketType.PLAYER_SIMULATION);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Returns the number of chunks taken into account when calculating the mob cap
 | |
| 	 */
 | |
| 	public int getNaturalSpawnChunkCount() {
 | |
| 		this.naturalSpawnChunkCounter.runAllUpdates();
 | |
| 		return this.naturalSpawnChunkCounter.chunks.size();
 | |
| 	}
 | |
| 
 | |
| 	public TriState hasPlayersNearby(long chunkPos) {
 | |
| 		this.naturalSpawnChunkCounter.runAllUpdates();
 | |
| 		int i = this.naturalSpawnChunkCounter.getLevel(chunkPos);
 | |
| 		if (i <= NaturalSpawner.INSCRIBED_SQUARE_SPAWN_DISTANCE_CHUNK) {
 | |
| 			return TriState.TRUE;
 | |
| 		} else {
 | |
| 			return i > 8 ? TriState.FALSE : TriState.DEFAULT;
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public void forEachEntityTickingChunk(LongConsumer action) {
 | |
| 		for (Entry entry : Long2ByteMaps.fastIterable(this.simulationChunkTracker.chunks)) {
 | |
| 			byte b = entry.getByteValue();
 | |
| 			long l = entry.getLongKey();
 | |
| 			if (ChunkLevel.isEntityTicking(b)) {
 | |
| 				action.accept(l);
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	public LongIterator getSpawnCandidateChunks() {
 | |
| 		this.naturalSpawnChunkCounter.runAllUpdates();
 | |
| 		return this.naturalSpawnChunkCounter.chunks.keySet().iterator();
 | |
| 	}
 | |
| 
 | |
| 	public String getDebugStatus() {
 | |
| 		return this.ticketDispatcher.getDebugStatus();
 | |
| 	}
 | |
| 
 | |
| 	public boolean hasTickets() {
 | |
| 		return this.ticketStorage.hasTickets();
 | |
| 	}
 | |
| 
 | |
| 	class FixedPlayerDistanceChunkTracker extends ChunkTracker {
 | |
| 		/**
 | |
| 		 * Chunks that are at most {@link #range} chunks away from the closest player.
 | |
| 		 */
 | |
| 		protected final Long2ByteMap chunks = new Long2ByteOpenHashMap();
 | |
| 		protected final int maxDistance;
 | |
| 
 | |
| 		protected FixedPlayerDistanceChunkTracker(final int maxDistance) {
 | |
| 			super(maxDistance + 2, 16, 256);
 | |
| 			this.maxDistance = maxDistance;
 | |
| 			this.chunks.defaultReturnValue((byte)(maxDistance + 2));
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		protected int getLevel(long chunkPos) {
 | |
| 			return this.chunks.get(chunkPos);
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		protected void setLevel(long chunkPos, int level) {
 | |
| 			byte b;
 | |
| 			if (level > this.maxDistance) {
 | |
| 				b = this.chunks.remove(chunkPos);
 | |
| 			} else {
 | |
| 				b = this.chunks.put(chunkPos, (byte)level);
 | |
| 			}
 | |
| 
 | |
| 			this.onLevelChange(chunkPos, b, level);
 | |
| 		}
 | |
| 
 | |
| 		/**
 | |
| 		 * Called after {@link PlayerChunkTracker#setLevel(long, int)} puts/removes chunk into/from {@link #chunksInRange}.
 | |
| 		 * 
 | |
| 		 * @param oldLevel Previous level of the chunk if it was smaller than {@link #range}, {@code range + 2} otherwise.
 | |
| 		 */
 | |
| 		protected void onLevelChange(long chunkPos, int oldLevel, int newLevel) {
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		protected int getLevelFromSource(long pos) {
 | |
| 			return this.havePlayer(pos) ? 0 : Integer.MAX_VALUE;
 | |
| 		}
 | |
| 
 | |
| 		private boolean havePlayer(long chunkPos) {
 | |
| 			ObjectSet<ServerPlayer> objectSet = DistanceManager.this.playersPerChunk.get(chunkPos);
 | |
| 			return objectSet != null && !objectSet.isEmpty();
 | |
| 		}
 | |
| 
 | |
| 		public void runAllUpdates() {
 | |
| 			this.runUpdates(Integer.MAX_VALUE);
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	class PlayerTicketTracker extends DistanceManager.FixedPlayerDistanceChunkTracker {
 | |
| 		private int viewDistance;
 | |
| 		private final Long2IntMap queueLevels = Long2IntMaps.synchronize(new Long2IntOpenHashMap());
 | |
| 		private final LongSet toUpdate = new LongOpenHashSet();
 | |
| 
 | |
| 		protected PlayerTicketTracker(final int maxDistance) {
 | |
| 			super(maxDistance);
 | |
| 			this.viewDistance = 0;
 | |
| 			this.queueLevels.defaultReturnValue(maxDistance + 2);
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		protected void onLevelChange(long chunkPos, int oldLevel, int newLevel) {
 | |
| 			this.toUpdate.add(chunkPos);
 | |
| 		}
 | |
| 
 | |
| 		public void updateViewDistance(int viewDistance) {
 | |
| 			for (Entry entry : this.chunks.long2ByteEntrySet()) {
 | |
| 				byte b = entry.getByteValue();
 | |
| 				long l = entry.getLongKey();
 | |
| 				this.onLevelChange(l, b, this.haveTicketFor(b), b <= viewDistance);
 | |
| 			}
 | |
| 
 | |
| 			this.viewDistance = viewDistance;
 | |
| 		}
 | |
| 
 | |
| 		private void onLevelChange(long chunkPos, int level, boolean hadTicket, boolean hasTicket) {
 | |
| 			if (hadTicket != hasTicket) {
 | |
| 				Ticket ticket = new Ticket(TicketType.PLAYER_LOADING, DistanceManager.PLAYER_TICKET_LEVEL);
 | |
| 				if (hasTicket) {
 | |
| 					DistanceManager.this.ticketDispatcher.submit(() -> DistanceManager.this.mainThreadExecutor.execute(() -> {
 | |
| 						if (this.haveTicketFor(this.getLevel(chunkPos))) {
 | |
| 							DistanceManager.this.ticketStorage.addTicket(chunkPos, ticket);
 | |
| 							DistanceManager.this.ticketsToRelease.add(chunkPos);
 | |
| 						} else {
 | |
| 							DistanceManager.this.ticketDispatcher.release(chunkPos, () -> {}, false);
 | |
| 						}
 | |
| 					}), chunkPos, () -> level);
 | |
| 				} else {
 | |
| 					DistanceManager.this.ticketDispatcher
 | |
| 						.release(chunkPos, () -> DistanceManager.this.mainThreadExecutor.execute(() -> DistanceManager.this.ticketStorage.removeTicket(chunkPos, ticket)), true);
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		@Override
 | |
| 		public void runAllUpdates() {
 | |
| 			super.runAllUpdates();
 | |
| 			if (!this.toUpdate.isEmpty()) {
 | |
| 				LongIterator longIterator = this.toUpdate.iterator();
 | |
| 
 | |
| 				while (longIterator.hasNext()) {
 | |
| 					long l = longIterator.nextLong();
 | |
| 					int i = this.queueLevels.get(l);
 | |
| 					int j = this.getLevel(l);
 | |
| 					if (i != j) {
 | |
| 						DistanceManager.this.ticketDispatcher.onLevelChange(new ChunkPos(l), () -> this.queueLevels.get(l), j, ix -> {
 | |
| 							if (ix >= this.queueLevels.defaultReturnValue()) {
 | |
| 								this.queueLevels.remove(l);
 | |
| 							} else {
 | |
| 								this.queueLevels.put(l, ix);
 | |
| 							}
 | |
| 						});
 | |
| 						this.onLevelChange(l, j, this.haveTicketFor(i), this.haveTicketFor(j));
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				this.toUpdate.clear();
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		private boolean haveTicketFor(int level) {
 | |
| 			return level <= this.viewDistance;
 | |
| 		}
 | |
| 	}
 | |
| }
 |