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

package com.hypixel.hytale.server.core.modules.entity.player;

import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.codec.Codec;
import java.util.concurrent.CompletionStage;
import java.util.Iterator;
import java.util.logging.Level;
import com.hypixel.hytale.logger.HytaleLogger;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletableFuture;
import com.hypixel.hytale.server.core.io.PacketHandler;
import com.hypixel.hytale.component.Store;
import java.util.List;
import java.util.Collection;
import java.util.Collections;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.longs.LongCollection;
import com.hypixel.hytale.server.core.Message;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkFlag;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.component.ComponentAccessor;
import it.unimi.dsi.fastutil.longs.LongSet;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.component.CommandBuffer;
import com.hypixel.hytale.server.core.entity.entities.Player;
import it.unimi.dsi.fastutil.longs.LongIterator;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.world.UnloadChunk;
import com.hypixel.hytale.math.util.ChunkUtil;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.common.fastutil.HLongOpenHashSet;
import com.hypixel.hytale.server.core.modules.entity.EntityModule;
import com.hypixel.hytale.component.ComponentType;
import com.hypixel.hytale.common.fastutil.HLongSet;
import java.util.concurrent.locks.StampedLock;
import com.hypixel.hytale.math.iterator.CircleSpiralIterator;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
import javax.annotation.Nonnull;
import com.hypixel.hytale.metrics.MetricsRegistry;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Component;

public class ChunkTracker implements Component<EntityStore>
{
    @Nonnull
    public static final MetricsRegistry<ChunkTracker> METRICS_REGISTRY;
    public static final int MAX_CHUNKS_PER_SECOND_LOCAL = 256;
    public static final int MAX_CHUNKS_PER_SECOND_LAN = 128;
    public static final int MAX_CHUNKS_PER_SECOND = 36;
    public static final int MAX_CHUNKS_PER_TICK = 4;
    public static final int MIN_LOADED_CHUNKS_RADIUS = 2;
    public static final int MAX_HOT_LOADED_CHUNKS_RADIUS = 8;
    public static final long MAX_FAILURE_BACKOFF_NANOS;
    @Nullable
    private TransformComponent transformComponent;
    private int chunkViewRadius;
    @Nonnull
    private final CircleSpiralIterator spiralIterator;
    @Nonnull
    private final StampedLock loadedLock;
    @Nonnull
    private final HLongSet loading;
    @Nonnull
    private final HLongSet loaded;
    @Nonnull
    private final HLongSet reload;
    private int maxChunksPerSecond;
    private float inverseMaxChunksPerSecond;
    private int maxChunksPerTick;
    private int minLoadedChunksRadius;
    private int maxHotLoadedChunksRadius;
    private float accumulator;
    private int sentViewRadius;
    private int hotRadius;
    private int lastChunkX;
    private int lastChunkZ;
    private boolean readyForChunks;
    
    public static ComponentType<EntityStore, ChunkTracker> getComponentType() {
        return EntityModule.get().getChunkTrackerComponentType();
    }
    
    public ChunkTracker() {
        this.spiralIterator = new CircleSpiralIterator();
        this.loadedLock = new StampedLock();
        this.loading = new HLongOpenHashSet();
        this.loaded = new HLongOpenHashSet();
        this.reload = new HLongOpenHashSet();
        this.minLoadedChunksRadius = 2;
        this.maxHotLoadedChunksRadius = 8;
        this.maxChunksPerTick = 4;
    }
    
    private ChunkTracker(@Nonnull final ChunkTracker other) {
        this.spiralIterator = new CircleSpiralIterator();
        this.loadedLock = new StampedLock();
        this.loading = new HLongOpenHashSet();
        this.loaded = new HLongOpenHashSet();
        this.reload = new HLongOpenHashSet();
        this.copyFrom(other);
    }
    
    public void unloadAll(@Nonnull final PlayerRef playerRefComponent) {
        final long stamp = this.loadedLock.writeLock();
        try {
            this.loading.clear();
            final LongIterator iterator = this.loaded.iterator();
            while (iterator.hasNext()) {
                final long chunkIndex = iterator.nextLong();
                final int chunkX = ChunkUtil.xOfChunkIndex(chunkIndex);
                final int chunkZ = ChunkUtil.zOfChunkIndex(chunkIndex);
                playerRefComponent.getPacketHandler().writeNoCache(new UnloadChunk(chunkX, chunkZ));
            }
            this.loaded.clear();
            this.sentViewRadius = 0;
            this.hotRadius = 0;
        }
        finally {
            this.loadedLock.unlockWrite(stamp);
        }
    }
    
