// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.server.npc.role.support;

import com.hypixel.hytale.assetstore.map.BlockTypeAssetMap;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.server.core.modules.entity.player.PlayerSettings;
import com.hypixel.hytale.protocol.GameMode;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.server.npc.entities.NPCEntity;
import com.hypixel.hytale.server.core.modules.collision.CollisionMath;
import com.hypixel.hytale.server.npc.util.NPCPhysicsMath;
import com.hypixel.hytale.math.vector.Vector2d;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.math.iterator.BlockIterator;
import com.hypixel.hytale.protocol.Opacity;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import it.unimi.dsi.fastutil.ints.IntSet;
import com.hypixel.hytale.server.core.modules.blockset.BlockSetModule;
import java.util.Objects;
import com.hypixel.hytale.math.shape.Box;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.function.consumer.TriConsumer;
import java.util.Set;
import com.hypixel.hytale.function.consumer.QuadConsumer;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.function.predicate.QuadPredicate;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import com.hypixel.hytale.function.consumer.DoubleQuadObjectConsumer;
import com.hypixel.hytale.server.npc.NPCPlugin;
import com.hypixel.hytale.math.random.RandomExtra;
import com.hypixel.hytale.common.collection.BucketItemPool;
import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.Object2ByteMap;
import java.util.function.Consumer;
import java.util.List;
import javax.annotation.Nonnull;
import com.hypixel.hytale.server.npc.role.Role;
import com.hypixel.hytale.server.core.modules.entity.component.BoundingBox;
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
import com.hypixel.hytale.server.core.modules.entity.item.ItemComponent;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.component.ComponentAccessor;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Ref;
import java.util.function.BiPredicate;

public class PositionCache
{
    public static final BiPredicate<Ref<EntityStore>, ComponentAccessor<EntityStore>> IS_VALID_PLAYER;
    public static final BiPredicate<Ref<EntityStore>, ComponentAccessor<EntityStore>> IS_VALID_NPC;
    public static final double MIN_LOS_BLOCKING_DISTANCE_SQUARED = 1.0E-6;
    public static final String FUNCTION_CAN_BE_ONLY_CALLED_WHILE_CONFIGURING_POSITION_CACHE = "function can be only called while configuring PositionCache";
    private static final float LOS_CACHE_TTL_MIN_SECONDS = 0.09f;
    private static final float LOS_CACHE_TTL_MAX_SECONDS = 0.11f;
    private static final float POSITION_CACHE_TTL_SECONDS = 0.2f;
    private static final ComponentType<EntityStore, TransformComponent> TRANSFORM_COMPONENT_TYPE;
    private static final ComponentType<EntityStore, ItemComponent> ITEM_COMPONENT_TYPE;
    private static final ComponentType<EntityStore, ModelComponent> MODEL_COMPONENT_TYPE;
    protected static final ComponentType<EntityStore, BoundingBox> BOUNDING_BOX_COMPONENT_TYPE;
    private double maxDroppedItemDistance;
    private double maxSpawnMarkerDistance;
    private int maxSpawnBeaconDistance;
    @Nonnull
    private final Role role;
    private int opaqueBlockSet;
    protected EntityList players;
    protected EntityList npcs;
    protected final List<Consumer<Role>> externalRegistrations;
    private final List<Ref<EntityStore>> droppedItems;
    private final List<Ref<EntityStore>> spawnMarkers;
    private final List<Ref<EntityStore>> spawnBeacons;
    private final Object2ByteMap<Ref<EntityStore>> lineOfSightCache;
    private final Object2ByteMap<Ref<EntityStore>> inverseLineOfSightCache;
    private final Object2ByteMap<Ref<EntityStore>> friendlyFireCache;
    protected final LineOfSightBuffer lineOfSightComputeBuffer;
    protected final LineOfSightEntityBuffer lineOfSightEntityComputeBuffer;
    private float cacheTTL;
    private float positionCacheNextUpdate;
    private boolean isBenchmarking;
    private boolean isConfiguring;
    private boolean couldBreathe;
    
