codecrafter47.bungeetablistplus.bridge.BukkitBridge.java Source code

Java tutorial

Introduction

Here is the source code for codecrafter47.bungeetablistplus.bridge.BukkitBridge.java

Source

/*
 * BungeeTabListPlus - a BungeeCord plugin to customize the tablist
 *
 * Copyright (C) 2014 - 2015 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.bridge;

import codecrafter47.bungeetablistplus.BungeeTabListPlus;
import codecrafter47.bungeetablistplus.api.bungee.placeholder.PlaceholderProvider;
import codecrafter47.bungeetablistplus.common.BTLPDataKeys;
import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants;
import codecrafter47.bungeetablistplus.common.network.DataStreamUtils;
import codecrafter47.bungeetablistplus.common.network.TypeAdapterRegistry;
import codecrafter47.bungeetablistplus.common.util.RateLimitedExecutor;
import codecrafter47.bungeetablistplus.data.NullDataHolder;
import codecrafter47.bungeetablistplus.data.TrackingDataCache;
import codecrafter47.bungeetablistplus.placeholder.Placeholder;
import codecrafter47.bungeetablistplus.player.ConnectedPlayer;
import codecrafter47.bungeetablistplus.player.Player;
import com.google.common.collect.Lists;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import de.codecrafter47.data.api.DataCache;
import de.codecrafter47.data.api.DataHolder;
import de.codecrafter47.data.api.DataKey;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import it.unimi.dsi.fastutil.objects.ReferenceSet;
import lombok.SneakyThrows;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.connection.Server;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.event.ServerConnectedEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.event.EventHandler;

import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;

public class BukkitBridge implements Listener {

    private static final TypeAdapterRegistry typeAdapterRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS;

    private static final RateLimitedExecutor rlExecutor = new RateLimitedExecutor(5000);

    private final BungeeTabListPlus plugin;

    private final Map<String, ServerBridgeDataCache> serverInformation = new HashMap<>();

    private final Set<String> registeredThirdPartyVariables = new HashSet<>();
    private final ReentrantLock thirdPartyVariablesLock = new ReentrantLock();

    private final UUID proxyId = UUID.randomUUID();

    private final NetDataKeyIdMap idMap = new NetDataKeyIdMap();

    public BukkitBridge(BungeeTabListPlus plugin) {
        this.plugin = plugin;
        plugin.getProxy().getPluginManager().registerListener(plugin.getPlugin(), this);
        plugin.getProxy().getScheduler().schedule(plugin.getPlugin(),
                () -> plugin.runInMainThread(this::checkForThirdPartyVariables), 2, 2, TimeUnit.SECONDS);
        plugin.getProxy().getScheduler().schedule(plugin.getPlugin(), this::requestMissingServerData, 1, 1,
                TimeUnit.SECONDS);
        plugin.getProxy().getScheduler().schedule(plugin.getPlugin(), this::removeObsoleteServerConnections, 5, 5,
                TimeUnit.SECONDS);
        plugin.getProxy().getScheduler().schedule(plugin.getPlugin(), this::tryInitConnections, 5, 5,
                TimeUnit.SECONDS);
    }

    private void checkForThirdPartyVariables() {
        try {
            for (ServerInfo serverInfo : plugin.getProxy().getServers().values()) {
                List<String> variables = getServerDataCache(serverInfo.getName())
                        .get(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES);
                if (variables != null) {
                    thirdPartyVariablesLock.lock();
                    try {
                        for (String variable : variables) {
                            if (!registeredThirdPartyVariables.contains(variable)) {
                                Placeholder.remoteThirdPartyDataKeys.put(variable,
                                        BTLPDataKeys.createThirdPartyVariableDataKey(variable));
                                // this implicitly causes a reload
                                plugin.registerPlaceholderProvider0(new PlaceholderProvider() {
                                    @Override
                                    public void setup() {
                                        bind(variable).to(context -> ((Player) context.getPlayer())
                                                .getOpt(BTLPDataKeys.createThirdPartyVariableDataKey(variable))
                                                .orElse(""));
                                    }
                                });
                                registeredThirdPartyVariables.add(variable);
                            }
                        }
                    } catch (Throwable th) {
                        plugin.getLogger().log(Level.SEVERE, "Unexpected exception", th);
                    } finally {
                        thirdPartyVariablesLock.unlock();
                    }
                }
            }
        } catch (ConcurrentModificationException ignored) {
            // The map returned from ProxyServer#getServers is mutable
            // If it is mutated while we are iterating over its values
            // an exception is thrown
        }
    }

    private void requestMissingServerData() {
        for (ServerBridgeDataCache cache : serverInformation.values()) {
            cache.requestMissingData();
        }
    }

    private void removeObsoleteServerConnections() {
        for (ServerBridgeDataCache cache : serverInformation.values()) {
            cache.removeObsoleteConnections();
        }
    }

    private void tryInitConnections() {
        // Usually the handshake is initiated by the server, but in case this fails, try again.
        for (ConnectedPlayer player : BungeeTabListPlus.getInstance().getConnectedPlayerManager().getPlayers()) {
            if (player.getBridgeDataCache().connection == null) {
                Server server = player.getPlayer().getServer();
                if (server != null && server.isConnected()) {
                    initializeHandshake(server);
                }
            }
        }
    }

    public PlayerBridgeDataCache createDataCacheForPlayer(ConnectedPlayer player) {
        return new PlayerBridgeDataCache();
    }

    public DataHolder getServerDataHolder(String server) {
        DataHolder data = serverInformation.get(server);
        return data != null ? data : NullDataHolder.INSTANCE;
    }

    private ServerBridgeDataCache getServerDataCache(String serverName) {
        if (!serverInformation.containsKey(serverName)) {
            serverInformation.putIfAbsent(serverName, new ServerBridgeDataCache());
        }
        return serverInformation.get(serverName);
    }

    @EventHandler
    public void onPluginMessage(PluginMessageEvent event) {
        if (event.getReceiver() instanceof ProxiedPlayer && event.getSender() instanceof Server) {

            ProxiedPlayer player = (ProxiedPlayer) event.getReceiver();
            Server server = (Server) event.getSender();

            if (event.getTag().equals(BridgeProtocolConstants.CHANNEL)) {
                event.setCancelled(true);

                ConnectedPlayer connectedPlayer = plugin.getConnectedPlayerManager().getPlayerIfPresent(player);
                if (connectedPlayer != null) {

                    PlayerBridgeDataCache cache = connectedPlayer.getBridgeDataCache();

                    DataInput input = new DataInputStream(new ByteArrayInputStream(event.getData()));

                    try {
                        int messageId = input.readUnsignedByte();

                        switch (messageId) {
                        case BridgeProtocolConstants.MESSAGE_ID_SERVER_HANDSHAKE:

                            if (cache.connection != null) {
                                disableConnection(cache);
                            }
                            cache.connection = server;
                            getServerDataCache(server.getInfo().getName()).addConnection(server);
                            ArrayList<DataKey<?>> keys = Lists.newArrayList(cache.getQueriedKeys());

                            ByteArrayDataOutput data = ByteStreams.newDataOutput();
                            data.writeByte(BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_DATA);
                            data.writeInt(keys.size());
                            for (DataKey<?> key : keys) {
                                DataStreamUtils.writeDataKey(data, key);
                                data.writeInt(idMap.getNetId(key));
                            }
                            server.sendData(BridgeProtocolConstants.CHANNEL, data.toByteArray());
                            break;
                        case BridgeProtocolConstants.MESSAGE_ID_SERVER_UPDATE_DATA:

                            if (cache.connection == server) {
                                onDataReceived(cache, input, input.readInt());
                            }

                            break;
                        case BridgeProtocolConstants.MESSAGE_ID_SERVER_UPDATE_SERVER_DATA:

                            if (cache.connection == server) {
                                ServerBridgeDataCache serverDataCache = getServerDataCache(
                                        server.getInfo().getName());
                                int revision = input.readInt();
                                int size = input.readInt();
                                if (size > 0 && revision == serverDataCache.lastRevision + 1) {
                                    onDataReceived(serverDataCache, input, size);
                                    serverDataCache.lastRevision = revision;
                                } else if (size > 0 || revision > serverDataCache.lastRevision) {
                                    Server connection = serverDataCache.getConnection();
                                    if (connection != null) {
                                        ByteArrayDataOutput output = ByteStreams.newDataOutput();
                                        output.writeByte(
                                                BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_RESET_SERVER_DATA);
                                        connection.sendData(BridgeProtocolConstants.CHANNEL, output.toByteArray());
                                        serverDataCache.lastRevision = 0;
                                    }
                                }
                            }

                            break;
                        case BridgeProtocolConstants.MESSAGE_ID_SERVER_DISABLE_CONNECTION:

                            disableConnection(cache);
                            serverInformation.get(server.getInfo().getName()).reset();
                            break;
                        case BridgeProtocolConstants.MESSAGE_ID_SERVER_ENABLE_CONNECTION:

                            UUID serverId = DataStreamUtils.readUUID(input);
                            ServerBridgeDataCache serverData = getServerDataCache(server.getInfo().getName());
                            if (!serverData.serverId.equals(serverId)) {
                                serverData.serverId = serverId;
                                serverData.reset();
                            }

                            initializeHandshake(server);
                            break;
                        case BridgeProtocolConstants.MESSAGE_ID_SERVER_OUTDATED:

                            rlExecutor.execute(() -> plugin.getLogger().warning(
                                    "Bridge plugin on server " + server.getInfo().getName() + " is outdated."));
                            break;
                        }
                    } catch (IOException ex) {
                        plugin.getLogger().log(Level.SEVERE,
                                "An unexpected error occurred while processing bridge data.", ex);
                    }
                }
            }
        }
    }

    @SuppressWarnings("unchecked")
    private void onDataReceived(DataCache cache, DataInput input, int size) throws IOException {
        if (size == 1) {
            int netId = input.readInt();
            DataKey<?> key = idMap.getKey(netId);

            if (key == null) {
                throw new AssertionError("Received unexpected data key net id " + netId);
            }

            boolean removed = input.readBoolean();

            if (removed) {

                plugin.runInMainThread(() -> cache.updateValue(key, null));
            } else {

                Object value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input);
                plugin.runInMainThread(() -> cache.updateValue((DataKey<Object>) key, value));
            }
        } else {
            Object[] update = new Object[size * 2];

            for (int i = 0; i < update.length; i += 2) {
                int netId = input.readInt();
                DataKey<?> key = idMap.getKey(netId);

                if (key == null) {
                    throw new AssertionError("Received unexpected data key net id " + netId);
                }

                boolean removed = input.readBoolean();
                Object value = null;

                if (!removed) {
                    value = typeAdapterRegistry.getTypeAdapter(key.getType()).read(input);
                }

                update[i] = key;
                update[i + 1] = value;
            }

            plugin.runInMainThread(() -> {
                for (int i = 0; i < update.length; i += 2) {
                    cache.updateValue((DataKey<Object>) update[i], update[i + 1]);
                }
            });
        }
    }

    @EventHandler
    public void onServerChange(ServerConnectedEvent event) {
        ProxiedPlayer player = event.getPlayer();
        ConnectedPlayer connectedPlayer = plugin.getConnectedPlayerManager().getPlayerIfPresent(player);
        if (connectedPlayer != null) {
            PlayerBridgeDataCache cache = connectedPlayer.getBridgeDataCache();
            disableConnection(cache);
        }
    }

    private void disableConnection(PlayerBridgeDataCache cache) {
        cache.connection = null;
        for (DataKey<?> key : cache.getQueriedKeys()) {
            cache.updateValue(key, null);
        }
    }

    @SneakyThrows
    private void initializeHandshake(Server server) {
        ByteArrayDataOutput data = ByteStreams.newDataOutput();
        data.writeByte(BridgeProtocolConstants.MESSAGE_ID_PROXY_HANDSHAKE);
        DataStreamUtils.writeUUID(data, proxyId);
        data.writeInt(BridgeProtocolConstants.VERSION);
        server.sendData(BridgeProtocolConstants.CHANNEL, data.toByteArray());
    }

    public class PlayerBridgeDataCache extends TrackingDataCache {
        private volatile Server connection = null;

        @Override
        @SneakyThrows
        protected <V> void onMissingData(DataKey<V> key) {
            super.onMissingData(key);
            if (connection != null) {
                ByteArrayDataOutput data = ByteStreams.newDataOutput();
                data.writeByte(BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_DATA);
                data.writeInt(1);
                DataStreamUtils.writeDataKey(data, key);
                data.writeInt(idMap.getNetId(key));
                connection.sendData(BridgeProtocolConstants.CHANNEL, data.toByteArray());
            }
        }
    }

    public class ServerBridgeDataCache extends TrackingDataCache {
        private final Set<DataKey<?>> receivedData = new CopyOnWriteArraySet<>();
        private ReferenceSet<Server> connections = new ReferenceOpenHashSet<>();

        private UUID serverId = UUID.randomUUID();

        private int lastRevision = 0;

        private synchronized void addConnection(Server server) {
            connections.add(server);
        }

        @Nullable
        private synchronized Server getConnection() {
            ObjectIterator<Server> iterator = connections.iterator();
            while (iterator.hasNext()) {
                Server server = iterator.next();
                if (server.isConnected()) {
                    return server;
                } else {
                    iterator.remove();
                }
            }
            return null;
        }

        private synchronized void removeObsoleteConnections() {
            ObjectIterator<Server> iterator = connections.iterator();
            while (iterator.hasNext()) {
                Server server = iterator.next();
                if (!server.isConnected()) {
                    iterator.remove();
                }
            }
        }

        @Override
        public <T> void updateValue(DataKey<T> dataKey, T object) {
            receivedData.add(dataKey);
            super.updateValue(dataKey, object);
        }

        @SneakyThrows
        private void requestMissingData() {
            if (getQueriedKeys().size() > receivedData.size()) {
                Server connection = getConnection();
                if (connection != null) {
                    List<DataKey<?>> keys = new LinkedList<>();
                    for (DataKey<?> key : getQueriedKeys()) {
                        if (!receivedData.contains(key)) {
                            keys.add(key);
                        }
                    }

                    ByteArrayDataOutput data = ByteStreams.newDataOutput();
                    data.writeByte(BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_SERVER_DATA);
                    data.writeInt(keys.size());
                    for (DataKey<?> key : keys) {
                        DataStreamUtils.writeDataKey(data, key);
                        data.writeInt(idMap.getNetId(key));
                    }
                    connection.sendData(BridgeProtocolConstants.CHANNEL, data.toByteArray());
                }
            }
        }

        private synchronized void reset() {
            connections.clear();
            for (DataKey<?> key : getQueriedKeys()) {
                super.updateValue(key, null);
            }
            receivedData.clear();
        }
    }
}