    public void clear() {
        final long stamp = this.loadedLock.writeLock();
        try {
            this.loading.clear();
            this.loaded.clear();
            this.sentViewRadius = 0;
            this.hotRadius = 0;
        }
        finally {
            this.loadedLock.unlockWrite(stamp);
        }
    }
    
    public void tick(@Nonnull final Player playerComponent, @Nonnull final PlayerRef playerRefComponent, @Nonnull final TransformComponent transformComponent, final float dt, @Nonnull final CommandBuffer<EntityStore> commandBuffer) {
        if (!this.readyForChunks) {
            return;
        }
        this.transformComponent = transformComponent;
        final int viewRadius = playerComponent.getViewRadius();
        this.chunkViewRadius = viewRadius;
        final int chunkViewRadius = viewRadius;
        final Vector3d position = transformComponent.getPosition();
        final int chunkX = MathUtil.floor(position.getX()) >> 5;
        final int chunkZ = MathUtil.floor(position.getZ()) >> 5;
        final int xDiff = Math.abs(this.lastChunkX - chunkX);
        final int zDiff = Math.abs(this.lastChunkZ - chunkZ);
        final int chunkMoveDistance = (xDiff > 0 || zDiff > 0) ? ((int)Math.ceil(Math.sqrt(xDiff * xDiff + zDiff * zDiff))) : 0;
        this.sentViewRadius = Math.max(0, this.sentViewRadius - chunkMoveDistance);
        this.hotRadius = Math.max(0, this.hotRadius - chunkMoveDistance);
        this.lastChunkX = chunkX;
        this.lastChunkZ = chunkZ;
        if (this.sentViewRadius == chunkViewRadius && this.hotRadius == Math.min(this.maxHotLoadedChunksRadius, chunkViewRadius) && this.reload.isEmpty()) {
            return;
        }
        if (this.sentViewRadius > chunkViewRadius) {
            this.sentViewRadius = chunkViewRadius;
        }
        if (this.hotRadius > chunkViewRadius) {
            this.hotRadius = chunkViewRadius;
        }
        final World world = commandBuffer.getExternalData().getWorld();
        final ChunkStore chunkStore = world.getChunkStore();
        final int minLoadedRadius = Math.max(this.minLoadedChunksRadius, chunkViewRadius);
        final int minLoadedRadiusSq = minLoadedRadius * minLoadedRadius;
        final long stamp = this.loadedLock.writeLock();
        try {
            this.loaded.removeIf(ChunkTracker::tryUnloadChunk, minLoadedRadiusSq, chunkX, chunkZ, playerRefComponent, this.loading);
            this.accumulator += dt;
            int toLoad = Math.min((int)(this.maxChunksPerSecond * this.accumulator), this.maxChunksPerTick);
            final int loadingSize = this.loading.size();
            toLoad -= loadingSize;
            if (!this.reload.isEmpty()) {
                final LongIterator iterator = this.reload.iterator();
                while (iterator.hasNext()) {
                    final long chunkCoordinates = iterator.nextLong();
                    if (chunkStore.isChunkOnBackoff(chunkCoordinates, ChunkTracker.MAX_FAILURE_BACKOFF_NANOS)) {
                        continue;
                    }
                    if (!this.loading.add(chunkCoordinates)) {
                        continue;
                    }
                    this.tryLoadChunkAsync(chunkStore, playerRefComponent, chunkCoordinates, transformComponent, commandBuffer);
                    iterator.remove();
                    --toLoad;
                    this.accumulator -= this.inverseMaxChunksPerSecond;
                }
            }
            if (this.sentViewRadius < minLoadedRadius) {
                boolean areAllLoaded = true;
                this.spiralIterator.init(chunkX, chunkZ, this.sentViewRadius, minLoadedRadius);
                while (toLoad > 0 && this.spiralIterator.hasNext()) {
                    final long chunkCoordinates = this.spiralIterator.next();
                    if (!this.loaded.contains(chunkCoordinates)) {
                        areAllLoaded = false;
                        if (chunkStore.isChunkOnBackoff(chunkCoordinates, ChunkTracker.MAX_FAILURE_BACKOFF_NANOS)) {
                            continue;
                        }
                        if (!this.loading.add(chunkCoordinates)) {
                            continue;
                        }
                        this.tryLoadChunkAsync(chunkStore, playerRefComponent, chunkCoordinates, transformComponent, commandBuffer);
                        --toLoad;
                        this.accumulator -= this.inverseMaxChunksPerSecond;
                    }
                    else {
                        if (!areAllLoaded) {
                            continue;
                        }
                        this.sentViewRadius = this.spiralIterator.getCompletedRadius();
                    }
                }
                if (areAllLoaded) {
                    this.sentViewRadius = this.spiralIterator.getCompletedRadius();
                }
            }
        }
        finally {
            this.loadedLock.unlockWrite(stamp);
        }
        final int maxHotRadius = Math.min(this.maxHotLoadedChunksRadius, this.sentViewRadius);
        if (this.hotRadius < maxHotRadius) {
            this.spiralIterator.init(chunkX, chunkZ, this.hotRadius, maxHotRadius);
            while (this.spiralIterator.hasNext()) {
                final Ref<ChunkStore> chunkReference = chunkStore.getChunkReference(this.spiralIterator.next());
                if (chunkReference != null) {
                    if (!chunkReference.isValid()) {
                        continue;
                    }
                    final WorldChunk worldChunkComponent = chunkStore.getStore().getComponent(chunkReference, WorldChunk.getComponentType());
                    assert worldChunkComponent != null;
                    if (worldChunkComponent.is(ChunkFlag.TICKING)) {
                        continue;
                    }
                    commandBuffer.run(_store -> worldChunkComponent.setFlag(ChunkFlag.TICKING, true));
                }
            }
            this.hotRadius = maxHotRadius;
        }
        if (this.sentViewRadius == chunkViewRadius) {
            this.accumulator = 0.0f;
        }
    }
    
