package net.minestom.server.instance; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.minestom.server.MinecraftServer; import net.minestom.server.ServerFlag; import net.minestom.server.Viewable; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.entity.Entity; import net.minestom.server.entity.Player; import net.minestom.server.utils.chunk.ChunkUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.UnmodifiableView; import space.vectrix.flare.fastutil.Int2ObjectSyncMap; import space.vectrix.flare.fastutil.Long2ObjectSyncMap; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import static net.minestom.server.instance.Chunk.CHUNK_SIZE_X; import static net.minestom.server.instance.Chunk.CHUNK_SIZE_Z; import static net.minestom.server.utils.chunk.ChunkUtils.*; final class EntityTrackerImpl implements EntityTracker { static final AtomicInteger TARGET_COUNTER = new AtomicInteger(); // Store all data associated to a Target // The array index is the Target enum ordinal final TargetEntry[] entries = EntityTracker.Target.TARGETS.stream().map((Function, TargetEntry>) TargetEntry::new).toArray(TargetEntry[]::new); private final Int2ObjectSyncMap entityPositions = Int2ObjectSyncMap.hashmap(); @Override public void register(@NotNull Entity entity, @NotNull Point point, @NotNull Target target, @Nullable Update update) { var prevPoint = entityPositions.putIfAbsent(entity.getEntityId(), point); if (prevPoint != null) return; final long index = getChunkIndex(point); for (TargetEntry entry : entries) { if (entry.target.type().isInstance(entity)) { entry.entities.add(entity); entry.addToChunk(index, entity); } } if (update != null) { update.referenceUpdate(point, this); nearbyEntitiesByChunkRange(point, ServerFlag.ENTITY_VIEW_DISTANCE, target, newEntity -> { if (newEntity == entity) return; update.add(newEntity); }); } } @Override public void unregister(@NotNull Entity entity, @NotNull Target target, @Nullable Update update) { final Point point = entityPositions.remove(entity.getEntityId()); if (point == null) return; final long index = getChunkIndex(point); for (TargetEntry entry : entries) { if (entry.target.type().isInstance(entity)) { entry.entities.remove(entity); entry.removeFromChunk(index, entity); } } if (update != null) { update.referenceUpdate(point, null); nearbyEntitiesByChunkRange(point, ServerFlag.ENTITY_VIEW_DISTANCE, target, newEntity -> { if (newEntity == entity) return; update.remove(newEntity); }); } } @Override public void move(@NotNull Entity entity, @NotNull Point newPoint, @NotNull Target target, @Nullable Update update) { Point oldPoint = entityPositions.put(entity.getEntityId(), newPoint); if (oldPoint == null || oldPoint.sameChunk(newPoint)) return; final long oldIndex = getChunkIndex(oldPoint); final long newIndex = getChunkIndex(newPoint); for (TargetEntry entry : entries) { if (entry.target.type().isInstance(entity)) { entry.addToChunk(newIndex, entity); entry.removeFromChunk(oldIndex, entity); } } if (update != null) { difference(oldPoint, newPoint, target, new Update<>() { @Override public void add(@NotNull T added) { if (entity != added) update.add(added); } @Override public void remove(@NotNull T removed) { if (entity != removed) update.remove(removed); } }); update.referenceUpdate(newPoint, this); } } @Override public @Unmodifiable Collection chunkEntities(int chunkX, int chunkZ, @NotNull Target target) { final TargetEntry entry = entries[target.ordinal()]; //noinspection unchecked var chunkEntities = (List) entry.chunkEntities(getChunkIndex(chunkX, chunkZ)); return Collections.unmodifiableList(chunkEntities); } @Override public void nearbyEntitiesByChunkRange(@NotNull Point point, int chunkRange, @NotNull Target target, @NotNull Consumer query) { final Long2ObjectSyncMap> entities = entries[target.ordinal()].chunkEntities; if (chunkRange == 0) { // Single chunk final var chunkEntities = (List) entities.get(getChunkIndex(point)); if (chunkEntities != null && !chunkEntities.isEmpty()) { chunkEntities.forEach(query); } } else { // Multiple chunks forChunksInRange(point, chunkRange, (chunkX, chunkZ) -> { final var chunkEntities = (List) entities.get(getChunkIndex(chunkX, chunkZ)); if (chunkEntities == null || chunkEntities.isEmpty()) return; chunkEntities.forEach(query); }); } } @Override public void nearbyEntities(@NotNull Point point, double range, @NotNull Target target, @NotNull Consumer query) { final Long2ObjectSyncMap> entities = entries[target.ordinal()].chunkEntities; final int minChunkX = ChunkUtils.getChunkCoordinate(point.x() - range); final int minChunkZ = ChunkUtils.getChunkCoordinate(point.z() - range); final int maxChunkX = ChunkUtils.getChunkCoordinate(point.x() + range); final int maxChunkZ = ChunkUtils.getChunkCoordinate(point.z() + range); final double squaredRange = range * range; if (minChunkX == maxChunkX && minChunkZ == maxChunkZ) { // Single chunk final var chunkEntities = (List) entities.get(getChunkIndex(point)); if (chunkEntities != null && !chunkEntities.isEmpty()) { chunkEntities.forEach(entity -> { final Point position = entityPositions.get(entity.getEntityId()); if (point.distanceSquared(position) <= squaredRange) query.accept(entity); }); } } else { // Multiple chunks final int chunkRange = (int) (range / Chunk.CHUNK_SECTION_SIZE) + 1; forChunksInRange(point, chunkRange, (chunkX, chunkZ) -> { final var chunkEntities = (List) entities.get(getChunkIndex(chunkX, chunkZ)); if (chunkEntities == null || chunkEntities.isEmpty()) return; chunkEntities.forEach(entity -> { final Point position = entityPositions.get(entity.getEntityId()); if (point.distanceSquared(position) <= squaredRange) { query.accept(entity); } }); }); } } @Override public @UnmodifiableView @NotNull Set<@NotNull T> entities(@NotNull Target target) { //noinspection unchecked return (Set) entries[target.ordinal()].entitiesView; } @Override public @NotNull Viewable viewable(@NotNull List<@NotNull SharedInstance> sharedInstances, int chunkX, int chunkZ) { var entry = entries[Target.PLAYERS.ordinal()]; return entry.viewers.computeIfAbsent(new ChunkViewKey(sharedInstances, chunkX, chunkZ), ChunkView::new); } private void difference(Point oldPoint, Point newPoint, @NotNull Target target, @NotNull Update update) { final TargetEntry entry = entries[target.ordinal()]; forDifferingChunksInRange(newPoint.chunkX(), newPoint.chunkZ(), oldPoint.chunkX(), oldPoint.chunkZ(), ServerFlag.ENTITY_VIEW_DISTANCE, (chunkX, chunkZ) -> { // Add final List entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ)); if (entities == null || entities.isEmpty()) return; for (Entity entity : entities) update.add((T) entity); }, (chunkX, chunkZ) -> { // Remove final List entities = entry.chunkEntities.get(getChunkIndex(chunkX, chunkZ)); if (entities == null || entities.isEmpty()) return; for (Entity entity : entities) update.remove((T) entity); }); } record ChunkViewKey(List sharedInstances, int chunkX, int chunkZ) { @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof ChunkViewKey key)) return false; return sharedInstances == key.sharedInstances && chunkX == key.chunkX && chunkZ == key.chunkZ; } } static final class TargetEntry { private final EntityTracker.Target target; private final Set entities = ConcurrentHashMap.newKeySet(); // Thread-safe since exposed private final Set entitiesView = Collections.unmodifiableSet(entities); // Chunk index -> entities inside it final Long2ObjectSyncMap> chunkEntities = Long2ObjectSyncMap.hashmap(); final Map viewers = new ConcurrentHashMap<>(); TargetEntry(Target target) { this.target = target; } List chunkEntities(long index) { return chunkEntities.computeIfAbsent(index, i -> (List) new CopyOnWriteArrayList()); } void addToChunk(long index, T entity) { chunkEntities(index).add(entity); } void removeFromChunk(long index, T entity) { List entities = chunkEntities.get(index); if (entities != null) entities.remove(entity); } } private final class ChunkView implements Viewable { private final ChunkViewKey key; private final int chunkX, chunkZ; private final Point point; final Set set = new SetImpl(); private int lastReferenceCount; private ChunkView(ChunkViewKey key) { this.key = key; this.chunkX = key.chunkX; this.chunkZ = key.chunkZ; this.point = new Vec(CHUNK_SIZE_X * chunkX, 0, CHUNK_SIZE_Z * chunkZ); } @Override public boolean addViewer(@NotNull Player player) { throw new UnsupportedOperationException("Chunk does not support manual viewers"); } @Override public boolean removeViewer(@NotNull Player player) { throw new UnsupportedOperationException("Chunk does not support manual viewers"); } @Override public @NotNull Set<@NotNull Player> getViewers() { return set; } private Collection references() { Int2ObjectOpenHashMap entityMap = new Int2ObjectOpenHashMap<>(lastReferenceCount); collectPlayers(EntityTrackerImpl.this, entityMap); if (!key.sharedInstances.isEmpty()) { for (SharedInstance instance : key.sharedInstances) { collectPlayers(instance.getEntityTracker(), entityMap); } } this.lastReferenceCount = entityMap.size(); return entityMap.values(); } private void collectPlayers(EntityTracker tracker, Int2ObjectOpenHashMap map) { tracker.nearbyEntitiesByChunkRange(point, ServerFlag.CHUNK_VIEW_DISTANCE, EntityTracker.Target.PLAYERS, (player) -> map.putIfAbsent(player.getEntityId(), player)); } final class SetImpl extends AbstractSet { @Override public @NotNull Iterator iterator() { return references().iterator(); } @Override public int size() { return references().size(); } @Override public void forEach(Consumer action) { references().forEach(action); } } } }