public abstract class BungeeTabListPlusSpongeAPI {
    private static BungeeTabListPlusSpongeAPI instance;

    /**
     * Registers a custom variable
     * <p>
     * You cannot use this to replace existing variables. If registering a variable which already
     * exists there may be an exception thrown but there is no guarantee that an exception
     * is thrown in that case.
     *
     * @param plugin   your plugin
     * @param variable your variable
     */
    public static void registerVariable(Object plugin, Variable variable) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.registerVariable0(plugin, variable);
    }

    protected abstract void registerVariable0(Object plugin, Variable variable);

    /**
     * Registers a custom per server variable.
     * <p>
     * You cannot use this to replace existing variables. If registering a variable which already
     * exists there may be an exception thrown but there is no guarantee that an exception
     * is thrown in that case.
     *
     * @param plugin   your plugin
     * @param variable your variable
     */
    public static void registerVariable(Object plugin, ServerVariable variable) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.registerVariable0(plugin, variable);
    }

    protected abstract void registerVariable0(Object plugin, ServerVariable variable);

    /**
     * Unregisters a variable
     *
     * @param variable the variable
     */
    public static void unregisterVariable(Variable variable) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.unregisterVariable0(variable);
    }

    protected abstract void unregisterVariable0(Variable variable);

    /**
     * Unregisters a per server variable
     *
     * @param variable the variable
     */
    public static void unregisterVariable(ServerVariable variable) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.unregisterVariable0(variable);
    }

    protected abstract void unregisterVariable0(ServerVariable variable);

    /**
     * Unregisters all variables registered by the give plugin
     *
     * @param plugin the plugin
     */
    public static void unregisterVariables(Object plugin) {
        Preconditions.checkState(instance != null, "instance is null, is the plugin enabled?");
        instance.unregisterVariables0(plugin);
    }

    protected abstract void unregisterVariables0(Object plugin);
} public abstract class ServerVariable {
    private final String name;

    /**
     * invoked by the subclass to set the name of the variable
     *
     * @param name name of the variable without { }
     */
    public ServerVariable(String name) {
        this.name = name;
    }

    /**
     * This method is periodically invoked by BungeeTabListPlus to check whether the replacement for the variable changed
     * <p>
     * This method will be invoked by threads other than the games main thread and may also be invoked concurrently.
     *
     * @return the replacement for the variable
     */
    public abstract String getReplacement();

    /**
     * getter for the variable name
     *
     * @return the name of the variable without { }
     */
    public final String getName() {
        return name;
    }
} public abstract class Variable {
    private final String name;

    /**
     * invoked by the subclass to set the name of the variable
     *
     * @param name name of the variable without { }
     */
    public Variable(String name) {
        this.name = name;
    }

    /**
     * This method is periodically invoked by BungeeTabListPlus to check whether the replacement for the variable changed
     * <p>
     * This method will be invoked by threads other than the games main thread and may also be invoked concurrently.
     *
     * @param player the player for which the variable should be replaced
     * @return the replacement for the variable
     */
    public abstract String getReplacement(Player player);

    /**
     * getter for the variable name
     *
     * @return the name of the variable without { }
     */
    public final String getName() {
        return name;
    }
} public final class BTLPDataKeys implements DataKeyCatalogue {

    public static final DataKey<String> PAPIPlaceholder = new DataKey<>("btlp:placeholderAPI", MinecraftData.SCOPE_PLAYER, TypeToken.STRING);
    public static final DataKey<List<String>> PAPI_REGISTERED_PLACEHOLDER_PLUGINS = new DataKey<>("btlp:placeholderAPI:plugins", MinecraftData.SCOPE_SERVER, TypeToken.STRING_LIST);

    public static DataKey<String> createPlaceholderAPIDataKey(String placeholder) {
        return PAPIPlaceholder.withParameter(placeholder);
    }

    public static final DataKey<String> ThirdPartyPlaceholder = new DataKey<>("btlp:thirdPartyPlaceholder", MinecraftData.SCOPE_PLAYER, TypeToken.STRING);

    public static DataKey<String> createThirdPartyVariableDataKey(String name) {
        return ThirdPartyPlaceholder.withParameter(name);
    }

    public static final DataKey<String> ThirdPartyServerPlaceholder = new DataKey<>("btlp:thirdPartyServerPlaceholder", MinecraftData.SCOPE_SERVER, TypeToken.STRING);

    public static DataKey<String> createThirdPartyServerVariableDataKey(String name) {
        return ThirdPartyServerPlaceholder.withParameter(name);
    }

    public final static DataKey<List<String>> REGISTERED_THIRD_PARTY_VARIABLES = new DataKey<>("thirdparty-variables", MinecraftData.SCOPE_SERVER, TypeToken.STRING_LIST);

    public final static DataKey<List<String>> REGISTERED_THIRD_PARTY_SERVER_VARIABLES = new DataKey<>("thirdparty-server-variables", MinecraftData.SCOPE_SERVER, TypeToken.STRING_LIST);

    public final static DataKey<Boolean> PLACEHOLDERAPI_PRESENT = new DataKey<>("placeholderapi-present", MinecraftData.SCOPE_SERVER, TypeToken.BOOLEAN);
} public class BridgeProtocolConstants {

    public static final String CHANNEL = "btlp:bridge";

    public static final int VERSION = 5;

    public static final int MESSAGE_ID_INTRODUCE = 0x00;

    public static final int MESSAGE_ID_ACK = 0x01;
    public static final int MESSAGE_ID_REQUEST_DATA = 0x02;
    public static final int MESSAGE_ID_UPDATE_DATA = 0x03;

    public static final int MESSAGE_ID_ACK_SERVER = 0x81;
    public static final int MESSAGE_ID_REQUEST_DATA_SERVER = 0x82;
    public static final int MESSAGE_ID_UPDATE_DATA_SERVER = 0x83;
} public class TypeAdapterRegistry {

    public static final TypeAdapterRegistry DEFAULT_TYPE_ADAPTERS = new TypeAdapterRegistry(ImmutableMap.<TypeToken<?>, TypeAdapter<?>>builder()
            .put(TypeToken.BOOLEAN, new TypeAdapter<Boolean>() {
                @Override
                public Boolean read(DataInput input) throws IOException {
                    return input.readBoolean();
                }

                @Override
                public void write(DataOutput output, Boolean object) throws IOException {
                    output.writeBoolean(object);
                }
            })
            .put(TypeToken.INTEGER, new TypeAdapter<Integer>() {
                @Override
                public Integer read(DataInput input) throws IOException {
                    return input.readInt();
                }

                @Override
                public void write(DataOutput output, Integer object) throws IOException {
                    output.writeInt(object);
                }
            })
            .put(TypeToken.FLOAT, new TypeAdapter<Float>() {
                @Override
                public Float read(DataInput input) throws IOException {
                    return input.readFloat();
                }

                @Override
                public void write(DataOutput output, Float object) throws IOException {
                    output.writeFloat(object);
                }
            })
            .put(TypeToken.DOUBLE, new TypeAdapter<Double>() {
                @Override
                public Double read(DataInput input) throws IOException {
                    return input.readDouble();
                }

                @Override
                public void write(DataOutput output, Double object) throws IOException {
                    output.writeDouble(object);
                }
            })
            .put(TypeToken.STRING, new TypeAdapter<String>() {
                @Override
                public String read(DataInput input) throws IOException {
                    return input.readUTF();
                }

                @Override
                public void write(DataOutput output, String object) throws IOException {
                    output.writeUTF(object);
                }
            })
            .put(TypeToken.DURATION, new TypeAdapter<Duration>() {
                @Override
                public Duration read(DataInput input) throws IOException {
                    long seconds = input.readLong();
                    int nanos = input.readInt();
                    return Duration.ofSeconds(seconds, nanos);
                }

                @Override
                public void write(DataOutput output, Duration object) throws IOException {
                    output.writeLong(object.getSeconds());
                    output.writeInt(object.getNano());
                }
            })
            .put(TypeToken.STRING_LIST, new TypeAdapter<List<String>>() {
                @Override
                public List<String> read(DataInput input) throws IOException {
                    int size = input.readInt();
                    ArrayList<String> list = new ArrayList<>(size);
                    for (int i = 0; i < size; i++) {
                        list.add(input.readUTF());
                    }
                    return list;
                }

                @Override
                public void write(DataOutput output, List<String> object) throws IOException {
                    int size = object.size();
                    output.writeInt(size);
                    for (int i = 0; i < size; i++) {
                        String element = object.get(i);
                        output.writeUTF(element);
                    }

                }
            }).build());

    public static TypeAdapterRegistry of(TypeAdapterRegistry... registries) {
        ImmutableMap.Builder<TypeToken<?>, TypeAdapter<?>> builder = ImmutableMap.builder();
        for (TypeAdapterRegistry registry : registries) {
            builder.putAll(registry.map);
        }
        return new TypeAdapterRegistry(builder.build());
    }

    private final ImmutableMap<TypeToken<?>, TypeAdapter<?>> map;

    public TypeAdapterRegistry(ImmutableMap<TypeToken<?>, TypeAdapter<?>> map) {
        this.map = map;
    }

    @SuppressWarnings("unchecked")
    public <T> TypeAdapter<T> getTypeAdapter(TypeToken<T> type) {
        return (TypeAdapter<T>) map.get(type);
    }
} public class RateLimitedExecutor implements Executor {

    private final long delay;

    private long lastExecuted = Long.MIN_VALUE;

    public RateLimitedExecutor(long delay) {
        this.delay = delay;
    }

    @Override
    public void execute(Runnable command) {
        long now = System.currentTimeMillis();
        if (lastExecuted + delay <= now) {
            lastExecuted = now;
            command.run();
        }
    }
} public class PlaceholderAPIHook {

    private final SpongePlugin plugin;

    public PlaceholderAPIHook(SpongePlugin plugin) {
        this.plugin = plugin;
    }

    public DataAccess<Player> getDataAccess() {
        return new PlaceholderAPIDataAccess(plugin.getLogger());
    }

    public List<String> getRegisteredPlaceholderPlugins() {
        return Store.get().allIds();
    }
} public class ChannelBufInputStream extends InputStream {

    private final ChannelBuf channelBuf;

    public ChannelBufInputStream(ChannelBuf channelBuf) {
        this.channelBuf = channelBuf;
    }

    @Override
    public int read() throws IOException {
        return channelBuf.available() > 0 ? Byte.toUnsignedInt(channelBuf.readByte()) : -1;
    }
} public abstract class AbstractBridge<Player, Server> {

    @Nonnull
    private final DataKeyRegistry dataKeyRegistry;
    @Nonnull
    private final TypeAdapterRegistry typeAdapterRegistry;

    private final int serverIdentifier;
    @Nonnull
    private final String pluginVersion;

    @Nonnull
    private final Server server;

    private final Map<Player, PlayerConnectionInfo> playerPlayerConnectionInfoMap = new ConcurrentHashMap<>();

    private final Map<Integer, BridgeData> serverBridgeDataMap = new ConcurrentHashMap<>();

    private final Object updateDataLock = new Object();

    @Nonnull
    private DataAccess<Player> playerDataAccess = JoinedDataAccess.of(); // always returns null
    @Nonnull
    private DataAccess<Server> serverDataAccess = JoinedDataAccess.of();

    public AbstractBridge(@Nonnull DataKeyRegistry dataKeyRegistry, @Nonnull TypeAdapterRegistry typeAdapterRegistry, @Nonnull String pluginVersion, @Nonnull Server server) {
        this.dataKeyRegistry = dataKeyRegistry;
        this.typeAdapterRegistry = typeAdapterRegistry;
        this.pluginVersion = pluginVersion;
        this.server = server;
        this.serverIdentifier = ThreadLocalRandom.current().nextInt();
    }

    public void onPlayerConnect(@Nonnull Player player) {
        playerPlayerConnectionInfoMap.put(player, new PlayerConnectionInfo());
    }

    public void onPlayerDisconnect(@Nonnull Player player) {
        playerPlayerConnectionInfoMap.remove(player);
    }

    public void onMessage(@Nonnull Player player, @Nonnull DataInput input) throws IOException {

        PlayerConnectionInfo connectionInfo = playerPlayerConnectionInfoMap.get(player);

        if (connectionInfo == null) {
            return;
        }

        int messageId = input.readUnsignedByte();

        if (messageId == BridgeProtocolConstants.MESSAGE_ID_INTRODUCE) {

            int proxyIdentifier = input.readInt();
            int protocolVersion = input.readInt();
            int minimumCompatibleProtocolVersion = input.readInt();
            String proxyPluginVersion = input.readUTF();

            int connectionId = proxyIdentifier + serverIdentifier;

            if (connectionId == connectionInfo.connectionIdentifier) {
                return;
            }

            if (BridgeProtocolConstants.VERSION < minimumCompatibleProtocolVersion) {
                throw new IOException("Incompatible version of BTLP on Proxy detected: " + proxyPluginVersion);
            }

            // reset connection state
            connectionInfo.connectionIdentifier = connectionId;
            connectionInfo.isConnectionValid = true;
            connectionInfo.nextIntroducePacketDelay = 1;
            connectionInfo.introducePacketDelay = 1;
            connectionInfo.hasReceived = false;
            connectionInfo.protocolVersion = Integer.min(BridgeProtocolConstants.VERSION, protocolVersion);
            connectionInfo.proxyIdentifier = proxyIdentifier;
            connectionInfo.playerBridgeData = new BridgeData();
            connectionInfo.serverBridgeData = serverBridgeDataMap.computeIfAbsent(proxyIdentifier, key -> new BridgeData());

            // send ACK 0
            ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
            DataOutput output = new DataOutputStream(byteArrayOutput);

            output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK);
            output.writeInt(connectionId);
            output.writeInt(0);

            byte[] message = byteArrayOutput.toByteArray();
            sendMessage(player, message);
        } else {

            if (!connectionInfo.isConnectionValid) {
                return;
            }

            int connectionId = input.readInt();

            if (connectionId != connectionInfo.connectionIdentifier) {
                return;
            }

            connectionInfo.hasReceived = true;

            int sequenceNumber = input.readInt();

            BridgeData bridgeData;
            boolean isServerMessage;

            if ((messageId & 0x80) == 0) {
                bridgeData = connectionInfo.playerBridgeData;
                isServerMessage = false;
            } else {
                bridgeData = connectionInfo.serverBridgeData;
                isServerMessage = true;
            }

            if (bridgeData == null) {
                return;
            }

            messageId = messageId & 0x7f;

            if (messageId == BridgeProtocolConstants.MESSAGE_ID_ACK) {

                int confirmed = sequenceNumber - bridgeData.lastConfirmed;

                if (confirmed > bridgeData.messagesPendingConfirmation.size()) {
                    return;
                }

                while (confirmed-- > 0) {
                    bridgeData.lastConfirmed++;
                    bridgeData.messagesPendingConfirmation.remove();
                }
            } else if (messageId == BridgeProtocolConstants.MESSAGE_ID_REQUEST_DATA) {

                if (sequenceNumber > bridgeData.nextIncomingMessageId) {
                    // ignore messages from the future
                    return;
                }

                ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
                DataOutput output = new DataOutputStream(byteArrayOutput);

                output.writeByte(BridgeProtocolConstants.MESSAGE_ID_ACK | (isServerMessage ? 0x80 : 0x00));
                output.writeInt(connectionId);
                output.writeInt(sequenceNumber);

                byte[] message = byteArrayOutput.toByteArray();
                sendMessage(player, message);

                if (sequenceNumber < bridgeData.nextIncomingMessageId) {
                    // ignore messages from the past after sending ACK
                    return;
                }

                bridgeData.nextIncomingMessageId++;

                int size = input.readInt();
                for (int i = 0; i < size; i++) {
                    DataKey<?> key = DataStreamUtils.readDataKey(input, dataKeyRegistry);
                    int keyNetId = input.readInt();

                    if (key != null) {
                        bridgeData.addRequest(key, keyNetId);
                    }
                }

                runAsync(() -> {
                    try {
                        updatePlayerData(player, connectionInfo);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });
            } else {
                throw new IllegalArgumentException("Unexpected message id: " + messageId);
            }
        }
    }

    /**
     * Sends introduce packets to the proxy to try to establish a connection.
     * <p>
     * Should be called periodically, recommended interval is 100ms to 1s.
     */
    public void sendIntroducePackets() throws IOException {
        for (Map.Entry<Player, PlayerConnectionInfo> entry : playerPlayerConnectionInfoMap.entrySet()) {
            PlayerConnectionInfo connectionInfo = entry.getValue();
            if (!connectionInfo.hasReceived) {
                if (--connectionInfo.nextIntroducePacketDelay <= 0) {
                    connectionInfo.nextIntroducePacketDelay = connectionInfo.introducePacketDelay++;

                    ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
                    DataOutput output = new DataOutputStream(byteArrayOutput);

                    output.writeByte(BridgeProtocolConstants.MESSAGE_ID_INTRODUCE);
                    output.writeInt(serverIdentifier);
                    output.writeInt(BridgeProtocolConstants.VERSION);
                    output.writeInt(BridgeProtocolConstants.VERSION);
                    output.writeUTF(pluginVersion);

                    byte[] message = byteArrayOutput.toByteArray();
                    sendMessage(entry.getKey(), message);
                }
            }
        }

    }

    /**
     * Sends unconfirmed messages to the proxy yet another time, to ensure their arrival.
     * <p>
     * Should be called periodically, recommended interval is 1s to 10s.
     */
    public void resendUnconfirmedMessages() {
        long now = System.currentTimeMillis();
        Map<Integer, Player> proxyIds = new HashMap<>();

        for (Map.Entry<Player, PlayerConnectionInfo> e : playerPlayerConnectionInfoMap.entrySet()) {
            Player player = e.getKey();
            PlayerConnectionInfo connectionInfo = e.getValue();

            if (!connectionInfo.isConnectionValid) {
                continue;
            }

            proxyIds.putIfAbsent(connectionInfo.proxyIdentifier, player);
            BridgeData bridgeData = connectionInfo.playerBridgeData;

            if (bridgeData == null) {
                continue;
            }

            if (bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) {
                for (byte[] message : bridgeData.messagesPendingConfirmation) {
                    sendMessage(player, message);
                }
            }
        }

        for (Map.Entry<Integer, Player> e : proxyIds.entrySet()) {
            Integer proxyIdentifier = e.getKey();
            Player player = e.getValue();
            BridgeData bridgeData = serverBridgeDataMap.get(proxyIdentifier);

            if (bridgeData != null && bridgeData.messagesPendingConfirmation.size() > 0 && (now > bridgeData.lastMessageSent + 1000 || bridgeData.messagesPendingConfirmation.size() > 5)) {
                for (byte[] message : bridgeData.messagesPendingConfirmation) {
                    sendMessage(player, message);
                }
            }
        }
    }

    public void updateData() throws IOException {
        synchronized (updateDataLock) {
            Map<Integer, Player> proxyIds = new HashMap<>();

            for (Map.Entry<Player, PlayerConnectionInfo> e : playerPlayerConnectionInfoMap.entrySet()) {
                Player player = e.getKey();
                PlayerConnectionInfo connectionInfo = e.getValue();

                if (!connectionInfo.isConnectionValid) {
                    continue;
                }

                proxyIds.putIfAbsent(connectionInfo.proxyIdentifier, player);

                updatePlayerData(player, connectionInfo);
            }

            for (Map.Entry<Integer, Player> e : proxyIds.entrySet()) {
                Integer proxyIdentifier = e.getKey();
                Player player = e.getValue();
                BridgeData bridgeData = serverBridgeDataMap.get(proxyIdentifier);

                if (bridgeData == null) {
                    continue;
                }

                int size = 0;

                for (CacheEntry entry : bridgeData.requestedData) {
                    Object value = serverDataAccess.get(entry.key, server);
                    entry.dirty = !Objects.equals(value, entry.value);
                    entry.value = value;

                    if (entry.dirty) {
                        size++;
                    }
                }

                ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
                DataOutput output = new DataOutputStream(byteArrayOutput);

                output.writeByte(BridgeProtocolConstants.MESSAGE_ID_UPDATE_DATA_SERVER);
                output.writeInt(proxyIdentifier + serverIdentifier);
                output.writeInt(bridgeData.nextOutgoingMessageId++);
                output.writeInt(size);

                for (CacheEntry entry : bridgeData.requestedData) {
                    if (entry.dirty) {
                        output.writeInt(entry.netId);
                        output.writeBoolean(entry.value == null);
                        if (entry.value != null) {
                            try {
                                typeAdapterRegistry.getTypeAdapter((TypeToken<Object>) entry.key.getType()).write(output, entry.value);
                            } catch (IOException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                }

                byte[] message = byteArrayOutput.toByteArray();
                bridgeData.messagesPendingConfirmation.add(message);
                bridgeData.lastMessageSent = System.currentTimeMillis();
                sendMessage(player, message);
            }
        }
    }

    private void updatePlayerData(@Nonnull Player player, @Nonnull PlayerConnectionInfo connectionInfo) throws IOException {
        synchronized (updateDataLock) {
            BridgeData bridgeData = connectionInfo.playerBridgeData;

            if (bridgeData == null) {
                return;
            }

            int size = 0;

            for (CacheEntry entry : bridgeData.requestedData) {
                Object value = playerDataAccess.get(entry.key, player);
                entry.dirty = !Objects.equals(value, entry.value);
                entry.value = value;

                if (entry.dirty) {
                    size++;
                }
            }

            if (size != 0) {
                ByteArrayOutputStream byteArrayOutput = new ByteArrayOutputStream();
                DataOutput output = new DataOutputStream(byteArrayOutput);
                output.writeByte(BridgeProtocolConstants.MESSAGE_ID_UPDATE_DATA);
                output.writeInt(connectionInfo.connectionIdentifier);
                output.writeInt(bridgeData.nextOutgoingMessageId++);
                output.writeInt(size);

                for (CacheEntry entry : bridgeData.requestedData) {
                    if (entry.dirty) {
                        output.writeInt(entry.netId);
                        output.writeBoolean(entry.value == null);
                        if (entry.value != null) {
                            try {
                                typeAdapterRegistry.getTypeAdapter((TypeToken<Object>) entry.key.getType()).write(output, entry.value);
                            } catch (IOException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                }

                byte[] message = byteArrayOutput.toByteArray();
                bridgeData.messagesPendingConfirmation.add(message);
                bridgeData.lastMessageSent = System.currentTimeMillis();
                sendMessage(player, message);
            }
        }
    }

    public void setPlayerDataAccess(@Nonnull DataAccess<Player> playerDataAccess) {
        this.playerDataAccess = playerDataAccess;
    }

    public void setServerDataAccess(@Nonnull DataAccess<Server> serverDataAccess) {
        this.serverDataAccess = serverDataAccess;
    }

    protected abstract void sendMessage(@Nonnull Player player, @Nonnull byte[] message);

    protected abstract void runAsync(@Nonnull Runnable task);

    private static class PlayerConnectionInfo {
        boolean isConnectionValid = false;
        boolean hasReceived = false;
        int connectionIdentifier = 0;
        int protocolVersion = 0;
        int proxyIdentifier = 0;
        int nextIntroducePacketDelay = 1;
        int introducePacketDelay = 1;
        @Nullable
        BridgeData playerBridgeData = null;
        @Nullable
        BridgeData serverBridgeData = null;
    }

    private static class CacheEntry {
        @Nonnull
        final DataKey<?> key;
        final int netId;
        @Nullable
        Object value = null;
        boolean dirty = false;

        CacheEntry(@Nonnull DataKey<?> key, int netId) {
            this.key = key;
            this.netId = netId;
        }
    }

    private static class BridgeData {
        final Queue<byte[]> messagesPendingConfirmation = new ConcurrentLinkedQueue<>();
        final List<CacheEntry> requestedData = new CopyOnWriteArrayList<>();
        int lastConfirmed = 0;
        int nextOutgoingMessageId = 1;
        int nextIncomingMessageId = 1;
        long lastMessageSent = 0;

        private void addRequest(@Nonnull DataKey<?> key, int netId) {
            for (CacheEntry registration : requestedData) {
                if (Objects.equals(registration.key, key)) {
                    return;
                }
            }

            requestedData.add(new CacheEntry(key, netId));
        }
    }
}
import codecrafter47.bungeetablistplus.api.sponge.BungeeTabListPlusSpongeAPI;
import codecrafter47.bungeetablistplus.api.sponge.ServerVariable;
import codecrafter47.bungeetablistplus.api.sponge.Variable;
import codecrafter47.bungeetablistplus.common.BTLPDataKeys;
import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants;
import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry;
import codecrafter47.bungeetablistplus.common.util.RateLimitedExecutor;
import codecrafter47.bungeetablistplus.spongebridge.placeholderapi.PlaceholderAPIHook;
import codecrafter47.bungeetablistplus.spongebridge.util.ChannelBufInputStream;
import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import de.codecrafter47.bungeetablistplus.bridge.AbstractBridge;
import de.codecrafter47.data.api.DataKey;
import de.codecrafter47.data.api.DataKeyRegistry;
import de.codecrafter47.data.api.JoinedDataAccess;
import de.codecrafter47.data.minecraft.api.MinecraftData;
import de.codecrafter47.data.sponge.AbstractSpongeDataAccess;
import de.codecrafter47.data.sponge.PlayerDataAccess;
import de.codecrafter47.data.sponge.ServerDataAccess;
import de.codecrafter47.data.sponge.api.SpongeData;
import org.slf4j.Logger;
import org.spongepowered.api.Game;
import org.spongepowered.api.Platform;
import org.spongepowered.api.Server;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GameAboutToStartServerEvent;
import org.spongepowered.api.event.game.state.GameInitializationEvent;
import org.spongepowered.api.event.game.state.GameStartedServerEvent;
import org.spongepowered.api.event.network.ClientConnectionEvent;
import org.spongepowered.api.network.ChannelBinding;
import org.spongepowered.api.network.PlayerConnection;
import org.spongepowered.api.plugin.Plugin;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.scheduler.SpongeExecutorService;
import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.io.DataInput;
import java.io.DataInputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
/*
 *     Copyright (C) 2020 Florian Stober
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package codecrafter47.bungeetablistplus.spongebridge;



@Plugin(id = "bungeetablistplus", name = "BungeeTabListPlus-SpongeBridge")
public class SpongePlugin extends BungeeTabListPlusSpongeAPI {

    private static final TypeAdapterRegistry typeRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS;

    private static final DataKeyRegistry keyRegistry = DataKeyRegistry.of(
            MinecraftData.class,
            BTLPDataKeys.class,
            SpongeData.class);

    private final RateLimitedExecutor rlExecutor = new RateLimitedExecutor(5000);

    @Inject
    private Game game;

    @Inject
    private Logger logger;

    private Bridge bridge;

    private ChannelBinding.RawDataChannel channel;

    private PlaceholderAPIHook placeholderAPIHook = null;