All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.minestom.server.instance.EntityTrackerImpl Maven / Gradle / Ivy

There is a newer version: 7320437640
Show newest version
package net.minestom.server.instance;

import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.ServerFlag;
import net.minestom.server.Viewable;
import net.minestom.server.coordinate.ChunkRange;
import net.minestom.server.coordinate.CoordConversion;
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.validate.Check;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import org.jetbrains.annotations.UnmodifiableView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

final class EntityTrackerImpl implements EntityTracker {
    private static final Logger LOGGER = LoggerFactory.getLogger(EntityTrackerImpl.class);

    static final AtomicInteger TARGET_COUNTER = new AtomicInteger();

    // Store all data associated to a Target
    // The array index is the Target enum ordinal
    final TargetEntry[] targetEntries = EntityTracker.Target.TARGETS.stream().map((Function, TargetEntry>) TargetEntry::new).toArray(TargetEntry[]::new);

    private final Int2ObjectSyncMap entriesByEntityId = Int2ObjectSyncMap.hashmap();
    private final Map entriesByEntityUuid = new ConcurrentHashMap<>();

    @Override
    public  void register(@NotNull Entity entity, @NotNull Point point,
                                            @NotNull Target target, @Nullable Update update) {
        EntityTrackerEntry newEntry = new EntityTrackerEntry(entity, point);

        EntityTrackerEntry prevEntryWithId = entriesByEntityId.putIfAbsent(entity.getEntityId(), newEntry);
        Check.isTrue(prevEntryWithId == null, "There is already an entity registered with id {0}", entity.getEntityId());
        EntityTrackerEntry prevEntryWithUuid = entriesByEntityUuid.putIfAbsent(entity.getUuid(), newEntry);
        Check.isTrue(prevEntryWithUuid == null, "There is already an entity registered with uuid {0}", entity.getUuid());

        final long index = CoordConversion.chunkIndex(point);
        for (TargetEntry targetEntry : targetEntries) {
            if (targetEntry.target.type().isInstance(entity)) {
                targetEntry.entities.add(entity);
                targetEntry.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) {
        EntityTrackerEntry entry = entriesByEntityId.remove(entity.getEntityId());
        entriesByEntityUuid.remove(entity.getUuid());
        final Point point = entry == null ? null : entry.getLastPosition();
        if (point == null) return;

        final long index = CoordConversion.chunkIndex(point);
        for (TargetEntry targetEntry : targetEntries) {
            if (targetEntry.target.type().isInstance(entity)) {
                targetEntry.entities.remove(entity);
                targetEntry.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 @Nullable Entity getEntityById(int id) {
        EntityTrackerEntry entry = entriesByEntityId.get(id);
        return entry == null ? null : entry.getEntity();
    }

    @Override
    public @Nullable Entity getEntityByUuid(UUID uuid) {
        EntityTrackerEntry entry = entriesByEntityUuid.get(uuid);
        return entry == null ? null : entry.getEntity();
    }

    @Override
    public  void move(@NotNull Entity entity, @NotNull Point newPoint,
                                        @NotNull Target target, @Nullable Update update) {
        EntityTrackerEntry entry = entriesByEntityId.get(entity.getEntityId());
        if (entry == null) {
            LOGGER.warn("Attempted to move unregistered entity {} in the entity tracker", entity.getEntityId());
            return;
        }
        Point oldPoint = entry.getLastPosition();
        entry.setLastPosition(newPoint);
        if (oldPoint == null || oldPoint.sameChunk(newPoint)) return;
        final long oldIndex = CoordConversion.chunkIndex(oldPoint);
        final long newIndex = CoordConversion.chunkIndex(newPoint);
        for (TargetEntry targetEntry : targetEntries) {
            if (targetEntry.target.type().isInstance(entity)) {
                targetEntry.addToChunk(newIndex, entity);
                targetEntry.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 = targetEntries[target.ordinal()];
        //noinspection unchecked
        var chunkEntities = (List) entry.chunkEntities(CoordConversion.chunkIndex(chunkX, chunkZ));
        return Collections.unmodifiableList(chunkEntities);
    }

    @Override
    public  void nearbyEntitiesByChunkRange(@NotNull Point point, int chunkRange, @NotNull Target target, @NotNull Consumer query) {
        final Long2ObjectSyncMap> entities = targetEntries[target.ordinal()].chunkEntities;
        if (chunkRange == 0) {
            // Single chunk
            final var chunkEntities = (List) entities.get(CoordConversion.chunkIndex(point));
            if (chunkEntities != null && !chunkEntities.isEmpty()) {
                chunkEntities.forEach(query);
            }
        } else {
            // Multiple chunks
            ChunkRange.chunksInRange(point, chunkRange, (chunkX, chunkZ) -> {
                final var chunkEntities = (List) entities.get(CoordConversion.chunkIndex(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 = targetEntries[target.ordinal()].chunkEntities;
        final int minChunkX = CoordConversion.globalToChunk(point.x() - range);
        final int minChunkZ = CoordConversion.globalToChunk(point.z() - range);
        final int maxChunkX = CoordConversion.globalToChunk(point.x() + range);
        final int maxChunkZ = CoordConversion.globalToChunk(point.z() + range);
        final double squaredRange = range * range;
        if (minChunkX == maxChunkX && minChunkZ == maxChunkZ) {
            // Single chunk
            final var chunkEntities = (List) entities.get(CoordConversion.chunkIndex(point));
            if (chunkEntities != null && !chunkEntities.isEmpty()) {
                chunkEntities.forEach(entity -> {
                    final Point position = entriesByEntityId.get(entity.getEntityId()).getLastPosition();
                    if (point.distanceSquared(position) <= squaredRange) query.accept(entity);
                });
            }
        } else {
            // Multiple chunks
            final int chunkRange = (int) (range / Chunk.CHUNK_SECTION_SIZE) + 1;
            ChunkRange.chunksInRange(point, chunkRange, (chunkX, chunkZ) -> {
                final var chunkEntities = (List) entities.get(CoordConversion.chunkIndex(chunkX, chunkZ));
                if (chunkEntities == null || chunkEntities.isEmpty()) return;
                chunkEntities.forEach(entity -> {
                    final Point position = entriesByEntityId.get(entity.getEntityId()).getLastPosition();
                    if (point.distanceSquared(position) <= squaredRange) {
                        query.accept(entity);
                    }
                });
            });
        }
    }

    @Override
    public @UnmodifiableView @NotNull  Set<@NotNull T> entities(@NotNull Target target) {
        //noinspection unchecked
        return (Set) targetEntries[target.ordinal()].entitiesView;
    }

    @Override
    public @NotNull Viewable viewable(@NotNull List<@NotNull SharedInstance> sharedInstances, int chunkX, int chunkZ) {
        var entry = targetEntries[Target.PLAYERS.ordinal()];
        return entry.viewers.computeIfAbsent(new ChunkViewKey(sharedInstances, chunkX, chunkZ), ChunkView::new);
    }

    private static class EntityTrackerEntry {
        private final Entity entity;
        private Point lastPosition;

        private EntityTrackerEntry(Entity entity, @Nullable Point lastPosition) {
            this.entity = entity;
            this.lastPosition = lastPosition;
        }

        public Entity getEntity() {
            return entity;
        }

        @Nullable
        public Point getLastPosition() {
            return lastPosition;
        }

        public void setLastPosition(Point lastPosition) {
            this.lastPosition = lastPosition;
        }
    }

    private  void difference(Point oldPoint, Point newPoint,
                                               @NotNull Target target, @NotNull Update update) {
        final TargetEntry entry = targetEntries[target.ordinal()];
        ChunkRange.chunksInRangeDiffering(newPoint.chunkX(), newPoint.chunkZ(), oldPoint.chunkX(), oldPoint.chunkZ(),
                ServerFlag.ENTITY_VIEW_DISTANCE, (chunkX, chunkZ) -> {
                    // Add
                    final List entities = entry.chunkEntities.get(CoordConversion.chunkIndex(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(CoordConversion.chunkIndex(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);
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy