codecrafter47.bungeetablistplus.bukkitbridge.BukkitBridge.java Source code

Java tutorial

Introduction

Here is the source code for codecrafter47.bungeetablistplus.bukkitbridge.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.bukkitbridge;

import codecrafter47.bungeetablistplus.api.bukkit.BungeeTabListPlusBukkitAPI;
import codecrafter47.bungeetablistplus.api.bukkit.Variable;
import codecrafter47.bungeetablistplus.bukkitbridge.placeholderapi.PlaceholderAPIHook;
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 com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import de.codecrafter47.data.api.*;
import de.codecrafter47.data.bukkit.AbstractBukkitDataAccess;
import de.codecrafter47.data.bukkit.PlayerDataAccess;
import de.codecrafter47.data.bukkit.ServerDataAccess;
import de.codecrafter47.data.bukkit.api.BukkitData;
import de.codecrafter47.data.bungee.api.BungeeData;
import de.codecrafter47.data.minecraft.api.MinecraftData;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.bukkit.Server;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRegisterChannelEvent;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.Plugin;

import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
import java.util.logging.Level;

public class BukkitBridge extends BungeeTabListPlusBukkitAPI implements Listener {

    private static final TypeAdapterRegistry typeRegistry = TypeAdapterRegistry.DEFAULT_TYPE_ADAPTERS;

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

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

    private final UUID serverId = UUID.randomUUID();

    private final Plugin plugin;

    private final Map<Player, PlayerBridgeData> playerData = new ConcurrentHashMap<>();
    private final Map<UUID, ServerBridgeData> serverData = new ConcurrentHashMap<>();

    private DataAccess<Player> playerDataAccess;
    private DataAccess<Server> serverDataAccess;

    private PlaceholderAPIHook placeholderAPIHook = null;

    private final ReadWriteLock apiLock = new ReentrantReadWriteLock();
    private final Map<String, Variable> variablesByName = new HashMap<>();
    private final Multimap<Plugin, Variable> variablesByPlugin = HashMultimap.create();

    private final Consumer<String> missingDataKeyLogger = null;
    /*
    private final Consumer<String> missingDataKeyLogger = new Consumer<String>() {
        
    private final Set<String> missingKeys = Sets.newConcurrentHashSet();
        
    @Override
    public void accept(String id) {
        if (missingKeys.add(id)) {
            plugin.getLogger().warning("Missing data key with id " + id + ". Is the plugin up-to-date?");
        }
    }
    };
    */

    public BukkitBridge(Plugin plugin) {
        this.plugin = plugin;
    }

    public void onEnable() {
        try {
            Field field = BungeeTabListPlusBukkitAPI.class.getDeclaredField("instance");
            field.setAccessible(true);
            field.set(null, this);
        } catch (NoSuchFieldException | IllegalAccessException ex) {
            plugin.getLogger().log(Level.SEVERE, "Failed to initialize API", ex);
        }

        plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, BridgeProtocolConstants.CHANNEL);
        plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, BridgeProtocolConstants.CHANNEL,
                (string, player, bytes) -> {

                    DataInput input = new DataInputStream(new ByteArrayInputStream(bytes));

                    try {
                        int messageId = input.readUnsignedByte();

                        switch (messageId) {
                        case BridgeProtocolConstants.MESSAGE_ID_PROXY_HANDSHAKE:
                            UUID proxyId = DataStreamUtils.readUUID(input);
                            int protocolVersion = input.readInt();

                            if (protocolVersion > BridgeProtocolConstants.VERSION) {
                                rlExecutor.execute(() -> plugin.getLogger()
                                        .warning("BungeeTabListPlus_BukkitBridge is outdated."));
                            } else if (protocolVersion < BridgeProtocolConstants.VERSION) {
                                rlExecutor.execute(() -> plugin.getLogger()
                                        .warning("BungeeTabListPlus proxy plugin outdated."));
                            } else {
                                playerData.put(player, new PlayerBridgeData(proxyId));
                                serverData.computeIfAbsent(proxyId, uuid -> new ServerBridgeData());
                                ByteArrayDataOutput data = ByteStreams.newDataOutput();
                                data.writeByte(BridgeProtocolConstants.MESSAGE_ID_SERVER_HANDSHAKE);
                                player.sendPluginMessage(plugin, BridgeProtocolConstants.CHANNEL,
                                        data.toByteArray());
                            }

                            break;

                        case BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_DATA:
                            BridgeData bridgeData = playerData.get(player);
                            if (bridgeData != null) {
                                handleDataRequest(bridgeData, input);
                            }
                            break;

                        case BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_SERVER_DATA:
                            PlayerBridgeData playerBridgeData = playerData.get(player);
                            if (playerBridgeData != null) {
                                bridgeData = serverData.get(playerBridgeData.proxyId);
                                if (bridgeData != null) {
                                    handleDataRequest(bridgeData, input);
                                }
                            }
                            break;

                        case BridgeProtocolConstants.MESSAGE_ID_PROXY_OUTDATED:
                            rlExecutor.execute(
                                    () -> plugin.getLogger().warning("BungeeTabListPlus proxy plugin outdated."));
                            break;

                        case BridgeProtocolConstants.MESSAGE_ID_PROXY_REQUEST_RESET_SERVER_DATA:
                            playerBridgeData = playerData.get(player);
                            if (playerBridgeData != null) {
                                bridgeData = serverData.get(playerBridgeData.proxyId);
                                if (bridgeData != null) {
                                    serverData.put(playerBridgeData.proxyId, new ServerBridgeData());
                                }
                            }
                            break;

                        default:
                            plugin.getLogger().warning("Received unknown message id " + messageId);
                            break;
                        }
                    } catch (IOException ex) {
                        plugin.getLogger().log(Level.SEVERE,
                                "An unexpected error occurred while processing a plugin message.", ex);
                    }
                });

        plugin.getServer().getPluginManager().registerEvents(this, plugin);

        updateDataHooks();

        // initialize bridge for players already on the server
        for (Player player : plugin.getServer().getOnlinePlayers()) {
            ByteArrayDataOutput data = ByteStreams.newDataOutput();
            data.writeByte(BridgeProtocolConstants.MESSAGE_ID_SERVER_ENABLE_CONNECTION);
            try {
                DataStreamUtils.writeUUID(data, serverId);
            } catch (IOException e) {
                throw new AssertionError(e);
            }
            player.sendPluginMessage(plugin, BridgeProtocolConstants.CHANNEL, data.toByteArray());
        }

        // start update task
        plugin.getServer().getScheduler().runTaskTimerAsynchronously(plugin, () -> {
            long now = System.currentTimeMillis();
            Map<UUID, Player> proxyIds = new HashMap<>();

            for (Map.Entry<Player, PlayerBridgeData> e : playerData.entrySet()) {
                Player player = e.getKey();
                PlayerBridgeData bridgeData = e.getValue();

                proxyIds.putIfAbsent(bridgeData.proxyId, player);

                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) {
                    ByteArrayDataOutput data = ByteStreams.newDataOutput();
                    data.writeByte(BridgeProtocolConstants.MESSAGE_ID_SERVER_UPDATE_DATA);
                    data.writeInt(size);

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

                    player.sendPluginMessage(plugin, BridgeProtocolConstants.CHANNEL, data.toByteArray());
                }
            }

            for (Map.Entry<UUID, Player> e : proxyIds.entrySet()) {
                UUID proxyId = e.getKey();
                Player player = e.getValue();
                ServerBridgeData bridgeData = serverData.get(proxyId);

                if (bridgeData == null) {
                    continue;
                }

                bridgeData.lastUpdate = now;

                int size = 0;

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

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

                ByteArrayDataOutput data = ByteStreams.newDataOutput();
                data.writeByte(BridgeProtocolConstants.MESSAGE_ID_SERVER_UPDATE_SERVER_DATA);

                if (size > 0) {
                    bridgeData.revision++;
                }

                data.writeInt(bridgeData.revision);
                data.writeInt(size);

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

                player.sendPluginMessage(plugin, BridgeProtocolConstants.CHANNEL, data.toByteArray());
            }

            for (Iterator<ServerBridgeData> iterator = serverData.values().iterator(); iterator.hasNext();) {
                ServerBridgeData data = iterator.next();
                if (now - data.lastUpdate > 900000) {
                    iterator.remove();
                }
            }

        }, 20, 20);
    }

    private void handleDataRequest(BridgeData bridgeData, DataInput input) throws IOException {
        int size = input.readInt();
        for (int i = 0; i < size; i++) {
            DataKey<?> key = DataStreamUtils.readDataKey(input, keyRegistry, missingDataKeyLogger);
            int keyNetId = input.readInt();

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

    private void updateDataHooks() {
        if (plugin.getServer().getPluginManager().getPlugin("PlaceholderAPI") != null) {
            placeholderAPIHook = new PlaceholderAPIHook(plugin);
        } else {
            placeholderAPIHook = null;
        }

        if (placeholderAPIHook != null) {
            playerDataAccess = JoinedDataAccess.of(new PlayerDataAccess(plugin), new ThirdPartyVariablesAccess(),
                    placeholderAPIHook.getDataAccess());
        } else {
            playerDataAccess = JoinedDataAccess.of(new PlayerDataAccess(plugin), new ThirdPartyVariablesAccess());
        }
        serverDataAccess = JoinedDataAccess.of(new ServerDataAccess(plugin), new BTLPServerDataKeyAccess());
    }

    @EventHandler
    public void onPlayerLeave(PlayerQuitEvent event) {
        playerData.remove(event.getPlayer());
    }

    @EventHandler
    public void onPluginDisable(PluginDisableEvent event) {
        updateDataHooks();
    }

    @EventHandler
    public void onPluginEnable(PluginEnableEvent event) {
        updateDataHooks();
    }

    @EventHandler
    @SneakyThrows
    public void onChannelRegistration(PlayerRegisterChannelEvent event) {
        if (BridgeProtocolConstants.CHANNEL.equals(event.getChannel())) {
            ByteArrayDataOutput data = ByteStreams.newDataOutput();
            data.writeByte(BridgeProtocolConstants.MESSAGE_ID_SERVER_ENABLE_CONNECTION);
            DataStreamUtils.writeUUID(data, serverId);
            event.getPlayer().sendPluginMessage(plugin, BridgeProtocolConstants.CHANNEL, data.toByteArray());
        }
    }

    @Override
    protected void registerVariable0(Plugin plugin, Variable variable) {
        Preconditions.checkNotNull(plugin, "plugin");
        Preconditions.checkNotNull(variable, "variable");
        apiLock.writeLock().lock();
        try {
            Preconditions.checkArgument(!variablesByName.containsKey(variable.getName()),
                    "variable already registered");
            variablesByName.put(variable.getName(), variable);
            variablesByPlugin.put(plugin, variable);
        } finally {
            apiLock.writeLock().unlock();
        }
    }

    @Override
    protected void unregisterVariable0(Variable variable) {
        Preconditions.checkNotNull(variable, "variable");
        apiLock.writeLock().lock();
        try {
            Preconditions.checkArgument(variablesByName.remove(variable.getName(), variable),
                    "variable not registered");
            variablesByPlugin.values().remove(variable);
        } finally {
            apiLock.writeLock().unlock();
        }
    }

    @Override
    protected void unregisterVariables0(Plugin plugin) {
        Preconditions.checkNotNull(plugin, "plugin");
        apiLock.writeLock().lock();
        try {
            for (Variable variable : variablesByPlugin.removeAll(plugin)) {
                variablesByName.remove(variable.getName());
            }

        } finally {
            apiLock.writeLock().unlock();
        }
    }

    private class ThirdPartyVariablesAccess extends AbstractBukkitDataAccess<Player> {
        ThirdPartyVariablesAccess() {
            super(BukkitBridge.this.plugin.getLogger(), BukkitBridge.this.plugin);
            addProvider(BTLPDataKeys.ThirdPartyPlaceholder, this::resolveVariable);
        }

        private String resolveVariable(Player player, DataKey<String> key) {
            apiLock.readLock().lock();
            try {
                Variable variable = variablesByName.get(key.getParameter());
                if (variable != null) {
                    String replacement = null;
                    try {
                        replacement = variable.getReplacement(player);
                    } catch (Throwable th) {
                        plugin.getLogger().log(Level.WARNING,
                                "An exception occurred while resolving a variable provided by a third party plugin",
                                th);
                    }
                    return replacement;
                }
                return null;
            } finally {
                apiLock.readLock().unlock();
            }
        }
    }

    private class BTLPServerDataKeyAccess extends AbstractBukkitDataAccess<Server> {
        BTLPServerDataKeyAccess() {
            super(BukkitBridge.this.plugin.getLogger(), BukkitBridge.this.plugin);
            addProvider(BTLPDataKeys.REGISTERED_THIRD_PARTY_VARIABLES, server -> {
                apiLock.readLock().lock();
                try {
                    return Lists.newArrayList(variablesByName.keySet());
                } finally {
                    apiLock.readLock().unlock();
                }
            });
            addProvider(BTLPDataKeys.PLACEHOLDERAPI_PRESENT, server -> placeholderAPIHook != null);
            addProvider(BTLPDataKeys.PAPI_REGISTERED_PLACEHOLDER_PLUGINS,
                    server -> placeholderAPIHook != null ? placeholderAPIHook.getRegisteredPlaceholderPlugins()
                            : null);
        }
    }

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

    private static class BridgeData {
        protected final List<CacheEntry> requestedData = new CopyOnWriteArrayList<>();

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

            requestedData.add(new CacheEntry(key, netId));
        }
    }

    private static class PlayerBridgeData extends BridgeData {
        private UUID proxyId;

        public PlayerBridgeData(UUID proxyId) {
            this.proxyId = proxyId;
        }
    }

    private static class ServerBridgeData extends BridgeData {
        private int revision = 0;
        private long lastUpdate;
    }
}