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

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

import com.hypixel.hytale.server.core.modules.interaction.interaction.config.data.CollectorTag;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.data.Collector;
import com.hypixel.hytale.function.function.TriFunction;
import java.util.Arrays;
import com.hypixel.hytale.protocol.packets.interaction.CancelInteractionChain;
import com.hypixel.hytale.common.util.ListUtil;
import com.hypixel.hytale.protocol.InteractionCooldown;
import com.hypixel.hytale.protocol.RootInteractionSettings;
import com.hypixel.hytale.protocol.GameMode;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.InteractionTypeUtils;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import com.hypixel.hytale.protocol.InteractionChainData;
import com.hypixel.hytale.protocol.Vector3f;
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
import com.hypixel.hytale.protocol.BlockPosition;
import com.hypixel.hytale.server.core.inventory.ItemStack;
import com.hypixel.hytale.server.core.inventory.Inventory;
import java.util.UUID;
import com.hypixel.hytale.math.vector.Vector4d;
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
import com.hypixel.hytale.math.util.ChunkUtil;
import java.util.Objects;
import com.hypixel.hytale.protocol.Packet;
import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot;
import com.hypixel.hytale.component.ComponentAccessor;
import io.sentry.protocol.SentryId;
import com.hypixel.hytale.metrics.metric.HistoricMetric;
import io.sentry.Sentry;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
import java.util.HashMap;
import io.sentry.protocol.Message;
import io.sentry.SentryLevel;
import io.sentry.SentryEvent;
import com.hypixel.hytale.protocol.WaitForDataFrom;
import com.hypixel.hytale.server.core.modules.interaction.interaction.operation.Operation;
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction;
import com.hypixel.hytale.server.core.modules.time.TimeResource;
import java.util.List;
import com.hypixel.hytale.protocol.InteractionState;
import com.hypixel.hytale.protocol.ForkedChainId;
import java.util.Iterator;
import com.hypixel.hytale.server.core.util.UUIDUtil;
import java.util.concurrent.TimeUnit;
import com.hypixel.hytale.common.util.FormatUtil;
import java.util.logging.Level;
import java.util.Deque;
import it.unimi.dsi.fastutil.objects.ObjectListIterator;
import com.hypixel.hytale.server.core.io.handlers.game.GamePacketHandler;
import java.util.Map;
import com.hypixel.hytale.server.core.entity.entities.Player;
import com.hypixel.hytale.component.Ref;
import com.hypixel.hytale.server.core.universe.world.World;
import com.hypixel.hytale.protocol.InteractionType;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import com.hypixel.hytale.component.CommandBuffer;
import java.util.function.Predicate;
import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain;
import com.hypixel.hytale.protocol.InteractionSyncData;
import it.unimi.dsi.fastutil.objects.ObjectList;
import com.hypixel.hytale.server.core.modules.interaction.IInteractionSimulationHandler;
import javax.annotation.Nullable;
import com.hypixel.hytale.server.core.universe.PlayerRef;
import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler;
import javax.annotation.Nonnull;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
import com.hypixel.hytale.component.Component;

public class InteractionManager implements Component<EntityStore>
{
    public static final double MAX_REACH_DISTANCE = 8.0;
    public static final float[] DEFAULT_CHARGE_TIMES;
    private static final HytaleLogger LOGGER;
    @Nonnull
    private final Int2ObjectMap<InteractionChain> chains;
    @Nonnull
    private final Int2ObjectMap<InteractionChain> unmodifiableChains;
    @Nonnull
    private final CooldownHandler cooldownHandler;
    @Nonnull
    private final LivingEntity entity;
    @Nullable
    private final PlayerRef playerRef;
    private boolean hasRemoteClient;
    @Nonnull
    private final IInteractionSimulationHandler interactionSimulationHandler;
    @Nonnull
    private final ObjectList<InteractionSyncData> tempSyncDataList;
    private int lastServerChainId;
    private int lastClientChainId;
    private long packetQueueTime;
    private final float[] globalTimeShift;
    private final boolean[] globalTimeShiftDirty;
    private boolean timeShiftsDirty;
    private final ObjectList<SyncInteractionChain> syncPackets;
    private long currentTime;
    @Nonnull
    private final ObjectList<InteractionChain> chainStartQueue;
    @Nonnull
    private final Predicate<InteractionChain> cachedTickChain;
    @Nullable
    protected CommandBuffer<EntityStore> commandBuffer;
    
    public InteractionManager(@Nonnull final LivingEntity entity, @Nullable final PlayerRef playerRef, @Nonnull final IInteractionSimulationHandler simulationHandler) {
        this.chains = new Int2ObjectOpenHashMap<InteractionChain>();
        this.unmodifiableChains = Int2ObjectMaps.unmodifiable((Int2ObjectMap<? extends InteractionChain>)this.chains);
        this.cooldownHandler = new CooldownHandler();
        this.tempSyncDataList = new ObjectArrayList<InteractionSyncData>();
        this.globalTimeShift = new float[InteractionType.VALUES.length];
        this.globalTimeShiftDirty = new boolean[InteractionType.VALUES.length];
        this.syncPackets = new ObjectArrayList<SyncInteractionChain>();
        this.currentTime = 1L;
        this.chainStartQueue = new ObjectArrayList<InteractionChain>();
        this.cachedTickChain = this::tickChain;
        this.entity = entity;
        this.playerRef = playerRef;
        this.hasRemoteClient = (playerRef != null);
        this.interactionSimulationHandler = simulationHandler;
    }
    
    @Nonnull
    public Int2ObjectMap<InteractionChain> getChains() {
        return this.unmodifiableChains;
    }
    
    @Nonnull
    public IInteractionSimulationHandler getInteractionSimulationHandler() {
        return this.interactionSimulationHandler;
    }
    
    private long getOperationTimeoutThreshold() {
        if (this.playerRef != null) {
            return this.playerRef.getPacketHandler().getOperationTimeoutThreshold();
        }
        assert this.commandBuffer != null;
        final World world = this.commandBuffer.getExternalData().getWorld();
        return world.getTickStepNanos() / 1000000 * 10;
    }
    
    private boolean waitingForClient(@Nonnull final Ref<EntityStore> ref) {
        assert this.commandBuffer != null;
        final Player playerComponent = this.commandBuffer.getComponent(ref, Player.getComponentType());
        return playerComponent != null && playerComponent.isWaitingForClientReady();
    }
    
    @Deprecated(forRemoval = true)
    public void setHasRemoteClient(final boolean hasRemoteClient) {
        this.hasRemoteClient = hasRemoteClient;
    }
    
    @Deprecated
    public void copyFrom(@Nonnull final InteractionManager interactionManager) {
        this.chains.putAll((Map<?, ?>)interactionManager.chains);
    }
    