    public boolean isLoaded(final long indexChunk) {
        final long stamp = this.loadedLock.readLock();
        try {
            return this.loaded.contains(indexChunk);
        }
        finally {
            this.loadedLock.unlockRead(stamp);
        }
    }
    
    public void removeForReload(final long indexChunk) {
        if (this.shouldBeVisible(indexChunk)) {
            final long stamp = this.loadedLock.writeLock();
            try {
                this.reload.add(indexChunk);
            }
            finally {
                this.loadedLock.unlockWrite(stamp);
            }
        }
    }
    
    public boolean shouldBeVisible(final long chunkCoordinates) {
        if (this.transformComponent == null) {
            return false;
        }
        final Vector3d position = this.transformComponent.getPosition();
        final int chunkX = MathUtil.floor(position.getX()) >> 5;
        final int chunkZ = MathUtil.floor(position.getZ()) >> 5;
        final int x = ChunkUtil.xOfChunkIndex(chunkCoordinates);
        final int z = ChunkUtil.zOfChunkIndex(chunkCoordinates);
        final int minLoadedRadius = Math.max(this.minLoadedChunksRadius, this.chunkViewRadius);
        return shouldBeVisible(minLoadedRadius * minLoadedRadius, chunkX, chunkZ, x, z);
    }
    
    @Nonnull
    public ChunkVisibility getChunkVisibility(final long indexChunk) {
        if (this.transformComponent == null) {
            return ChunkVisibility.NONE;
        }
        final Vector3d position = this.transformComponent.getPosition();
        final int chunkX = MathUtil.floor(position.getX()) >> 5;
        final int chunkZ = MathUtil.floor(position.getZ()) >> 5;
        final int x = ChunkUtil.xOfChunkIndex(indexChunk);
        final int z = ChunkUtil.zOfChunkIndex(indexChunk);
        final int xDiff = Math.abs(x - chunkX);
        final int zDiff = Math.abs(z - chunkZ);
        final int distanceSq = xDiff * xDiff + zDiff * zDiff;
        final int minLoadedRadius = Math.max(this.minLoadedChunksRadius, this.chunkViewRadius);
        final boolean shouldBeVisible = distanceSq <= minLoadedRadius * minLoadedRadius;
        if (shouldBeVisible) {
            final boolean isHot = distanceSq <= this.maxHotLoadedChunksRadius * this.maxHotLoadedChunksRadius;
            return isHot ? ChunkVisibility.HOT : ChunkVisibility.COLD;
        }
        return ChunkVisibility.NONE;
    }
    
    public int getMaxChunksPerSecond() {
        return this.maxChunksPerSecond;
    }
    
    public void setMaxChunksPerSecond(final int maxChunksPerSecond) {
        this.maxChunksPerSecond = maxChunksPerSecond;
        this.inverseMaxChunksPerSecond = 1.0f / maxChunksPerSecond;
    }
    