    public PositionCache(@Nonnull final Role role) {
        this.externalRegistrations = new ObjectArrayList<Consumer<Role>>();
        this.droppedItems = new ObjectArrayList<Ref<EntityStore>>();
        this.spawnMarkers = new ObjectArrayList<Ref<EntityStore>>();
        this.spawnBeacons = new ObjectArrayList<Ref<EntityStore>>();
        this.lineOfSightCache = new Object2ByteOpenHashMap<Ref<EntityStore>>();
        this.inverseLineOfSightCache = new Object2ByteOpenHashMap<Ref<EntityStore>>();
        this.friendlyFireCache = new Object2ByteOpenHashMap<Ref<EntityStore>>();
        this.lineOfSightComputeBuffer = new LineOfSightBuffer();
        this.lineOfSightEntityComputeBuffer = new LineOfSightEntityBuffer();
        this.cacheTTL = 0.09f;
        this.couldBreathe = true;
        this.role = role;
        this.players = new EntityList(null, PositionCache.IS_VALID_PLAYER);
        this.npcs = new EntityList(null, PositionCache.IS_VALID_NPC);
    }
    
    public boolean isBenchmarking() {
        return this.isBenchmarking;
    }
    
    public void setBenchmarking(final boolean benchmarking) {
        this.isBenchmarking = benchmarking;
    }
    
    public void setCouldBreathe(final boolean couldBreathe) {
        this.couldBreathe = couldBreathe;
    }
    
    public EntityList getPlayers() {
        return this.players;
    }
    
    public EntityList getNpcs() {
        return this.npcs;
    }
    
    public boolean tickPositionCacheNextUpdate(final float dt) {
        final float positionCacheNextUpdate = this.positionCacheNextUpdate - dt;
        this.positionCacheNextUpdate = positionCacheNextUpdate;
        return positionCacheNextUpdate <= 0.0f;
    }
    
    public void resetPositionCacheNextUpdate() {
        this.positionCacheNextUpdate = 0.2f;
    }
    
    public double getMaxDroppedItemDistance() {
        return this.maxDroppedItemDistance;
    }
    
    public double getMaxSpawnMarkerDistance() {
        return this.maxSpawnMarkerDistance;
    }
    
    public int getMaxSpawnBeaconDistance() {
        return this.maxSpawnBeaconDistance;
    }
    
    public void addExternalPositionCacheRegistration(final Consumer<Role> registration) {
        this.externalRegistrations.add(registration);
    }
    
    @Nonnull
    public List<Consumer<Role>> getExternalRegistrations() {
        return this.externalRegistrations;
    }
    
    public void reset(final boolean isConfiguring) {
        this.players.reset();
        this.npcs.reset();
        this.maxDroppedItemDistance = 0.0;
        this.droppedItems.clear();
        this.spawnMarkers.clear();
        this.spawnBeacons.clear();
        this.positionCacheNextUpdate = RandomExtra.randomRange(0.0f, 0.2f);
        this.clearLineOfSightCache();
        this.isConfiguring = isConfiguring;
    }
    