    public void tick(@Nonnull final Ref<EntityStore> ref, @Nonnull final CommandBuffer<EntityStore> commandBuffer, final float dt) {
        this.currentTime += commandBuffer.getExternalData().getWorld().getTickStepNanos();
        this.commandBuffer = commandBuffer;
        this.clearAllGlobalTimeShift(dt);
        this.cooldownHandler.tick(dt);
        for (final InteractionChain interactionChain : this.chainStartQueue) {
            this.executeChain0(ref, interactionChain);
        }
        this.chainStartQueue.clear();
        Deque<SyncInteractionChain> packetQueue = null;
        if (this.playerRef != null) {
            packetQueue = ((GamePacketHandler)this.playerRef.getPacketHandler()).getInteractionPacketQueue();
        }
        if (packetQueue != null && !packetQueue.isEmpty()) {
            for (boolean first = true; this.tryConsumePacketQueue(ref, packetQueue) || first; first = false) {
                if (!this.chains.isEmpty()) {
                    this.chains.values().removeIf(this.cachedTickChain);
                }
                float cooldownDt = 0.0f;
                for (final float shift : this.globalTimeShift) {
                    cooldownDt = Math.max(cooldownDt, shift);
                }
                if (cooldownDt > 0.0f) {
                    this.cooldownHandler.tick(cooldownDt);
                }
            }
            this.commandBuffer = null;
            return;
        }
        if (!this.chains.isEmpty()) {
            this.chains.values().removeIf(this.cachedTickChain);
        }
        this.commandBuffer = null;
    }
    