    public void setDefaultMaxChunksPerSecond(@Nonnull final PlayerRef playerRef) {
        if (playerRef.getPacketHandler().isLocalConnection()) {
            this.maxChunksPerSecond = 256;
        }
        else if (playerRef.getPacketHandler().isLANConnection()) {
            this.maxChunksPerSecond = 128;
        }
        else {
            this.maxChunksPerSecond = 36;
        }
        this.inverseMaxChunksPerSecond = 1.0f / this.maxChunksPerSecond;
    }
    
    public int getMaxChunksPerTick() {
        return this.maxChunksPerTick;
    }
    
    public void setMaxChunksPerTick(final int maxChunksPerTick) {
        this.maxChunksPerTick = maxChunksPerTick;
    }
    
    public int getMinLoadedChunksRadius() {
        return this.minLoadedChunksRadius;
    }
    
    public void setMinLoadedChunksRadius(final int minLoadedChunksRadius) {
        this.minLoadedChunksRadius = minLoadedChunksRadius;
    }
    
    public int getMaxHotLoadedChunksRadius() {
        return this.maxHotLoadedChunksRadius;
    }
    
    public void setMaxHotLoadedChunksRadius(final int maxHotLoadedChunksRadius) {
        this.maxHotLoadedChunksRadius = maxHotLoadedChunksRadius;
    }
    
    public int getLoadedChunksCount() {
        long stamp = this.loadedLock.tryOptimisticRead();
        final int size = this.loaded.size();
        if (this.loadedLock.validate(stamp)) {
            return size;
        }
        stamp = this.loadedLock.readLock();
        try {
            return this.loaded.size();
        }
        finally {
            this.loadedLock.unlockRead(stamp);
        }
    }
    
    public int getLoadingChunksCount() {
        long stamp = this.loadedLock.tryOptimisticRead();
        final int size = this.loading.size();
        if (this.loadedLock.validate(stamp)) {
            return size;
        }
        stamp = this.loadedLock.readLock();
        try {
            return this.loading.size();
        }
        finally {
            this.loadedLock.unlockRead(stamp);
        }
    }
    
    @Nonnull
    private String getLoadedChunksGrid() {
        final int viewRadius = this.chunkViewRadius;
        final int chunkXMin = this.lastChunkX - viewRadius;
        final int chunkZMin = this.lastChunkZ - viewRadius;
        final int chunkXMax = this.lastChunkX + viewRadius;
        final int chunkZMax = this.lastChunkZ + viewRadius;
        final StringBuilder sb = new StringBuilder();
        sb.append("(").append(chunkXMin).append(", ").append(chunkZMin).append(") -> (").append(chunkXMax).append(", ").append(chunkZMax).append(")\n");
        for (int x = chunkXMin; x <= chunkXMax; ++x) {
            for (int z = chunkZMin; z <= chunkZMax; ++z) {
                final long index = ChunkUtil.indexChunk(x, z);
                if (this.loaded.contains(index)) {
                    final ChunkVisibility chunkVisibility = this.getChunkVisibility(index);
                    switch (chunkVisibility.ordinal()) {
                        case 0: {
                            sb.append('X');
                            break;
                        }
                        case 1: {
                            sb.append('#');
                            break;
                        }
                        case 2: {
                            sb.append('&');
                            break;
                        }
                    }
                }
                else if (this.loading.contains(index)) {
                    sb.append('%');
                }
                else {
                    sb.append(' ');
                }
            }
            sb.append('\n');
        }
        return sb.toString();
    }
    
    @Nonnull
    public Message getLoadedChunksMessage() {
        final long stamp = this.loadedLock.readLock();
        try {
            return Message.translation("server.commands.chunkTracker.loaded").monospace(true).param("grid", this.getLoadedChunksGrid()).param("viewRadius", this.chunkViewRadius).param("sentViewRadius", this.sentViewRadius).param("hotRadius", this.hotRadius).param("readyForChunks", this.readyForChunks).param("loaded", this.loaded.size()).param("loading", this.loading.size());
        }
        finally {
            this.loadedLock.unlockRead(stamp);
        }
    }
    