    public void finalizeConfiguration() {
        this.isConfiguring = false;
        this.npcs.finalizeConfiguration();
        this.players.finalizeConfiguration();
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackBuckets(false, this.npcs.getBucketRanges());
            roleStats.trackBuckets(true, this.players.getBucketRanges());
        }
    }
    
    public void clear(final double tickTime) {
        this.clearLineOfSightCache(tickTime);
        if (this.isBenchmarking) {
            NPCPlugin.get().collectSensorSupportTickDone(this.role.getRoleIndex());
        }
        this.isBenchmarking = false;
    }
    
    public boolean couldBreatheCached() {
        return this.couldBreathe;
    }
    
    public <T, U, V> void forEachPlayer(@Nonnull final DoubleQuadObjectConsumer<Ref<EntityStore>, T, U, V> consumer, final T t, final U u, final V v, final double d, final ComponentAccessor<EntityStore> componentAccessor) {
        this.players.forEachEntity(consumer, t, u, v, d, componentAccessor);
    }
    
    @Nullable
    public Ref<EntityStore> getClosestPlayerInRange(final double minRange, final double maxRange, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.getClosestPlayerInRange(minRange, maxRange, p -> true, componentAccessor);
    }
    
    @Nullable
    public Ref<EntityStore> getClosestPlayerInRange(final double minRange, final double maxRange, @Nonnull final Predicate<Ref<EntityStore>> filter, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.players.getClosestEntityInRange(minRange, maxRange, filter, componentAccessor);
    }
    
    @Nullable
    public Ref<EntityStore> getClosestNPCInRange(final double minRange, final double maxRange, @Nonnull final Predicate<Ref<EntityStore>> filter, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        return this.npcs.getClosestEntityInRange(minRange, maxRange, filter, componentAccessor);
    }
    
    public <S, T> void processNPCsInRange(@Nonnull final Ref<EntityStore> ref, final double minRange, final double maxRange, final boolean useProjectedDistance, final Ref<EntityStore> ignoredEntityReference, @Nonnull final Role role, @Nonnull final QuadPredicate<S, Ref<EntityStore>, Role, T> filter, final S s, final T t, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        this.processEntitiesInRange(ref, this.npcs, minRange, maxRange, useProjectedDistance, ignoredEntityReference, role, filter, s, t, componentAccessor);
    }
    
    public <S, T> void processPlayersInRange(@Nonnull final Ref<EntityStore> ref, final double minRange, final double maxRange, final boolean useProjectedDistance, final Ref<EntityStore> ignoredEntityReference, @Nonnull final Role role, @Nonnull final QuadPredicate<S, Ref<EntityStore>, Role, T> filter, final S s, final T t, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        this.processEntitiesInRange(ref, this.players, minRange, maxRange, useProjectedDistance, ignoredEntityReference, role, filter, s, t, componentAccessor);
    }
    
    public <S, T> void processEntitiesInRange(@Nonnull final Ref<EntityStore> ref, @Nonnull final EntityList entities, final double minRange, final double maxRange, final boolean useProjectedDistance, final Ref<EntityStore> ignoredEntityReference, @Nonnull final Role role, @Nonnull final QuadPredicate<S, Ref<EntityStore>, Role, T> filter, final S s, final T t, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (useProjectedDistance) {
            entities.getClosestEntityInRangeProjected(ref, ignoredEntityReference, role.getActiveMotionController(), minRange, maxRange, filter, role, s, t, componentAccessor);
        }
        else {
            entities.getClosestEntityInRange(ignoredEntityReference, minRange, maxRange, filter, role, s, t, componentAccessor);
        }
    }
    
    @Nullable
    public <S> Ref<EntityStore> getClosestDroppedItemInRange(@Nonnull final Ref<EntityStore> ref, double minRange, double maxRange, @Nonnull final QuadPredicate<S, Ref<EntityStore>, Role, ComponentAccessor<EntityStore>> filter, final Role role, final S s, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final int droppedItemsSize = this.droppedItems.size();
        if (droppedItemsSize == 0) {
            return null;
        }
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, PositionCache.TRANSFORM_COMPONENT_TYPE);
        assert transformComponent != null;
        final Vector3d position = transformComponent.getPosition();
        minRange *= minRange;
        maxRange *= maxRange;
        for (int index = 0; index < droppedItemsSize; ++index) {
            final Ref<EntityStore> itemEntityRef = this.droppedItems.get(index);
            if (itemEntityRef.isValid()) {
                final ItemComponent itemComponent = componentAccessor.getComponent(itemEntityRef, PositionCache.ITEM_COMPONENT_TYPE);
                if (itemComponent != null) {
                    final TransformComponent itemEntityTransformComponent = componentAccessor.getComponent(itemEntityRef, PositionCache.TRANSFORM_COMPONENT_TYPE);
                    assert itemEntityTransformComponent != null;
                    final double squaredDistance = itemEntityTransformComponent.getPosition().distanceSquaredTo(position);
                    if (squaredDistance >= minRange) {
                        if (squaredDistance >= maxRange) {
                            break;
                        }
                        if (filter.test(s, itemEntityRef, role, componentAccessor)) {
                            return itemEntityRef;
                        }
                    }
                }
            }
        }
        return null;
    }
    
    public <S> boolean isEntityCountInRange(final double minRange, final double maxRange, final int minCount, final int maxCount, final boolean findPlayers, final Role role, @Nonnull final QuadPredicate<S, Ref<EntityStore>, Role, ComponentAccessor<EntityStore>> filter, final S s, final ComponentAccessor<EntityStore> componentAccessor) {
        int count = 0;
        if (findPlayers) {
            count = this.players.countEntitiesInRange(minRange, maxRange, maxCount + 1, filter, s, role, componentAccessor);
            if (count > maxCount) {
                return false;
            }
        }
        count += this.npcs.countEntitiesInRange(minRange, maxRange, maxCount - count + 1, filter, s, role, componentAccessor);
        return count >= minCount && count <= maxCount;
    }
    
    public <S, T> int countEntitiesInRange(final double minRange, final double maxRange, final boolean findPlayers, @Nonnull final QuadPredicate<S, Ref<EntityStore>, T, ComponentAccessor<EntityStore>> filter, final S s, final T t, final ComponentAccessor<EntityStore> componentAccessor) {
        int count = 0;
        if (findPlayers) {
            count = this.players.countEntitiesInRange(minRange, maxRange, Integer.MAX_VALUE, filter, s, t, componentAccessor);
        }
        return count + this.npcs.countEntitiesInRange(minRange, maxRange, Integer.MAX_VALUE, filter, s, t, componentAccessor);
    }
    
    public void requirePlayerDistanceSorted(final double v) {
        final int value = MathUtil.ceil(v);
        if (!this.isConfiguring) {
            throw new IllegalStateException("function can be only called while configuring PositionCache");
        }
        this.players.requireDistanceSorted(value);
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackRange(true, RoleStats.RangeType.SORTED, value);
        }
    }
    
    public void requirePlayerDistanceUnsorted(final double v) {
        final int value = MathUtil.ceil(v);
        if (!this.isConfiguring) {
            throw new IllegalStateException("function can be only called while configuring PositionCache");
        }
        this.players.requireDistanceUnsorted(value);
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackRange(true, RoleStats.RangeType.UNSORTED, value);
        }
    }
    
    public void requirePlayerDistanceAvoidance(final double v) {
        final int value = MathUtil.ceil(v);
        if (!this.isConfiguring) {
            throw new IllegalStateException("function can be only called while configuring PositionCache");
        }
        this.players.requireDistanceAvoidance(value);
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackRange(true, RoleStats.RangeType.AVOIDANCE, value);
        }
    }
    
    public void requireEntityDistanceSorted(final double v) {
        final int value = MathUtil.ceil(v);
        if (!this.isConfiguring) {
            throw new IllegalStateException("function can be only called while configuring PositionCache");
        }
        this.npcs.requireDistanceSorted(value);
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackRange(false, RoleStats.RangeType.SORTED, value);
        }
    }
    
    public void requireEntityDistanceUnsorted(final double v) {
        final int value = MathUtil.ceil(v);
        if (!this.isConfiguring) {
            throw new IllegalStateException("function can be only called while configuring PositionCache");
        }
        this.npcs.requireDistanceUnsorted(value);
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackRange(false, RoleStats.RangeType.UNSORTED, value);
        }
    }
    
    public void requireEntityDistanceAvoidance(final double v) {
        int value = MathUtil.ceil(v);
        if (!this.isConfiguring) {
            throw new IllegalStateException("function can be only called while configuring PositionCache");
        }
        value = this.npcs.requireDistanceAvoidance(value);
        final RoleStats roleStats = this.role.getRoleStats();
        if (roleStats != null) {
            roleStats.trackRange(false, RoleStats.RangeType.AVOIDANCE, value);
        }
    }
    
    public void requireDroppedItemDistance(final double value) {
        if (this.maxDroppedItemDistance < value) {
            this.maxDroppedItemDistance = value;
        }
    }
    
    public void requireSpawnMarkerDistance(final double value) {
        if (this.maxSpawnMarkerDistance < value) {
            this.maxSpawnMarkerDistance = value;
        }
    }
    
    public void requireSpawnBeaconDistance(final int value) {
        if (this.maxSpawnBeaconDistance < value) {
            this.maxSpawnBeaconDistance = value;
        }
    }
    
    @Nonnull
    public Role getRole() {
        return this.role;
    }
    
    public <T, U, V, R> void forEachNPCUnordered(final double maxDistance, @Nonnull final QuadPredicate<Ref<EntityStore>, T, U, ComponentAccessor<EntityStore>> predicate, @Nonnull final QuadConsumer<Ref<EntityStore>, T, V, R> consumer, final T t, final U u, final V v, final R r, final ComponentAccessor<EntityStore> componentAccessor) {
        this.npcs.forEachEntityUnordered(maxDistance, predicate, consumer, t, u, v, r, componentAccessor);
    }
    
    public <T> void forEachEntityInAvoidanceRange(@Nonnull final Set<Ref<EntityStore>> ignoredEntitiesForAvoidance, @Nonnull final TriConsumer<Ref<EntityStore>, T, CommandBuffer<EntityStore>> consumer, final T t, final CommandBuffer<EntityStore> commandBuffer) {
        this.npcs.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, commandBuffer);
        this.players.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, commandBuffer);
    }
    
    public <T, U> void forEachEntityInAvoidanceRange(@Nonnull final Set<Ref<EntityStore>> ignoredEntitiesForAvoidance, @Nonnull final QuadConsumer<Ref<EntityStore>, T, U, CommandBuffer<EntityStore>> consumer, final T t, final U u, final CommandBuffer<EntityStore> commandBuffer) {
        this.npcs.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, u, commandBuffer);
        this.players.forEachEntityAvoidance(ignoredEntitiesForAvoidance, consumer, t, u, commandBuffer);
    }
    
    public void setOpaqueBlockSet(final int blockSet) {
        this.opaqueBlockSet = blockSet;
    }
    
    private static <T> boolean testLineOfSightRays(@Nonnull final Ref<EntityStore> ref, @Nonnull final Ref<EntityStore> targetRef, @Nonnull final RayPredicate<T> predicate, @Nonnull final T t, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, PositionCache.TRANSFORM_COMPONENT_TYPE);
        assert transformComponent != null;
        final Vector3d position = transformComponent.getPosition();
        final ModelComponent modelComponent = componentAccessor.getComponent(ref, PositionCache.MODEL_COMPONENT_TYPE);
        final float eyeHeight = (modelComponent != null) ? modelComponent.getModel().getEyeHeight() : 0.0f;
        final double sx = position.getX();
        final double sy = position.getY() + eyeHeight;
        final double sz = position.getZ();
        final TransformComponent targetTransformComponent = componentAccessor.getComponent(targetRef, PositionCache.TRANSFORM_COMPONENT_TYPE);
        assert targetTransformComponent != null;
        final Vector3d targetPosition = targetTransformComponent.getPosition();
        final double tx = targetPosition.getX();
        final double ty = targetPosition.getY();
        final double tz = targetPosition.getZ();
        final ModelComponent targetModelComponent = componentAccessor.getComponent(targetRef, PositionCache.MODEL_COMPONENT_TYPE);
        if (targetModelComponent != null) {
            return predicate.test(sx, sy, sz, tx, ty + targetModelComponent.getModel().getEyeHeight(), tz, t, componentAccessor);
        }
        double ox = 0.0;
        double oy = 0.0;
        double oz = 0.0;
        final BoundingBox boundingBoxComponent = componentAccessor.getComponent(targetRef, PositionCache.BOUNDING_BOX_COMPONENT_TYPE);
        if (boundingBoxComponent != null) {
            final Box boundingBox = boundingBoxComponent.getBoundingBox();
            ox = (boundingBox.getMax().getX() + boundingBox.getMin().getX()) / 2.0;
            oy = (boundingBox.getMax().getY() + boundingBox.getMin().getY()) / 2.0;
            oz = (boundingBox.getMax().getZ() + boundingBox.getMin().getZ()) / 2.0;
        }
        return predicate.test(sx, sy, sz, tx + ox, ty + oy, tz + oz, t, componentAccessor);
    }
    
    private boolean hasLineOfSightInternal(@Nonnull final Ref<EntityStore> ref, @Nonnull final Ref<EntityStore> targetRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        if (ref.equals(targetRef)) {
            return false;
        }
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, PositionCache.TRANSFORM_COMPONENT_TYPE);
        assert transformComponent != null;
        final TransformComponent targetTransformComponent = componentAccessor.getComponent(targetRef, PositionCache.TRANSFORM_COMPONENT_TYPE);
        assert targetTransformComponent != null;
        if (transformComponent.getPosition().distanceSquaredTo(targetTransformComponent.getPosition()) <= 1.0E-12) {
            return true;
        }
        final World world = componentAccessor.getExternalData().getWorld();
        Objects.requireNonNull(world, "World can't be null in isLOS");
        final Int2ObjectMap<IntSet> blockSets = BlockSetModule.getInstance().getBlockSets();
        final IntSet opaqueSet = (this.opaqueBlockSet >= 0 && blockSets != null) ? blockSets.get(this.opaqueBlockSet) : null;
        try {
            this.lineOfSightComputeBuffer.result = true;
            this.lineOfSightComputeBuffer.assetMap = BlockType.getAssetMap();
            this.lineOfSightComputeBuffer.opaqueSet = opaqueSet;
            this.lineOfSightComputeBuffer.world = world;
            return testLineOfSightRays(ref, targetRef, (sx, sy, sz, tx, ty, tz, buffer, accessor) -> {
                buffer.chunk = buffer.world.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(sx, sz));
                if (buffer.chunk == null) {
                    return false;
                }
                else {
                    BlockIterator.iterateFromTo(sx, sy, sz, tx, ty, tz, (x, y, z, px, py, pz, qx, qy, qz, iBuffer) -> {
                        if (!ChunkUtil.isInsideChunk(iBuffer.chunk.getX(), iBuffer.chunk.getZ(), x, z)) {
                            iBuffer.chunk = iBuffer.world.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(x, z));
                            if (iBuffer.chunk == null) {
                                return iBuffer.result = false;
                            }
                        }
                        final int blockId = iBuffer.chunk.getBlock(x, y, z);
                        if (blockId == 0) {
                            return true;
                        }
                        else {
                            final BlockType blockType = iBuffer.assetMap.getAsset(blockId);
                            if (blockType == BlockType.UNKNOWN || blockType.getOpacity() == null || blockType.getOpacity() != Opacity.Transparent || (iBuffer.opaqueSet != null && iBuffer.opaqueSet.contains(blockId))) {
                                return iBuffer.result = false;
                            }
                            else {
                                return true;
                            }
                        }
                    }, buffer);
                    return buffer.result;
                }
            }, this.lineOfSightComputeBuffer, componentAccessor);
        }
        finally {
            this.lineOfSightComputeBuffer.clearRefs();
        }
    }
    
    public boolean hasLineOfSight(@Nonnull final Ref<EntityStore> ref, @Nonnull final Ref<EntityStore> targetRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final boolean cached = this.lineOfSightCache.containsKey(targetRef);
        if (cached) {
            if (this.isBenchmarking) {
                NPCPlugin.get().collectSensorSupportLosTest(this.role.getRoleIndex(), true, 0L);
            }
            return this.lineOfSightCache.getByte(targetRef) != 0;
        }
        boolean hasLineOfSight;
        if (this.isBenchmarking) {
            final long start = System.nanoTime();
            hasLineOfSight = this.hasLineOfSightInternal(ref, targetRef, componentAccessor);
            NPCPlugin.get().collectSensorSupportLosTest(this.role.getRoleIndex(), false, System.nanoTime() - start);
        }
        else {
            hasLineOfSight = this.hasLineOfSightInternal(ref, targetRef, componentAccessor);
        }
        this.lineOfSightCache.put(targetRef, (byte)(hasLineOfSight ? 1 : 0));
        return hasLineOfSight;
    }
    
    public boolean hasInverseLineOfSight(@Nonnull final Ref<EntityStore> ref, @Nonnull final Ref<EntityStore> targetRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final boolean cached = this.inverseLineOfSightCache.containsKey(targetRef);
        if (this.isBenchmarking) {
            NPCPlugin.get().collectSensorSupportInverseLosTest(this.role.getRoleIndex(), cached);
        }
        if (cached) {
            return this.inverseLineOfSightCache.getByte(targetRef) != 0;
        }
        final boolean hasLineOfSight = this.hasLineOfSightInternal(targetRef, ref, componentAccessor);
        this.inverseLineOfSightCache.put(targetRef, (byte)(hasLineOfSight ? 1 : 0));
        return hasLineOfSight;
    }
    
    public boolean isFriendlyBlockingLineOfSight(@Nonnull final Ref<EntityStore> ref, @Nonnull final Ref<EntityStore> targetRef, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final boolean cached = this.friendlyFireCache.containsKey(targetRef);
        if (this.isBenchmarking) {
            NPCPlugin.get().collectSensorSupportFriendlyBlockingTest(this.role.getRoleIndex(), cached);
        }
        if (cached) {
            return this.friendlyFireCache.getByte(targetRef) != 0;
        }
        final boolean blocking = testLineOfSightRays(ref, targetRef, (sx, sy, sz, tx, ty, tz, _this, accessor) -> {
            final LineOfSightEntityBuffer buffer = _this.lineOfSightEntityComputeBuffer;
            buffer.pos.assign(sx, sy, sz);
            buffer.dir.assign(tx - sx, ty - sy, tz - sz);
            final double squaredLength = buffer.dir.squaredLength();
            if (squaredLength < 1.0E-6) {
                return false;
            }
            else {
                return _this.players.testAnyEntityDistanceSquared(squaredLength, (positionCache, targetRef1, buffer1, componentAccessor1, length2) -> positionCache.testLineOfSightEntity(ref, targetRef1, buffer1, componentAccessor1, length2), _this, buffer, accessor) || _this.npcs.testAnyEntityDistanceSquared(squaredLength, (positionCache1, targetRef2, buffer2, componentAccessor2, length3) -> positionCache1.testLineOfSightEntity(ref, targetRef2, buffer2, componentAccessor2, length3), _this, buffer, accessor);
            }
        }, this, componentAccessor);
        this.friendlyFireCache.put(targetRef, (byte)(blocking ? 1 : 0));
        return blocking;
    }
    
    private boolean testLineOfSightEntity(@Nonnull final Ref<EntityStore> ref, @Nonnull final Ref<EntityStore> targetRef, @Nonnull final LineOfSightEntityBuffer buffer, @Nonnull final ComponentAccessor<EntityStore> componentAccessor, final double length2) {
        return !targetRef.equals(ref) && this.role.isFriendly(targetRef, componentAccessor) && rayIsIntersectingEntity(targetRef, buffer.pos, buffer.dir, buffer.minMax, length2, componentAccessor);
    }
    
    private void clearLineOfSightCache(final double tickTime) {
        this.cacheTTL -= (float)tickTime;
        if (this.cacheTTL <= 0.0f) {
            this.clearLineOfSightCache();
        }
    }
    
    private void clearLineOfSightCache() {
        this.cacheTTL = RandomExtra.randomRange(0.09f, 0.11f);
        this.lineOfSightCache.clear();
        this.inverseLineOfSightCache.clear();
        this.friendlyFireCache.clear();
    }
    
    protected static boolean rayIsIntersectingEntity(@Nonnull final Ref<EntityStore> ref, @Nonnull final Vector3d pos, @Nonnull final Vector3d dir, @Nonnull final Vector2d minMax, final double length2, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final BoundingBox boundingBoxComponent = componentAccessor.getComponent(ref, PositionCache.BOUNDING_BOX_COMPONENT_TYPE);
        if (boundingBoxComponent == null) {
            return false;
        }
        final TransformComponent transformComponent = componentAccessor.getComponent(ref, PositionCache.TRANSFORM_COMPONENT_TYPE);
        assert transformComponent != null;
        final Vector3d position = transformComponent.getPosition();
        final double px = position.getX();
        final double py = position.getY();
        final double pz = position.getZ();
        final double dx = px - pos.x;
        final double dy = py - pos.y;
        final double dz = pz - pos.z;
        final double dotProduct = NPCPhysicsMath.dotProduct(dir.x, dir.y, dir.z, dx, dy, dz);
        if (dotProduct <= 0.0) {
            return false;
        }
        final double dist2 = NPCPhysicsMath.dotProduct(dx, dy, dz);
        return dotProduct * dotProduct < dist2 * length2 && CollisionMath.intersectRayAABB(pos, dir, px, py, pz, boundingBoxComponent.getBoundingBox(), minMax);
    }
    
    @Nonnull
    public List<Ref<EntityStore>> getDroppedItemList() {
        return this.droppedItems;
    }
    
    @Nonnull
    public List<Ref<EntityStore>> getSpawnMarkerList() {
        return this.spawnMarkers;
    }
    
    @Nonnull
    public List<Ref<EntityStore>> getSpawnBeaconList() {
        return this.spawnBeacons;
    }
    
    static {
        IS_VALID_PLAYER = ((ref, componentAccessor) -> {
            final Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
            if (playerComponent == null || playerComponent.isWaitingForClientReady()) {
                return false;
            }
            else if (playerComponent.getGameMode() == GameMode.Adventure) {
                return true;
            }
            else if (playerComponent.getGameMode() == GameMode.Creative) {
                final PlayerSettings playerSettingsComponent = componentAccessor.getComponent(ref, PlayerSettings.getComponentType());
                return playerSettingsComponent != null && playerSettingsComponent.creativeSettings().allowNPCDetection();
            }
            else {
                return false;
            }
        });
        IS_VALID_NPC = ((ref, accessor) -> accessor.getArchetype(ref).contains(NPCEntity.getComponentType()));
        TRANSFORM_COMPONENT_TYPE = TransformComponent.getComponentType();
        ITEM_COMPONENT_TYPE = ItemComponent.getComponentType();
        MODEL_COMPONENT_TYPE = ModelComponent.getComponentType();
        BOUNDING_BOX_COMPONENT_TYPE = BoundingBox.getComponentType();
    }
    
    private static class LineOfSightBuffer
    {
        @Nullable
        public World world;
        @Nullable
        public WorldChunk chunk;
        @Nullable
        public IntSet opaqueSet;
        @Nullable
        public BlockTypeAssetMap<String, BlockType> assetMap;
        public boolean result;
        
        public void clearRefs() {
            this.world = null;
            this.chunk = null;
            this.opaqueSet = null;
            this.assetMap = null;
        }
    }
    
    private static class LineOfSightEntityBuffer
    {
        public final Vector3d pos;
        public final Vector3d dir;
        public final Vector2d minMax;
        
        private LineOfSightEntityBuffer() {
            this.pos = new Vector3d();
            this.dir = new Vector3d();
            this.minMax = new Vector2d();
        }
    }
    
    @FunctionalInterface
    public interface RayPredicate<T>
    {
        boolean test(final double p0, final double p1, final double p2, final double p3, final double p4, final double p5, final T p6, @Nonnull final ComponentAccessor<EntityStore> p7);
    }
}