    private boolean tryConsumePacketQueue(@Nonnull final Ref<EntityStore> ref, @Nonnull final Deque<SyncInteractionChain> packetQueue) {
        final Iterator<SyncInteractionChain> it = packetQueue.iterator();
        boolean finished = false;
        boolean desynced = false;
        int highestChainId = -1;
        boolean changed = false;
    Label_0019:
        while (it.hasNext()) {
            final SyncInteractionChain packet = it.next();
            if (packet.desync) {
                final HytaleLogger.Api context = InteractionManager.LOGGER.at(Level.FINE);
                if (context.isEnabled()) {
                    context.log("Client packet flagged as desync");
                }
                desynced = true;
            }
            InteractionChain chain = this.chains.get(packet.chainId);
            if (chain != null && packet.forkedId != null) {
                ForkedChainId id = packet.forkedId;
                while (id != null) {
                    final InteractionChain subChain = chain.getForkedChain(id);
                    if (subChain == null) {
                        InteractionChain.TempChain tempChain = chain.getTempForkedChain(id);
                        if (tempChain == null) {
                            continue Label_0019;
                        }
                        tempChain.setBaseForkedChainId(id);
                        ForkedChainId lastId = id;
                        for (id = id.forkedId; id != null; id = id.forkedId) {
                            tempChain = tempChain.getOrCreateTempForkedChain(id);
                            tempChain.setBaseForkedChainId(id);
                            lastId = id;
                        }
                        tempChain.setForkedChainId(packet.forkedId);
                        tempChain.setBaseForkedChainId(lastId);
                        tempChain.setChainData(packet.data);
                        this.sync(ref, tempChain, packet);
                        changed = true;
                        it.remove();
                        this.packetQueueTime = 0L;
                        continue Label_0019;
                    }
                    else {
                        chain = subChain;
                        id = id.forkedId;
                    }
                }
            }
            highestChainId = Math.max(highestChainId, packet.chainId);
            if (chain == null && !finished) {
                if (this.syncStart(ref, packet)) {
                    changed = true;
                    it.remove();
                    this.packetQueueTime = 0L;
                }
                else {
                    if (!this.waitingForClient(ref)) {
                        long queuedTime;
                        if (this.packetQueueTime == 0L) {
                            this.packetQueueTime = this.currentTime;
                            queuedTime = 0L;
                        }
                        else {
                            queuedTime = this.currentTime - this.packetQueueTime;
                        }
                        HytaleLogger.Api context2 = InteractionManager.LOGGER.at(Level.FINE);
                        if (context2.isEnabled()) {
                            context2.log("Queued chain %d for %s", packet.chainId, FormatUtil.nanosToString(queuedTime));
                        }
                        if (queuedTime > TimeUnit.MILLISECONDS.toNanos(this.getOperationTimeoutThreshold())) {
                            this.sendCancelPacket(packet.chainId, packet.forkedId);
                            it.remove();
                            context2 = InteractionManager.LOGGER.at(Level.FINE);
                            if (context2.isEnabled()) {
                                context2.log("Discarding packet due to queuing for too long: %s", packet);
                            }
                        }
                    }
                    if (desynced) {
                        continue;
                    }
                    finished = true;
                }
            }
            else if (chain != null) {
                this.sync(ref, chain, packet);
                changed = true;
                it.remove();
                this.packetQueueTime = 0L;
            }
            else {
                if (!desynced) {
                    continue;
                }
                this.sendCancelPacket(packet.chainId, packet.forkedId);
                it.remove();
                final HytaleLogger.Api ctx = InteractionManager.LOGGER.at(Level.FINE);
                ctx.log("Discarding packet due to desync: %s", packet);
            }
        }
        if (desynced && !packetQueue.isEmpty()) {
            HytaleLogger.Api ctx2 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx2.isEnabled()) {
                ctx2.log("Discarding previous packets in queue: (before) %d", packetQueue.size());
            }
            packetQueue.removeIf(v -> {
                final boolean shouldRemove = this.getChain(v.chainId, v.forkedId) == null && UUIDUtil.isEmptyOrNull(v.data.proxyId) && v.initial;
                if (shouldRemove) {
                    final HytaleLogger.Api ctx3 = InteractionManager.LOGGER.at(Level.FINE);
                    if (ctx3.isEnabled()) {
                        ctx3.log("Discarding: %s", v);
                    }
                    this.sendCancelPacket(v.chainId, v.forkedId);
                }
                return shouldRemove;
            });
            ctx2 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx2.isEnabled()) {
                ctx2.log("Discarded previous packets in queue: (after) %d", packetQueue.size());
            }
        }
        return changed;
    }
    
    @Nullable
    private InteractionChain getChain(final int chainId, @Nullable final ForkedChainId forkedChainId) {
        InteractionChain chain = this.chains.get(chainId);
        if (chain != null && forkedChainId != null) {
            for (ForkedChainId id = forkedChainId; id != null; id = id.forkedId) {
                final InteractionChain subChain = chain.getForkedChain(id);
                if (subChain == null) {
                    return null;
                }
                chain = subChain;
            }
        }
        return chain;
    }
    
    private boolean tickChain(@Nonnull final InteractionChain chain) {
        if (chain.wasPreTicked()) {
            chain.setPreTicked(false);
            return false;
        }
        if (!this.hasRemoteClient) {
            chain.updateSimulatedState();
        }
        chain.getForkedChains().values().removeIf(this.cachedTickChain);
        final Ref<EntityStore> ref = this.entity.getReference();
        assert ref != null;
        if (chain.getServerState() == InteractionState.NotFinished) {
            final int baseOpIndex = chain.getOperationIndex();
            try {
                this.doTickChain(ref, chain);
            }
            catch (final ChainCancelledException e) {
                chain.setServerState(e.state);
                chain.setClientState(e.state);
                chain.updateServerState();
                if (!this.hasRemoteClient) {
                    chain.updateSimulatedState();
                }
                if (chain.requiresClient()) {
                    this.sendSyncPacket(chain, baseOpIndex, this.tempSyncDataList);
                    this.sendCancelPacket(chain);
                }
            }
            if (chain.getServerState() != InteractionState.NotFinished) {
                HytaleLogger.Api context = InteractionManager.LOGGER.at(Level.FINE);
                if (context.isEnabled()) {
                    context.log("Server finished chain: %d-%s, %s in %fs", chain.getChainId(), chain.getForkedChainId(), chain, chain.getTimeInSeconds());
                }
                if (!chain.requiresClient() || chain.getClientState() != InteractionState.NotFinished) {
                    context = InteractionManager.LOGGER.at(Level.FINE);
                    if (context.isEnabled()) {
                        context.log("Remove Chain: %d-%s, %s", chain.getChainId(), chain.getForkedChainId(), chain);
                    }
                    this.handleCancelledChain(ref, chain);
                    chain.onCompletion(this.cooldownHandler, this.hasRemoteClient);
                    return chain.getForkedChains().isEmpty();
                }
            }
            else if (chain.getClientState() != InteractionState.NotFinished && !this.waitingForClient(ref)) {
                if (chain.getWaitingForServerFinished() == 0L) {
                    chain.setWaitingForServerFinished(this.currentTime);
                }
                final long waitMillis = TimeUnit.NANOSECONDS.toMillis(this.currentTime - chain.getWaitingForServerFinished());
                final HytaleLogger.Api context2 = InteractionManager.LOGGER.at(Level.FINE);
                if (context2.isEnabled()) {
                    context2.log("Client finished chain but server hasn't! %d, %s, %s", chain.getChainId(), chain, waitMillis);
                }
                final long threshold = this.getOperationTimeoutThreshold();
                if (waitMillis > threshold) {
                    InteractionManager.LOGGER.at(Level.SEVERE).log("Client finished chain earlier than server! %d, %s", chain.getChainId(), chain);
                }
            }
            return false;
        }
        if (!chain.requiresClient() || chain.getClientState() != InteractionState.NotFinished) {
            InteractionManager.LOGGER.at(Level.FINE).log("Remove Chain: %d, %s", chain.getChainId(), chain);
            this.handleCancelledChain(ref, chain);
            chain.onCompletion(this.cooldownHandler, this.hasRemoteClient);
            return chain.getForkedChains().isEmpty();
        }
        if (!this.waitingForClient(ref)) {
            if (chain.getWaitingForClientFinished() == 0L) {
                chain.setWaitingForClientFinished(this.currentTime);
            }
            final long waitMillis2 = TimeUnit.NANOSECONDS.toMillis(this.currentTime - chain.getWaitingForClientFinished());
            final HytaleLogger.Api context3 = InteractionManager.LOGGER.at(Level.FINE);
            if (context3.isEnabled()) {
                context3.log("Server finished chain but client hasn't! %d, %s, %s", chain.getChainId(), chain, waitMillis2);
            }
            final long threshold2 = this.getOperationTimeoutThreshold();
            final TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType());
            if (timeResource.getTimeDilationModifier() == 1.0f && waitMillis2 > threshold2) {
                this.sendCancelPacket(chain);
                return chain.getForkedChains().isEmpty();
            }
        }
        return false;
    }
    
    private void handleCancelledChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionChain chain) {
        assert this.commandBuffer != null;
        final RootInteraction root = chain.getRootInteraction();
        final int maxOperations = root.getOperationMax();
        if (chain.getOperationCounter() >= maxOperations) {
            return;
        }
        final InteractionEntry entry = chain.getInteraction(chain.getOperationIndex());
        if (entry == null) {
            return;
        }
        final Operation operation = root.getOperation(chain.getOperationCounter());
        if (operation == null) {
            throw new IllegalStateException("Failed to find operation during simulation tick of chain '" + root.getId());
        }
        final InteractionContext context = chain.getContext();
        entry.getServerState().state = InteractionState.Failed;
        if (entry.getClientState() != null) {
            entry.getClientState().state = InteractionState.Failed;
        }
        try {
            context.initEntry(chain, entry, this.entity);
            final TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType());
            operation.handle(ref, false, entry.getTimeInSeconds(this.currentTime) * timeResource.getTimeDilationModifier(), chain.getType(), context);
        }
        finally {
            context.deinitEntry(chain, entry, this.entity);
        }
        chain.setOperationCounter(maxOperations);
    }
    
    private void doTickChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionChain chain) {
        final ObjectList<InteractionSyncData> interactionData = this.tempSyncDataList;
        interactionData.clear();
        RootInteraction root = chain.getRootInteraction();
        int maxOperations = root.getOperationMax();
        int currentOp = chain.getOperationCounter();
        final int baseOpIndex = chain.getOperationIndex();
        int callDepth = chain.getCallDepth();
        if (chain.consumeFirstRun()) {
            if (chain.getForkedChainId() == null) {
                chain.setTimeShift(this.getGlobalTimeShift(chain.getType()));
            }
            else {
                final InteractionChain parent = this.chains.get(chain.getChainId());
                chain.setFirstRun(parent != null && parent.isFirstRun());
            }
        }
        else {
            chain.setTimeShift(0.0f);
        }
        if (!chain.getContext().getEntity().isValid()) {
            throw new ChainCancelledException(chain.getServerState());
        }
        while (true) {
            final Operation simOp = this.hasRemoteClient ? null : root.getOperation(chain.getSimulatedOperationCounter());
            final WaitForDataFrom simWaitFrom = (simOp != null) ? simOp.getWaitForDataFrom() : null;
            final long tickTime = this.currentTime;
            if (!this.hasRemoteClient && simWaitFrom != WaitForDataFrom.Server) {
                this.simulationTick(ref, chain, tickTime);
            }
            interactionData.add(this.serverTick(ref, chain, tickTime));
            if (!chain.getContext().getEntity().isValid() && chain.getServerState() != InteractionState.Finished && chain.getServerState() != InteractionState.Failed) {
                throw new ChainCancelledException(chain.getServerState());
            }
            if (!this.hasRemoteClient && simWaitFrom == WaitForDataFrom.Server) {
                this.simulationTick(ref, chain, tickTime);
            }
            if (!this.hasRemoteClient) {
                if (chain.getRootInteraction() != chain.getSimulatedRootInteraction()) {
                    throw new IllegalStateException("Simulation and server tick are not in sync (root interaction).\n" + chain.getRootInteraction().getId() + " vs " + String.valueOf(chain.getSimulatedRootInteraction()));
                }
                if (chain.getOperationCounter() != chain.getSimulatedOperationCounter()) {
                    throw new IllegalStateException("Simulation and server tick are not in sync (operation position).\nRoot: " + chain.getRootInteraction().getId() + "\nCounter: " + chain.getOperationCounter() + " vs " + chain.getSimulatedOperationCounter() + "\nIndex: " + chain.getOperationIndex());
                }
            }
            if (callDepth != chain.getCallDepth()) {
                callDepth = chain.getCallDepth();
                root = chain.getRootInteraction();
                maxOperations = root.getOperationMax();
            }
            else if (currentOp == chain.getOperationCounter()) {
                break;
            }
            chain.nextOperationIndex();
            currentOp = chain.getOperationCounter();
            if (currentOp < maxOperations) {
                continue;
            }
            while (callDepth > 0) {
                chain.popRoot();
                callDepth = chain.getCallDepth();
                currentOp = chain.getOperationCounter();
                root = chain.getRootInteraction();
                maxOperations = root.getOperationMax();
                if (currentOp < maxOperations || callDepth == 0) {
                    break;
                }
            }
            if (callDepth == 0 && currentOp >= maxOperations) {
                break;
            }
        }
        chain.updateServerState();
        if (!this.hasRemoteClient) {
            chain.updateSimulatedState();
        }
        if (chain.requiresClient()) {
            this.sendSyncPacket(chain, baseOpIndex, interactionData);
        }
    }
    
    @Nullable
    private InteractionSyncData serverTick(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionChain chain, final long tickTime) {
        assert this.commandBuffer != null;
        final RootInteraction root = chain.getRootInteraction();
        final Operation operation = root.getOperation(chain.getOperationCounter());
        assert operation != null;
        final InteractionEntry entry = chain.getOrCreateInteractionEntry(chain.getOperationIndex());
        InteractionSyncData returnData = null;
        boolean wasWrong = entry.consumeDesyncFlag();
        if (entry.getClientState() == null) {
            wasWrong |= !entry.setClientState(chain.removeInteractionSyncData(chain.getOperationIndex()));
        }
        if (wasWrong) {
            returnData = entry.getServerState();
            chain.flagDesync();
            chain.clearInteractionSyncData(chain.getOperationIndex());
        }
        final TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType());
        final float tickTimeDilation = timeResource.getTimeDilationModifier();
        if (operation.getWaitForDataFrom() != WaitForDataFrom.Client || entry.getClientState() != null) {
            final int serverDataHashCode = entry.getServerDataHashCode();
            final InteractionContext context = chain.getContext();
            float time = entry.getTimeInSeconds(tickTime);
            boolean firstRun = false;
            if (entry.getTimestamp() == 0L) {
                time = chain.getTimeShift();
                entry.setTimestamp(tickTime, time);
                firstRun = true;
            }
            time *= tickTimeDilation;
            try {
                context.initEntry(chain, entry, this.entity);
                operation.tick(ref, this.entity, firstRun, time, chain.getType(), context, this.cooldownHandler);
            }
            finally {
                context.deinitEntry(chain, entry, this.entity);
            }
            final InteractionSyncData serverData = entry.getServerState();
            if (firstRun || serverDataHashCode != entry.getServerDataHashCode()) {
                returnData = serverData;
            }
            try {
                context.initEntry(chain, entry, this.entity);
                operation.handle(ref, firstRun, time, chain.getType(), context);
            }
            finally {
                context.deinitEntry(chain, entry, this.entity);
            }
            this.removeInteractionIfFinished(ref, chain, entry);
            return returnData;
        }
        if (this.waitingForClient(ref)) {
            return null;
        }
        if (entry.getWaitingForSyncData() == 0L) {
            entry.setWaitingForSyncData(this.currentTime);
        }
        final long waitMillis = TimeUnit.NANOSECONDS.toMillis(this.currentTime - entry.getWaitingForSyncData());
        final HytaleLogger.Api context2 = InteractionManager.LOGGER.at(Level.FINE);
        if (context2.isEnabled()) {
            context2.log("Wait for interaction clientData: %d, %s, %s", chain.getOperationIndex(), entry, waitMillis);
        }
        final long threshold = this.getOperationTimeoutThreshold();
        if (tickTimeDilation == 1.0f && waitMillis > threshold) {
            final SentryEvent event = new SentryEvent();
            event.setLevel(SentryLevel.ERROR);
            final Message message = new Message();
            message.setMessage("Client failed to send client data, ending early to prevent desync");
            final HashMap<String, Object> unknown = new HashMap<String, Object>();
            unknown.put("Threshold", threshold);
            unknown.put("Wait Millis", waitMillis);
            unknown.put("Current Root", (chain.getRootInteraction() != null) ? chain.getRootInteraction().getId() : "<null>");
            final Operation innerOp = operation.getInnerOperation();
            unknown.put("Current Op", innerOp.getClass().getName());
            if (innerOp instanceof final Interaction interaction) {
                unknown.put("Current Interaction", interaction.getId());
            }
            unknown.put("Current Index", chain.getOperationIndex());
            unknown.put("Current Op Counter", chain.getOperationCounter());
            final HistoricMetric metric = ref.getStore().getExternalData().getWorld().getBufferedTickLengthMetricSet();
            final long[] periods = metric.getPeriodsNanos();
            for (int i = 0; i < periods.length; ++i) {
                final String length = FormatUtil.timeUnitToString(periods[i], TimeUnit.NANOSECONDS, true);
                final double average = metric.getAverage(i);
                final long min = metric.calculateMin(i);
                final long max = metric.calculateMax(i);
                final String value = FormatUtil.simpleTimeUnitFormat(min, average, max, TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS, 3);
                unknown.put(String.format("World Perf %s", length), value);
            }
            event.setExtras(unknown);
            event.setMessage(message);
            final SentryId eventId = Sentry.captureEvent(event);
            InteractionManager.LOGGER.atWarning().log("Client failed to send client data, ending early to prevent desync. %s", eventId);
            chain.setServerState(InteractionState.Failed);
            chain.setClientState(InteractionState.Failed);
            this.sendCancelPacket(chain);
            return null;
        }
        if (entry.consumeSendInitial() || wasWrong) {
            returnData = entry.getServerState();
        }
        return returnData;
    }
    
    private void removeInteractionIfFinished(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionChain chain, @Nonnull final InteractionEntry entry) {
        if (chain.getOperationIndex() == entry.getIndex() && entry.getServerState().state != InteractionState.NotFinished) {
            chain.setFinalState(entry.getServerState().state);
        }
        if (entry.getServerState().state != InteractionState.NotFinished) {
            InteractionManager.LOGGER.at(Level.FINE).log("Server finished interaction: %d, %s", entry.getIndex(), entry);
            if (!chain.requiresClient() || (entry.getClientState() != null && entry.getClientState().state != InteractionState.NotFinished)) {
                InteractionManager.LOGGER.at(Level.FINER).log("Remove Interaction: %d, %s", entry.getIndex(), entry);
                chain.removeInteractionEntry(this, entry.getIndex());
            }
        }
        else if (entry.getClientState() != null && entry.getClientState().state != InteractionState.NotFinished && !this.waitingForClient(ref)) {
            if (entry.getWaitingForServerFinished() == 0L) {
                entry.setWaitingForServerFinished(this.currentTime);
            }
            final long waitMillis = TimeUnit.NANOSECONDS.toMillis(this.currentTime - entry.getWaitingForServerFinished());
            final HytaleLogger.Api context = InteractionManager.LOGGER.at(Level.FINE);
            if (context.isEnabled()) {
                context.log("Client finished interaction but server hasn't! %s, %d, %s, %s", entry.getClientState().state, entry.getIndex(), entry, waitMillis);
            }
            final long threshold = this.getOperationTimeoutThreshold();
            if (waitMillis > threshold) {
                final HytaleLogger.Api ctx = InteractionManager.LOGGER.at(Level.SEVERE);
                if (ctx.isEnabled()) {
                    ctx.log("Client finished interaction earlier than server! %d, %s", entry.getIndex(), entry);
                }
            }
        }
    }
    
    private void simulationTick(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionChain chain, final long tickTime) {
        assert this.commandBuffer != null;
        final RootInteraction rootInteraction = chain.getRootInteraction();
        final Operation operation = rootInteraction.getOperation(chain.getSimulatedOperationCounter());
        if (operation == null) {
            throw new IllegalStateException("Failed to find operation during simulation tick of chain '" + rootInteraction.getId());
        }
        final InteractionEntry entry = chain.getOrCreateInteractionEntry(chain.getClientOperationIndex());
        final InteractionContext context = chain.getContext();
        entry.setUseSimulationState(true);
        try {
            context.initEntry(chain, entry, this.entity);
            float time = entry.getTimeInSeconds(tickTime);
            boolean firstRun = false;
            if (entry.getTimestamp() == 0L) {
                time = chain.getTimeShift();
                entry.setTimestamp(tickTime, time);
                firstRun = true;
            }
            final TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType());
            final float tickTimeDilation = timeResource.getTimeDilationModifier();
            time *= tickTimeDilation;
            operation.simulateTick(ref, this.entity, firstRun, time, chain.getType(), context, this.cooldownHandler);
        }
        finally {
            context.deinitEntry(chain, entry, this.entity);
            entry.setUseSimulationState(false);
        }
        if (!entry.setClientState(entry.getSimulationState())) {
            throw new RuntimeException("Simulation failed");
        }
        this.removeInteractionIfFinished(ref, chain, entry);
    }
    
    private boolean syncStart(@Nonnull final Ref<EntityStore> ref, @Nonnull final SyncInteractionChain packet) {
        assert this.commandBuffer != null;
        final int index = packet.chainId;
        if (!packet.initial) {
            if (packet.forkedId == null) {
                final HytaleLogger.Api ctx = InteractionManager.LOGGER.at(Level.FINE);
                if (ctx.isEnabled()) {
                    ctx.log("Got syncStart for %d-%s but packet wasn't the first.", index, packet.forkedId);
                }
            }
            return true;
        }
        if (packet.forkedId != null) {
            final HytaleLogger.Api ctx = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx.isEnabled()) {
                ctx.log("Can't start a forked chain from the client: %d %s", index, packet.forkedId);
            }
            return true;
        }
        final InteractionType type = packet.interactionType;
        if (index <= 0) {
            final HytaleLogger.Api ctx2 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx2.isEnabled()) {
                ctx2.log("Invalid client chainId! Got %d but client id's should be > 0", index);
            }
            this.sendCancelPacket(index, packet.forkedId);
            return true;
        }
        if (index <= this.lastClientChainId) {
            final HytaleLogger.Api ctx2 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx2.isEnabled()) {
                ctx2.log("Invalid client chainId! The last clientChainId was %d but just got %d", this.lastClientChainId, index);
            }
            this.sendCancelPacket(index, packet.forkedId);
            return true;
        }
        final UUID proxyId = packet.data.proxyId;
        InteractionContext context;
        if (!UUIDUtil.isEmptyOrNull(proxyId)) {
            final World world = this.commandBuffer.getExternalData().getWorld();
            final Ref<EntityStore> proxyTarget = world.getEntityStore().getRefFromUUID(proxyId);
            if (proxyTarget == null) {
                if (this.packetQueueTime != 0L && this.currentTime - this.packetQueueTime > TimeUnit.MILLISECONDS.toNanos(this.getOperationTimeoutThreshold()) / 2L) {
                    final HytaleLogger.Api ctx3 = InteractionManager.LOGGER.at(Level.FINE);
                    if (ctx3.isEnabled()) {
                        ctx3.log("Proxy entity never spawned");
                    }
                    this.sendCancelPacket(index, packet.forkedId);
                    return true;
                }
                return false;
            }
            else {
                context = InteractionContext.forProxyEntity(this, this.entity, proxyTarget);
            }
        }
        else {
            context = InteractionContext.forInteraction(this, ref, type, packet.equipSlot, this.commandBuffer);
        }
        final String rootInteractionId = context.getRootInteractionId(type);
        if (rootInteractionId == null) {
            final HytaleLogger.Api ctx3 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx3.isEnabled()) {
                ctx3.log("Missing root interaction: %d, %s, %s", index, this.entity.getInventory().getItemInHand(), type);
            }
            this.sendCancelPacket(index, packet.forkedId);
            return true;
        }
        final RootInteraction rootInteraction = RootInteraction.getRootInteractionOrUnknown(rootInteractionId);
        if (rootInteraction == null) {
            return false;
        }
        if (!this.applyRules(context, packet.data, type, rootInteraction)) {
            return false;
        }
        final Inventory entityInventory = this.entity.getInventory();
        final ItemStack itemInHand = entityInventory.getActiveHotbarItem();
        final ItemStack utilityItem = entityInventory.getUtilityItem();
        final String serverItemInHandId = (itemInHand != null) ? itemInHand.getItemId() : null;
        final String serverUtilityItemId = (utilityItem != null) ? utilityItem.getItemId() : null;
        if (packet.activeHotbarSlot != entityInventory.getActiveHotbarSlot()) {
            final HytaleLogger.Api ctx4 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx4.isEnabled()) {
                ctx4.log("Active slot miss match: %d, %d != %d, %s, %s, %s", index, entityInventory.getActiveHotbarSlot(), packet.activeHotbarSlot, serverItemInHandId, packet.itemInHandId, type);
            }
            this.sendCancelPacket(index, packet.forkedId);
            if (this.playerRef != null) {
                this.playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, entityInventory.getActiveHotbarSlot()));
            }
            return true;
        }
        if (packet.activeUtilitySlot != entityInventory.getActiveUtilitySlot()) {
            final HytaleLogger.Api ctx4 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx4.isEnabled()) {
                ctx4.log("Active slot miss match: %d, %d != %d, %s, %s, %s", index, entityInventory.getActiveUtilitySlot(), packet.activeUtilitySlot, serverItemInHandId, packet.itemInHandId, type);
            }
            this.sendCancelPacket(index, packet.forkedId);
            if (this.playerRef != null) {
                this.playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-5, entityInventory.getActiveUtilitySlot()));
            }
            return true;
        }
        if (!Objects.equals(serverItemInHandId, packet.itemInHandId)) {
            final HytaleLogger.Api ctx4 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx4.isEnabled()) {
                ctx4.log("ItemInHand miss match: %d, %s, %s, %s", index, serverItemInHandId, packet.itemInHandId, type);
            }
            this.sendCancelPacket(index, packet.forkedId);
            return true;
        }
        if (!Objects.equals(serverUtilityItemId, packet.utilityItemId)) {
            final HytaleLogger.Api ctx4 = InteractionManager.LOGGER.at(Level.FINE);
            if (ctx4.isEnabled()) {
                ctx4.log("UtilityItem miss match: %d, %s, %s, %s", index, serverUtilityItemId, packet.utilityItemId, type);
            }
            this.sendCancelPacket(index, packet.forkedId);
            return true;
        }
        if (this.isOnCooldown(ref, type, rootInteraction, true)) {
            return false;
        }
        final InteractionChain chain = this.initChain(packet.data, type, context, rootInteraction, null, true);
        chain.setChainId(index);
        this.sync(ref, chain, packet);
        final World world2 = this.commandBuffer.getExternalData().getWorld();
        if (packet.data.blockPosition != null) {
            final BlockPosition targetBlock = world2.getBaseBlock(packet.data.blockPosition);
            context.getMetaStore().putMetaObject(Interaction.TARGET_BLOCK, targetBlock);
            context.getMetaStore().putMetaObject(Interaction.TARGET_BLOCK_RAW, packet.data.blockPosition);
            if (!packet.data.blockPosition.equals(targetBlock)) {
                final WorldChunk otherChunk = world2.getChunkIfInMemory(ChunkUtil.indexChunkFromBlock(packet.data.blockPosition.x, packet.data.blockPosition.z));
                if (otherChunk == null) {
                    final HytaleLogger.Api ctx5 = InteractionManager.LOGGER.at(Level.FINE);
                    if (ctx5.isEnabled()) {
                        ctx5.log("Unloaded chunk interacted with: %d, %s", index, type);
                    }
                    this.sendCancelPacket(index, packet.forkedId);
                    return true;
                }
                final int blockId = world2.getBlock(targetBlock.x, targetBlock.y, targetBlock.z);
                final int otherBlockId = world2.getBlock(packet.data.blockPosition.x, packet.data.blockPosition.y, packet.data.blockPosition.z);
                if (blockId != otherBlockId) {
                    otherChunk.setBlock(packet.data.blockPosition.x, packet.data.blockPosition.y, packet.data.blockPosition.z, 0, BlockType.EMPTY, 0, 0, 1052);
                }
            }
        }
        if (packet.data.entityId >= 0) {
            final EntityStore entityComponentStore = world2.getEntityStore();
            final Ref<EntityStore> entityReference = entityComponentStore.getRefFromNetworkId(packet.data.entityId);
            if (entityReference != null) {
                context.getMetaStore().putMetaObject(Interaction.TARGET_ENTITY, entityReference);
            }
        }
        if (packet.data.targetSlot != Integer.MIN_VALUE) {
            context.getMetaStore().putMetaObject(Interaction.TARGET_SLOT, packet.data.targetSlot);
        }
        if (packet.data.hitLocation != null) {
            final Vector3f hit = packet.data.hitLocation;
            context.getMetaStore().putMetaObject(Interaction.HIT_LOCATION, new Vector4d(hit.x, hit.y, hit.z, 1.0));
        }
        if (packet.data.hitDetail != null) {
            context.getMetaStore().putMetaObject(Interaction.HIT_DETAIL, packet.data.hitDetail);
        }
        this.lastClientChainId = index;
        if (!this.tickChain(chain)) {
            chain.setPreTicked(true);
            this.chains.put(index, chain);
        }
        return true;
    }
    
    public void sync(@Nonnull final Ref<EntityStore> ref, @Nonnull final ChainSyncStorage chainSyncStorage, @Nonnull final SyncInteractionChain packet) {
        assert this.commandBuffer != null;
        if (packet.newForks != null) {
            for (final SyncInteractionChain fork : packet.newForks) {
                chainSyncStorage.syncFork(ref, this, fork);
            }
        }
        if (packet.interactionData == null) {
            chainSyncStorage.setClientState(packet.state);
            return;
        }
        for (int i = 0; i < packet.interactionData.length; ++i) {
            final InteractionSyncData syncData = packet.interactionData[i];
            if (syncData != null) {
                final int index = packet.operationBaseIndex + i;
                if (!chainSyncStorage.isSyncDataOutOfOrder(index)) {
                    final InteractionEntry interaction = chainSyncStorage.getInteraction(index);
                    if (interaction != null && chainSyncStorage instanceof InteractionChain) {
                        final InteractionChain interactionChain = (InteractionChain)chainSyncStorage;
                        if ((interaction.getClientState() != null && interaction.getClientState().state != InteractionState.NotFinished && syncData.state == InteractionState.NotFinished) || !interaction.setClientState(syncData)) {
                            chainSyncStorage.clearInteractionSyncData(index);
                            interaction.flagDesync();
                            interactionChain.flagDesync();
                            return;
                        }
                        chainSyncStorage.updateSyncPosition(index);
                        final HytaleLogger.Api context = InteractionManager.LOGGER.at(Level.FINEST);
                        if (context.isEnabled()) {
                            final TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType());
                            final float tickTimeDilation = timeResource.getTimeDilationModifier();
                            context.log("%d, %d: Time (Sync) - Server: %s vs Client: %s", packet.chainId, index, interaction.getTimeInSeconds(this.currentTime) * tickTimeDilation, interaction.getClientState().progress);
                        }
                        this.removeInteractionIfFinished(ref, interactionChain, interaction);
                    }
                    else {
                        chainSyncStorage.putInteractionSyncData(index, syncData);
                    }
                }
            }
        }
        final int last = packet.operationBaseIndex + packet.interactionData.length;
        chainSyncStorage.clearInteractionSyncData(last);
        chainSyncStorage.setClientState(packet.state);
    }
    
    public boolean canRun(@Nonnull final InteractionType type, @Nonnull final RootInteraction rootInteraction) {
        return this.canRun(type, (short)(-1), rootInteraction);
    }
    
    public boolean canRun(@Nonnull final InteractionType type, final short equipSlot, @Nonnull final RootInteraction rootInteraction) {
        return applyRules(null, type, equipSlot, rootInteraction, this.chains, null);
    }
    
    public boolean applyRules(@Nonnull final InteractionContext context, @Nonnull final InteractionChainData data, @Nonnull final InteractionType type, @Nonnull final RootInteraction rootInteraction) {
        final List<InteractionChain> chainsToCancel = new ObjectArrayList<InteractionChain>();
        if (!applyRules(data, type, context.getHeldItemSlot(), rootInteraction, this.chains, chainsToCancel)) {
            return false;
        }
        for (final InteractionChain interactionChain : chainsToCancel) {
            this.cancelChains(interactionChain);
        }
        return true;
    }
    
    public void cancelChains(@Nonnull final InteractionChain chain) {
        chain.setServerState(InteractionState.Failed);
        chain.setClientState(InteractionState.Failed);
        this.sendCancelPacket(chain);
        for (final InteractionChain fork : chain.getForkedChains().values()) {
            this.cancelChains(fork);
        }
    }
    
    private static boolean applyRules(@Nullable final InteractionChainData data, @Nonnull final InteractionType type, final int heldItemSlot, @Nullable final RootInteraction rootInteraction, @Nonnull final Map<?, InteractionChain> chains, @Nullable final List<InteractionChain> chainsToCancel) {
        if (chains.isEmpty() || rootInteraction == null) {
            return true;
        }
        for (final InteractionChain chain : chains.values()) {
            if (chain.getForkedChainId() != null && !chain.isPredicted()) {
                continue;
            }
            if (data != null && !Objects.equals(chain.getChainData().proxyId, data.proxyId)) {
                continue;
            }
            if (type == InteractionType.Equipped && chain.getType() == InteractionType.Equipped && chain.getContext().getHeldItemSlot() != heldItemSlot) {
                continue;
            }
            if (chain.getServerState() == InteractionState.NotFinished) {
                final RootInteraction currentRoot = chain.getRootInteraction();
                final Operation currentOp = currentRoot.getOperation(chain.getOperationCounter());
                if (rootInteraction.getRules().validateInterrupts(type, rootInteraction.getData().getTags(), chain.getType(), currentRoot.getData().getTags(), currentRoot.getRules())) {
                    if (chainsToCancel != null) {
                        chainsToCancel.add(chain);
                    }
                }
                else if (currentOp != null && currentOp.getRules() != null && rootInteraction.getRules().validateInterrupts(type, rootInteraction.getData().getTags(), chain.getType(), currentOp.getTags(), currentOp.getRules())) {
                    if (chainsToCancel != null) {
                        chainsToCancel.add(chain);
                    }
                }
                else {
                    if (rootInteraction.getRules().validateBlocked(type, rootInteraction.getData().getTags(), chain.getType(), currentRoot.getData().getTags(), currentRoot.getRules())) {
                        return false;
                    }
                    if (currentOp != null && currentOp.getRules() != null && rootInteraction.getRules().validateBlocked(type, rootInteraction.getData().getTags(), chain.getType(), currentOp.getTags(), currentOp.getRules())) {
                        return false;
                    }
                }
            }
            if ((chainsToCancel == null || chainsToCancel.isEmpty()) && !applyRules(data, type, heldItemSlot, rootInteraction, chain.getForkedChains(), chainsToCancel)) {
                return false;
            }
        }
        return true;
    }
    
    public boolean tryStartChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final CommandBuffer<EntityStore> commandBuffer, @Nonnull final InteractionType type, @Nonnull final InteractionContext context, @Nonnull final RootInteraction rootInteraction) {
        final InteractionChain chain = this.initChain(type, context, rootInteraction, false);
        if (!this.applyRules(context, chain.getChainData(), type, rootInteraction)) {
            return false;
        }
        this.executeChain(ref, commandBuffer, chain);
        return true;
    }
    
    public void startChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final CommandBuffer<EntityStore> commandBuffer, @Nonnull final InteractionType type, @Nonnull final InteractionContext context, @Nonnull final RootInteraction rootInteraction) {
        final InteractionChain chain = this.initChain(type, context, rootInteraction, false);
        this.executeChain(ref, commandBuffer, chain);
    }
    
    @Nonnull
    public InteractionChain initChain(@Nonnull final InteractionType type, @Nonnull final InteractionContext context, @Nonnull final RootInteraction rootInteraction, final boolean forceRemoteSync) {
        return this.initChain(type, context, rootInteraction, -1, null, forceRemoteSync);
    }
    
    @Nonnull
    public InteractionChain initChain(@Nonnull final InteractionType type, @Nonnull final InteractionContext context, @Nonnull final RootInteraction rootInteraction, final int entityId, @Nullable final BlockPosition blockPosition, final boolean forceRemoteSync) {
        final InteractionChainData data = new InteractionChainData(entityId, UUIDUtil.EMPTY_UUID, null, null, blockPosition, Integer.MIN_VALUE, null);
        return this.initChain(data, type, context, rootInteraction, null, forceRemoteSync);
    }
    
    @Nonnull
    public InteractionChain initChain(@Nonnull final InteractionChainData data, @Nonnull final InteractionType type, @Nonnull final InteractionContext context, @Nonnull final RootInteraction rootInteraction, @Nullable final Runnable onCompletion, final boolean forceRemoteSync) {
        return new InteractionChain(type, context, data, rootInteraction, onCompletion, forceRemoteSync || !this.hasRemoteClient);
    }
    
    public void queueExecuteChain(@Nonnull final InteractionChain chain) {
        this.chainStartQueue.add(chain);
    }
    
    public void executeChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final CommandBuffer<EntityStore> commandBuffer, @Nonnull final InteractionChain chain) {
        this.commandBuffer = commandBuffer;
        this.executeChain0(ref, chain);
        this.commandBuffer = null;
    }
    
    private void executeChain0(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionChain chain) {
        if (this.isOnCooldown(ref, chain.getType(), chain.getInitialRootInteraction(), false)) {
            chain.setServerState(InteractionState.Failed);
            chain.setClientState(InteractionState.Failed);
            return;
        }
        final int lastServerChainId = this.lastServerChainId - 1;
        this.lastServerChainId = lastServerChainId;
        int index = lastServerChainId;
        if (index >= 0) {
            final int lastServerChainId2 = -1;
            this.lastServerChainId = lastServerChainId2;
            index = lastServerChainId2;
        }
        chain.setChainId(index);
        if (this.tickChain(chain)) {
            return;
        }
        InteractionManager.LOGGER.at(Level.FINE).log("Add Chain: %d, %s", index, chain);
        chain.setPreTicked(true);
        this.chains.put(index, chain);
    }
    
    private boolean isOnCooldown(@Nonnull final Ref<EntityStore> ref, @Nonnull final InteractionType type, @Nonnull final RootInteraction root, final boolean remote) {
        assert this.commandBuffer != null;
        InteractionCooldown cooldown = root.getCooldown();
        String cooldownId = root.getId();
        float cooldownTime = InteractionTypeUtils.getDefaultCooldown(type);
        float[] cooldownChargeTimes = InteractionManager.DEFAULT_CHARGE_TIMES;
        boolean interruptRecharge = false;
        if (cooldown != null) {
            cooldownTime = cooldown.cooldown;
            if (cooldown.chargeTimes != null && cooldown.chargeTimes.length > 0) {
                cooldownChargeTimes = cooldown.chargeTimes;
            }
            if (cooldown.cooldownId != null) {
                cooldownId = cooldown.cooldownId;
            }
            if (cooldown.interruptRecharge) {
                interruptRecharge = true;
            }
            if (cooldown.clickBypass && remote) {
                this.cooldownHandler.resetCooldown(cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge);
                return false;
            }
        }
        final Player playerComponent = this.commandBuffer.getComponent(ref, Player.getComponentType());
        final GameMode gameMode = (playerComponent != null) ? playerComponent.getGameMode() : GameMode.Adventure;
        final RootInteractionSettings settings = root.getSettings().get(gameMode);
        if (settings != null) {
            cooldown = settings.cooldown;
            if (cooldown != null) {
                cooldownTime = cooldown.cooldown;
                if (cooldown.chargeTimes != null && cooldown.chargeTimes.length > 0) {
                    cooldownChargeTimes = cooldown.chargeTimes;
                }
                if (cooldown.cooldownId != null) {
                    cooldownId = cooldown.cooldownId;
                }
                if (cooldown.interruptRecharge) {
                    interruptRecharge = true;
                }
                if (cooldown.clickBypass && remote) {
                    this.cooldownHandler.resetCooldown(cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge);
                    return false;
                }
            }
            if (settings.allowSkipChainOnClick && remote) {
                this.cooldownHandler.resetCooldown(cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge);
                return false;
            }
        }
        return this.cooldownHandler.isOnCooldown(root, cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge);
    }
    
    public void tryRunHeldInteraction(@Nonnull final Ref<EntityStore> ref, @Nonnull final CommandBuffer<EntityStore> commandBuffer, @Nonnull final InteractionType type) {
        this.tryRunHeldInteraction(ref, commandBuffer, type, (short)(-1));
    }
    
    public void tryRunHeldInteraction(@Nonnull final Ref<EntityStore> ref, @Nonnull final CommandBuffer<EntityStore> commandBuffer, @Nonnull final InteractionType type, final short equipSlot) {
        final Inventory inventory = this.entity.getInventory();
        ItemStack itemStack = null;
        switch (type) {
            case Held: {
                itemStack = inventory.getItemInHand();
                break;
            }
            case HeldOffhand: {
                itemStack = inventory.getUtilityItem();
                break;
            }
            case Equipped: {
                if (equipSlot == -1) {
                    throw new IllegalArgumentException();
                }
                itemStack = inventory.getArmor().getItemStack(equipSlot);
                break;
            }
            default: {
                throw new IllegalArgumentException();
            }
        }
        if (itemStack == null || itemStack.isEmpty()) {
            return;
        }
        final String rootId = itemStack.getItem().getInteractions().get(type);
        if (rootId == null) {
            return;
        }
        final RootInteraction root = RootInteraction.getAssetMap().getAsset(rootId);
        if (root == null || !this.canRun(type, equipSlot, root)) {
            return;
        }
        final InteractionContext context = InteractionContext.forInteraction(this, ref, type, equipSlot, commandBuffer);
        this.startChain(ref, commandBuffer, type, context, root);
    }
    
    public void sendSyncPacket(@Nonnull final InteractionChain chain, final int operationBaseIndex, @Nullable final List<InteractionSyncData> interactionData) {
        if (chain.hasSentInitial() && (interactionData == null || ListUtil.emptyOrAllNull(interactionData)) && chain.getNewForks().isEmpty()) {
            return;
        }
        if (this.playerRef != null) {
            final SyncInteractionChain packet = makeSyncPacket(chain, operationBaseIndex, interactionData);
            this.syncPackets.add(packet);
        }
    }
    
    @Nonnull
    private static SyncInteractionChain makeSyncPacket(@Nonnull final InteractionChain chain, final int operationBaseIndex, @Nullable final List<InteractionSyncData> interactionData) {
        SyncInteractionChain[] forks = null;
        final List<InteractionChain> newForks = chain.getNewForks();
        if (!newForks.isEmpty()) {
            forks = new SyncInteractionChain[newForks.size()];
            for (int i = 0; i < newForks.size(); ++i) {
                final InteractionChain fc = newForks.get(i);
                forks[i] = makeSyncPacket(fc, 0, null);
            }
            newForks.clear();
        }
        final SyncInteractionChain packet = new SyncInteractionChain(0, 0, 0, null, null, null, !chain.hasSentInitial(), false, chain.hasSentInitial() ? Integer.MIN_VALUE : RootInteraction.getRootInteractionIdOrUnknown(chain.getInitialRootInteraction().getId()), chain.getType(), chain.getContext().getHeldItemSlot(), chain.getChainId(), chain.getForkedChainId(), chain.getChainData(), chain.getServerState(), forks, operationBaseIndex, (InteractionSyncData[])((interactionData == null) ? null : ((InteractionSyncData[])interactionData.toArray(InteractionSyncData[]::new))));
        chain.setSentInitial(true);
        return packet;
    }
    
    private void sendCancelPacket(@Nonnull final InteractionChain chain) {
        this.sendCancelPacket(chain.getChainId(), chain.getForkedChainId());
    }
    
    public void sendCancelPacket(final int chainId, @Nonnull final ForkedChainId forkedChainId) {
        if (this.playerRef != null) {
            this.playerRef.getPacketHandler().writeNoCache(new CancelInteractionChain(chainId, forkedChainId));
        }
    }
    
    public void clear() {
        this.forEachInteraction((chain, _i, _a) -> {
            chain.setServerState(InteractionState.Failed);
            chain.setClientState(InteractionState.Failed);
            this.sendCancelPacket(chain);
            return null;
        }, (Object)null);
        this.chainStartQueue.clear();
    }
    
    public void clearAllGlobalTimeShift(final float dt) {
        if (this.timeShiftsDirty) {
            boolean clearFlag = true;
            for (int i = 0; i < this.globalTimeShift.length; ++i) {
                if (!this.globalTimeShiftDirty[i]) {
                    this.globalTimeShift[i] = 0.0f;
                }
                else {
                    clearFlag = false;
                    final float[] globalTimeShift = this.globalTimeShift;
                    final int n = i;
                    globalTimeShift[n] += dt;
                }
            }
            Arrays.fill(this.globalTimeShiftDirty, false);
            if (clearFlag) {
                this.timeShiftsDirty = false;
            }
        }
    }
    
    public void setGlobalTimeShift(@Nonnull final InteractionType type, final float shift) {
        if (shift < 0.0f) {
            throw new IllegalArgumentException("Can't shift backwards");
        }
        this.globalTimeShift[type.ordinal()] = shift;
        this.globalTimeShiftDirty[type.ordinal()] = true;
        this.timeShiftsDirty = true;
    }
    
    public float getGlobalTimeShift(@Nonnull final InteractionType type) {
        return this.globalTimeShift[type.ordinal()];
    }
    
    public <T> T forEachInteraction(@Nonnull final TriFunction<InteractionChain, Interaction, T, T> func, @Nonnull final T val) {
        return forEachInteraction(this.chains, func, val);
    }
    
    private static <T> T forEachInteraction(@Nonnull final Map<?, InteractionChain> chains, @Nonnull final TriFunction<InteractionChain, Interaction, T, T> func, @Nonnull T val) {
        if (chains.isEmpty()) {
            return val;
        }
        for (final InteractionChain chain : chains.values()) {
            Operation operation = chain.getRootInteraction().getOperation(chain.getOperationCounter());
            if (operation != null) {
                operation = operation.getInnerOperation();
                if (operation instanceof final Interaction interaction) {
                    val = func.apply(chain, interaction, val);
                }
            }
            val = (T)forEachInteraction(chain.getForkedChains(), (TriFunction<InteractionChain, Interaction, Object, Object>)func, val);
        }
        return val;
    }
    
    public void walkChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final Collector collector, @Nonnull final InteractionType type, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        this.walkChain(ref, collector, type, null, componentAccessor);
    }
    
    public void walkChain(@Nonnull final Ref<EntityStore> ref, @Nonnull final Collector collector, @Nonnull final InteractionType type, @Nullable final RootInteraction rootInteraction, @Nonnull final ComponentAccessor<EntityStore> componentAccessor) {
        walkChain(collector, type, InteractionContext.forInteraction(this, ref, type, componentAccessor), rootInteraction);
    }
    
    public static void walkChain(@Nonnull final Collector collector, @Nonnull final InteractionType type, @Nonnull final InteractionContext context, @Nullable RootInteraction rootInteraction) {
        if (rootInteraction == null) {
            final String rootInteractionId = context.getRootInteractionId(type);
            if (rootInteractionId == null) {
                throw new IllegalArgumentException("No interaction ID found for " + String.valueOf(type) + ", " + String.valueOf(context));
            }
            rootInteraction = RootInteraction.getAssetMap().getAsset(rootInteractionId);
        }
        if (rootInteraction == null) {
            throw new IllegalArgumentException("No interactions are defined for " + String.valueOf(type) + ", " + String.valueOf(context));
        }
        collector.start();
        collector.into(context, null);
        walkInteractions(collector, context, CollectorTag.ROOT, rootInteraction.getInteractionIds());
        collector.outof();
        collector.finished();
    }
    
    public static boolean walkInteractions(@Nonnull final Collector collector, @Nonnull final InteractionContext context, @Nonnull final CollectorTag tag, @Nonnull final String[] interactionIds) {
        for (final String id : interactionIds) {
            if (walkInteraction(collector, context, tag, id)) {
                return true;
            }
        }
        return false;
    }
    
    public static boolean walkInteraction(@Nonnull final Collector collector, @Nonnull final InteractionContext context, @Nonnull final CollectorTag tag, @Nullable final String id) {
        if (id == null) {
            return false;
        }
        final Interaction interaction = Interaction.getAssetMap().getAsset(id);
        if (interaction == null) {
            throw new IllegalArgumentException("Failed to find interaction: " + id);
        }
        if (collector.collect(tag, context, interaction)) {
            return true;
        }
        collector.into(context, interaction);
        interaction.walk(collector, context);
        collector.outof();
        return false;
    }
    
    public ObjectList<SyncInteractionChain> getSyncPackets() {
        return this.syncPackets;
    }
    
    @Nonnull
    @Override
    public Component<EntityStore> clone() {
        final InteractionManager manager = new InteractionManager(this.entity, this.playerRef, this.interactionSimulationHandler);
        manager.copyFrom(this);
        return manager;
    }
    
    static {
        DEFAULT_CHARGE_TIMES = new float[] { 0.0f };
        LOGGER = HytaleLogger.forEnclosingClass();
    }
    
    public static class ChainCancelledException extends RuntimeException
    {
        @Nonnull
        private final InteractionState state;
        
        public ChainCancelledException(@Nonnull final InteractionState state) {
            this.state = state;
        }
    }
}