    @Nonnull
    public String getLoadedChunksDebug() {
        final long stamp = this.loadedLock.readLock();
        try {
            return "Chunks (#: Loaded, &: Loading, ' ': Not loaded):\n" + this.getLoadedChunksGrid() + "\nView Radius: " + this.chunkViewRadius + "\nSent View Radius: " + this.sentViewRadius + "\nHot Radius: " + this.hotRadius + "\nReady For Chunks: " + this.readyForChunks + "\nLoaded: " + this.loaded.size() + "\nLoading: " + this.loading.size();
        }
        finally {
            this.loadedLock.unlockRead(stamp);
        }
    }
    
    public void setReadyForChunks(final boolean readyForChunks) {
        this.readyForChunks = readyForChunks;
    }
    
    public boolean isReadyForChunks() {
        return this.readyForChunks;
    }
    
    public void copyFrom(@Nonnull final ChunkTracker chunkTracker) {
        final long stamp = this.loadedLock.writeLock();
        try {
            final long otherStamp = chunkTracker.loadedLock.readLock();
            try {
                this.loading.addAll(chunkTracker.loading);
                this.loaded.addAll(chunkTracker.loaded);
                this.reload.addAll(chunkTracker.reload);
                this.sentViewRadius = 0;
            }
            finally {
                chunkTracker.loadedLock.unlockRead(otherStamp);
            }
        }
        finally {
            this.loadedLock.unlockWrite(stamp);
        }
    }
    
    private static boolean shouldBeVisible(final int chunkViewRadiusSquared, final int chunkX, final int chunkZ, final int x, final int z) {
        final int xDiff = Math.abs(x - chunkX);
        final int zDiff = Math.abs(z - chunkZ);
        final int distanceSq = xDiff * xDiff + zDiff * zDiff;
        return distanceSq <= chunkViewRadiusSquared;
    }
    
    public static boolean tryUnloadChunk(final long chunkIndex, final int chunkViewRadiusSquared, final int chunkX, final int chunkZ, @Nonnull final PlayerRef playerRef, @Nonnull final LongSet loading) {
        final int x = ChunkUtil.xOfChunkIndex(chunkIndex);
        final int z = ChunkUtil.zOfChunkIndex(chunkIndex);
        if (shouldBeVisible(chunkViewRadiusSquared, x, z, chunkX, chunkZ)) {
            return false;
        }
        final Ref<EntityStore> ref = playerRef.getReference();
        if (ref == null || !ref.isValid()) {
            loading.remove(chunkIndex);
            return true;
        }
        final Store<EntityStore> store = ref.getStore();
        final World world = store.getExternalData().getWorld();
        final PacketHandler packetHandler = playerRef.getPacketHandler();
        final ChunkStore chunkComponentStore = world.getChunkStore();
        final Ref<ChunkStore> chunkRef = chunkComponentStore.getChunkReference(chunkIndex);
        if (chunkRef != null) {
            final Store<ChunkStore> chunkStore = chunkComponentStore.getStore();
            final ObjectArrayList<Packet> packets = new ObjectArrayList<Packet>();
            chunkStore.fetch(Collections.singletonList(chunkRef), ChunkStore.UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, playerRef, packets);
            for (int i = 0; i < packets.size(); ++i) {
                packetHandler.write(packets.get(i));
            }
        }
        packetHandler.writeNoCache(new UnloadChunk(x, z));
        loading.remove(chunkIndex);
        return true;
    }
    
    public void tryLoadChunkAsync(@Nonnull final ChunkStore chunkStore, @Nonnull final PlayerRef playerRefComponent, final long chunkIndex, @Nonnull final TransformComponent transformComponent, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        final World world = componentAccessor.getExternalData().getWorld();
        final Vector3d position = transformComponent.getPosition();
        final int chunkX = MathUtil.floor(position.getX()) >> 5;
        final int chunkZ = MathUtil.floor(position.getZ()) >> 5;
        final int x = ChunkUtil.xOfChunkIndex(chunkIndex);
        final int z = ChunkUtil.zOfChunkIndex(chunkIndex);
        final boolean isHot = shouldBeVisible(this.maxHotLoadedChunksRadius, chunkX, chunkZ, x, z);
        final Ref<ChunkStore> chunkReference = chunkStore.getChunkReference(chunkIndex);
        if (chunkReference != null) {
            final WorldChunk worldChunkComponent = chunkStore.getStore().getComponent(chunkReference, WorldChunk.getComponentType());
            assert worldChunkComponent != null;
            if (worldChunkComponent.is(ChunkFlag.TICKING)) {
                this._loadChunkAsync(chunkIndex, playerRefComponent, chunkReference, chunkStore);
                return;
            }
        }
        int flags = -2147483632;
        if (isHot) {
            flags |= 0x4;
        }
        chunkStore.getChunkReferenceAsync(chunkIndex, flags).thenComposeAsync(reference -> {
            if (reference == null || !reference.isValid()) {
                final long stamp = this.loadedLock.writeLock();
                try {
                    this.loading.remove(chunkIndex);
                }
                finally {
                    this.loadedLock.unlockWrite(stamp);
                }
                return CompletableFuture.completedFuture((Object)null);
            }
            else {
                final long stamp2 = this.loadedLock.readLock();
                try {
                    if (!this.loading.contains(chunkIndex)) {
                        return CompletableFuture.completedFuture((Object)null);
                    }
                }
                finally {
                    this.loadedLock.unlockRead(stamp2);
                }
                return (CompletableFuture<Object>)this._loadChunkAsync(chunkIndex, playerRefComponent, reference, chunkStore);
            }
        }, (Executor)world).exceptionallyAsync(throwable -> {
            final long stamp3 = this.loadedLock.writeLock();
            try {
                this.loading.remove(chunkIndex);
            }
            finally {
                this.loadedLock.unlockWrite(stamp3);
            }
            HytaleLogger.getLogger().at(Level.SEVERE).withCause(throwable).log("Failed to load chunk! %s, %s", chunkX, chunkZ);
            return null;
        });
    }
    
    @Nonnull
    private CompletableFuture<Void> _loadChunkAsync(final long chunkIndex, @Nonnull final PlayerRef playerRefComponent, @Nonnull final Ref<ChunkStore> chunkRef, @Nonnull final ChunkStore chunkComponentStore) {
        final List<Packet> packets = new ObjectArrayList<Packet>();
        chunkComponentStore.getStore().fetch(Collections.singletonList(chunkRef), ChunkStore.LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, playerRefComponent, packets);
        final ObjectArrayList<CompletableFuture<Packet>> futurePackets = new ObjectArrayList<CompletableFuture<Packet>>();
        chunkComponentStore.getStore().fetch(Collections.singletonList(chunkRef), ChunkStore.LOAD_FUTURE_PACKETS_DATA_QUERY_SYSTEM_TYPE, playerRefComponent, futurePackets);
        return CompletableFuture.allOf((CompletableFuture<?>[])futurePackets.toArray(CompletableFuture[]::new)).thenAcceptAsync(o -> {
            for (final CompletableFuture<Packet> futurePacket : futurePackets) {
                final Packet packet = futurePacket.join();
                if (packet != null) {
                    packets.add(packet);
                }
            }
            final long writeStamp = this.loadedLock.writeLock();
            try {
                if (this.loading.remove(chunkIndex)) {
                    for (int i = 0; i < packets.size(); ++i) {
                        playerRefComponent.getPacketHandler().write(packets.get(i));
                    }
                    this.loaded.add(chunkIndex);
                }
            }
            finally {
                this.loadedLock.unlockWrite(writeStamp);
            }
        });
    }
    
    @Nonnull
    @Override
    public Component<EntityStore> clone() {
        return new ChunkTracker(this);
    }
    
    static {
        METRICS_REGISTRY = new MetricsRegistry<ChunkTracker>().register("ViewRadius", tracker -> tracker.chunkViewRadius, (Codec<Integer>)Codec.INTEGER).register("SentViewRadius", tracker -> tracker.sentViewRadius, (Codec<Integer>)Codec.INTEGER).register("HotRadius", tracker -> tracker.hotRadius, (Codec<Integer>)Codec.INTEGER).register("LoadedChunksCount", ChunkTracker::getLoadedChunksCount, Codec.INTEGER).register("LoadingChunksCount", ChunkTracker::getLoadingChunksCount, Codec.INTEGER).register("MaxChunksPerSecond", ChunkTracker::getMaxChunksPerSecond, Codec.INTEGER).register("MaxChunksPerTick", ChunkTracker::getMaxChunksPerTick, Codec.INTEGER).register("ReadyForChunks", ChunkTracker::isReadyForChunks, Codec.BOOLEAN).register("LastChunkX", tracker -> tracker.lastChunkX, (Codec<Integer>)Codec.INTEGER).register("LastChunkZ", tracker -> tracker.lastChunkZ, (Codec<Integer>)Codec.INTEGER);
        MAX_FAILURE_BACKOFF_NANOS = TimeUnit.SECONDS.toNanos(10L);
    }
    
    public enum ChunkVisibility
    {
        NONE, 
        HOT, 
        COLD;
    }
}
