codecrafter47.bungeetablistplus.BungeeTabListPlus.java Source code

Java tutorial

Introduction

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

import codecrafter47.bungeetablistplus.api.bungee.*;
import codecrafter47.bungeetablistplus.api.bungee.placeholder.PlaceholderProvider;
import codecrafter47.bungeetablistplus.api.bungee.tablist.TabListProvider;
import codecrafter47.bungeetablistplus.bridge.BukkitBridge;
import codecrafter47.bungeetablistplus.bridge.PlaceholderAPIHook;
import codecrafter47.bungeetablistplus.command.CommandBungeeTabListPlus;
import codecrafter47.bungeetablistplus.common.BugReportingService;
import codecrafter47.bungeetablistplus.common.network.BridgeProtocolConstants;
import codecrafter47.bungeetablistplus.config.MainConfig;
import codecrafter47.bungeetablistplus.data.BTLPBungeeDataKeys;
import codecrafter47.bungeetablistplus.listener.TabListListener;
import codecrafter47.bungeetablistplus.managers.*;
import codecrafter47.bungeetablistplus.placeholder.*;
import codecrafter47.bungeetablistplus.player.ConnectedPlayer;
import codecrafter47.bungeetablistplus.player.FakePlayerManagerImpl;
import codecrafter47.bungeetablistplus.player.IPlayerProvider;
import codecrafter47.bungeetablistplus.player.Player;
import codecrafter47.bungeetablistplus.protocol.ProtocolManager;
import codecrafter47.bungeetablistplus.tablist.DefaultCustomTablist;
import codecrafter47.bungeetablistplus.tablistproviders.legacy.CheckedTabListProvider;
import codecrafter47.bungeetablistplus.updater.UpdateChecker;
import codecrafter47.bungeetablistplus.updater.UpdateNotifier;
import codecrafter47.bungeetablistplus.util.PingTask;
import codecrafter47.bungeetablistplus.version.BungeeProtocolVersionProvider;
import codecrafter47.bungeetablistplus.version.ProtocolSupportVersionProvider;
import codecrafter47.bungeetablistplus.version.ProtocolVersionProvider;
import codecrafter47.bungeetablistplus.yamlconfig.YamlConfig;
import codecrafter47.util.chat.ChatUtil;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import de.codecrafter47.data.api.DataKey;
import de.codecrafter47.data.bukkit.api.BukkitData;
import de.codecrafter47.data.bungee.api.BungeeData;
import de.sabbertran.proxysuite.ProxySuiteAPI;
import lombok.Getter;
import net.md_5.bungee.UserConnection;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.ProxyServer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.scheduler.ScheduledTask;
import net.md_5.bungee.connection.LoginResult;
import org.yaml.snakeyaml.error.YAMLException;

import javax.annotation.Nonnull;
import java.awt.image.BufferedImage;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Main Class of BungeeTabListPlus
 *
 * @author Florian Stober
 */
public class BungeeTabListPlus extends BungeeTabListPlusAPI {

    /**
     * Holds an INSTANCE of itself if the plugin is enabled
     */
    private static BungeeTabListPlus INSTANCE;
    @Getter
    private final Plugin plugin;
    public Collection<IPlayerProvider> playerProviders;
    @Getter
    private ResendThread resendThread;

    @Getter
    private RedisPlayerManager redisPlayerManager;
    @Getter
    private DataManager dataManager;
    private BugReportingService bugReportingService;

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

    /**
     * Static getter for the current instance of the plugin
     *
     * @return the current instance of the plugin, null if the plugin is
     * disabled
     */
    public static BungeeTabListPlus getInstance(Plugin plugin) {
        if (INSTANCE == null) {
            INSTANCE = new BungeeTabListPlus(plugin);
        }
        return INSTANCE;
    }

    public static BungeeTabListPlus getInstance() {
        return INSTANCE;
    }

    @Getter
    private MainConfig config;

    private FakePlayerManagerImpl fakePlayerManager;

    /**
     * provides access to the Placeholder Manager use this to add Placeholders
     */
    private PlaceholderManagerImpl placeholderManager;

    private PermissionManager pm;

    private TabListManager tabLists;
    private final TabListListener listener = new TabListListener(this);

    private ScheduledTask refreshThread = null;

    private final static Collection<String> hiddenPlayers = new HashSet<>();

    private BukkitBridge bukkitBridge;

    private UpdateChecker updateChecker = null;

    private final Map<String, PingTask> serverState = new HashMap<>();

    private SkinManager skins;

    @Getter
    private ConnectedPlayerManager connectedPlayerManager = new ConnectedPlayerManager();

    @Getter
    private PlaceholderAPIHook placeholderAPIHook;

    public PingTask getServerState(String serverName) {
        if (serverState.containsKey(serverName)) {
            return serverState.get(serverName);
        }
        ServerInfo serverInfo = ProxyServer.getInstance().getServerInfo(serverName);
        if (serverInfo != null) {
            // start server ping tasks
            int delay = config.pingDelay;
            if (delay <= 0 || delay > 10) {
                delay = 10;
            }
            PingTask task = new PingTask(serverInfo);
            serverState.put(serverName, task);
            plugin.getProxy().getScheduler().schedule(plugin, task, delay, delay, TimeUnit.SECONDS);
        }
        return serverState.get(serverName);
    }

    @Getter
    private ProtocolVersionProvider protocolVersionProvider;

    private Map<Float, Set<Runnable>> scheduledTasks = new ConcurrentHashMap<>();

    /**
     * Called when the plugin is enabled
     */
    public void onEnable() {
        if (!plugin.getDataFolder().exists()) {
            plugin.getDataFolder().mkdirs();
        }

        try {
            Class.forName("net.md_5.bungee.api.Title");
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException("You need to run at least BungeeCord version #995");
        }

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

        INSTANCE = this;

        try {
            File file = new File(plugin.getDataFolder(), "config.yml");
            if (!file.exists()) {
                config = new MainConfig();
                BufferedWriter writer = new BufferedWriter(
                        new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8));
                YamlConfig.writeWithComments(writer, config, "This is the configuration file of BungeeTabListPlus",
                        "See https://github.com/CodeCrafter47/BungeeTabListPlus/wiki for additional information");
            } else {
                config = YamlConfig.read(new FileInputStream(file), MainConfig.class);
            }
        } catch (IOException | YAMLException ex) {
            plugin.getLogger().warning("Unable to load Config");
            plugin.getLogger().log(Level.WARNING, null, ex);
            plugin.getLogger().warning("Disabling Plugin");
            return;
        }

        if (config.automaticallySendBugReports) {
            String revision = "unknown";
            try {
                Properties current = new Properties();
                current.load(getClass().getClassLoader().getResourceAsStream("version.properties"));
                revision = current.getProperty("revision", revision);
            } catch (IOException ex) {
                getLogger().log(Level.SEVERE, "Unexpected exception", ex);
            }

            String version = getPlugin().getDescription().getVersion();

            if (!"unknown".equals(revision)) {
                version += "-git-" + revision;
            }

            String systemInfo = "" + "System Info\n" + "===========\n" + "Bungee: " + getProxy().getVersion() + "\n"
                    + "Java: " + System.getProperty("java.version") + "\n";

            bugReportingService = new BugReportingService(Level.SEVERE, getPlugin().getDescription().getName(),
                    version, command -> plugin.getProxy().getScheduler().runAsync(plugin, command), systemInfo);
            bugReportingService.registerLogger(getLogger());
        }

        resendThread = new ResendThread();

        File headsFolder = new File(plugin.getDataFolder(), "heads");

        if (!headsFolder.exists()) {
            headsFolder.mkdirs();

            try {
                // copy default heads
                ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(plugin.getFile()));

                ZipEntry entry;
                while ((entry = zipInputStream.getNextEntry()) != null) {
                    if (!entry.isDirectory() && entry.getName().startsWith("heads/")) {
                        try {
                            File targetFile = new File(plugin.getDataFolder(), entry.getName());
                            targetFile.getParentFile().mkdirs();
                            if (!targetFile.exists()) {
                                Files.copy(zipInputStream, targetFile.toPath());
                                getLogger().info("Extracted " + entry.getName());
                            }
                        } catch (IOException ex) {
                            getLogger().log(Level.SEVERE, "Failed to extract file " + entry.getName(), ex);
                        }
                    }
                }

                zipInputStream.close();
            } catch (IOException ex) {
                getLogger().log(Level.SEVERE, "Error extracting files", ex);
            }
        }

        skins = new SkinManagerImpl(plugin, headsFolder);

        fakePlayerManager = new FakePlayerManagerImpl(plugin);

        playerProviders = new ArrayList<>();

        if (plugin.getProxy().getPluginManager().getPlugin("RedisBungee") != null) {
            redisPlayerManager = new RedisPlayerManager(connectedPlayerManager, this, getLogger());
            playerProviders.add(redisPlayerManager);
            plugin.getLogger().info("Hooked RedisBungee");
        }

        playerProviders.add(connectedPlayerManager);

        playerProviders.add(fakePlayerManager);

        plugin.getProxy().registerChannel(BridgeProtocolConstants.CHANNEL);
        bukkitBridge = new BukkitBridge(this);

        pm = new PermissionManager(this);

        dataManager = new DataManager(this);

        placeholderManager = new PlaceholderManagerImpl();
        placeholderManager.internalRegisterPlaceholderProvider(new BasicPlaceholders());
        placeholderManager.internalRegisterPlaceholderProvider(new BukkitPlaceholders());
        placeholderManager.internalRegisterPlaceholderProvider(new ColorPlaceholder());
        placeholderManager.internalRegisterPlaceholderProvider(new ConditionalPlaceholders());
        placeholderManager.internalRegisterPlaceholderProvider(new OnlineStatePlaceholder());
        placeholderManager.internalRegisterPlaceholderProvider(new PlayerCountPlaceholder());
        if (plugin.getProxy().getPluginManager().getPlugin("RedisBungee") != null) {
            placeholderManager.internalRegisterPlaceholderProvider(new RedisBungeePlaceholders());
        }
        placeholderManager.internalRegisterPlaceholderProvider(new TimePlaceholders());

        if (plugin.getProxy().getPluginManager().getPlugin("ProtocolSupportBungee") != null) {
            protocolVersionProvider = new ProtocolSupportVersionProvider();
        } else {
            protocolVersionProvider = new BungeeProtocolVersionProvider();
        }

        // register commands and update Notifier
        ProxyServer.getInstance().getPluginManager().registerCommand(plugin, new CommandBungeeTabListPlus());
        ProxyServer.getInstance().getScheduler().schedule(plugin, new UpdateNotifier(this), 15, 15,
                TimeUnit.MINUTES);

        // Start metrics
        try {
            Metrics metrics = new Metrics(plugin);
            metrics.start();
        } catch (IOException e) {
            plugin.getLogger().log(Level.SEVERE, "Failed to initialize Metrics", e);
        }

        // Load updateCheck thread
        if (config.checkForUpdates) {
            updateChecker = new UpdateChecker(plugin);
            plugin.getLogger().info("Starting UpdateChecker Task");
            plugin.getProxy().getScheduler()
                    .schedule(plugin, updateChecker, 0, UpdateChecker.interval, TimeUnit.MINUTES).getId();
        }

        // Start packet listeners
        ProtocolManager protocolManager = new ProtocolManager(plugin);
        protocolManager.enable();

        int[] serversHash = { getProxy().getServers().hashCode() };
        getProxy().getScheduler().schedule(plugin, () -> {
            int hash = getProxy().getServers().hashCode();
            if (hash != serversHash[0]) {
                serversHash[0] = hash;
                getLogger().info("Network topology change detected. Reloading plugin.");
                reload();
            }
        }, 1, 1, TimeUnit.MINUTES);

        placeholderAPIHook = new PlaceholderAPIHook(this);

        tabLists = new TabListManager(this);
        if (!tabLists.loadTabLists()) {
            return;
        }

        ProxyServer.getInstance().getPluginManager().registerListener(plugin, listener);
        plugin.getProxy().getScheduler().runAsync(plugin, resendThread);
        restartRefreshThread();

        plugin.getProxy().getScheduler().schedule(plugin, () -> {
            ImmutableList<String> filesNeedingUpgrade = tabLists.getFilesNeedingUpgrade();
            if (!filesNeedingUpgrade.isEmpty()) {
                BaseComponent[] message = ChatUtil.parseBBCode(
                        "&c[BungeeTabListPlus] Warning: &eThe following tablist configuration files need to be upgraded: &f"
                                + Joiner.on(", ").join(filesNeedingUpgrade)
                                + "\n&eYou can find more information at &b[url]https://github.com/CodeCrafter47/BungeeTabListPlus/wiki/Updating[/url]&e.");
                for (ProxiedPlayer player : plugin.getProxy().getPlayers()) {
                    if (getPermissionManager().hasPermission(player, "bungeetablistplus.admin")) {
                        player.sendMessage(message);
                    }
                }
            }
        }, 15, 15, TimeUnit.MINUTES);
    }

    public void onDisable() {
        if (bugReportingService != null) {
            bugReportingService.unregisterLogger(getLogger());
        }
    }

    private Double requestedUpdateInterval = null;

    private void restartRefreshThread() {
        if (refreshThread != null) {
            refreshThread.cancel();
        }
        double updateInterval = config.tablistUpdateInterval;
        if (updateInterval <= 0 || updateInterval > 2) {
            updateInterval = 2;
        }
        if (requestedUpdateInterval != null && (requestedUpdateInterval < updateInterval || updateInterval <= 0)) {
            updateInterval = requestedUpdateInterval;
        }
        if (updateInterval > 0) {
            try {
                refreshThread = ProxyServer.getInstance().getScheduler().schedule(plugin, this::resendTabLists,
                        (long) (updateInterval * 1000), (long) (updateInterval * 1000), TimeUnit.MILLISECONDS);
            } catch (RejectedExecutionException ignored) {
                // this occurs on proxy shutdown -> we can safely ignore it
            }
        } else {
            refreshThread = null;
        }
    }

    public void requireUpdateInterval(double updateInterval) {
        if (requestedUpdateInterval == null || updateInterval < requestedUpdateInterval) {
            requestedUpdateInterval = updateInterval;
            restartRefreshThread();
        }
    }

    /**
     * Reloads most settings of the plugin
     */
    public boolean reload() {
        if (!resendThread.isInMainThread()) {
            AtomicReference<Boolean> ref = new AtomicReference<>(null);
            resendThread.execute(() -> {
                ref.set(reload());
            });
            while (ref.get() == null) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException ignored) {
                    return false;
                }
            }
            return ref.get();
        }
        failIfNotMainThread();
        try {
            // todo requestedUpdateInterval = null;
            fakePlayerManager.removeConfigFakePlayers();
            config = YamlConfig.read(new FileInputStream(new File(plugin.getDataFolder(), "config.yml")),
                    MainConfig.class);
            placeholderManager.reload();
            if (reloadTablists())
                return false;
            fakePlayerManager.reload();
            resendTabLists();
            restartRefreshThread();
            skins.onReload();
        } catch (IOException | YAMLException ex) {
            plugin.getLogger().log(Level.WARNING, "Unable to reload Config", ex);
            return false;
        }
        return true;
    }

    private boolean reloadTablists() {
        failIfNotMainThread();
        TabListManager tabListManager = new TabListManager(this);
        if (!tabListManager.loadTabLists()) {
            return true;
        }
        tabListManager.customTabLists = tabLists.customTabLists;
        tabLists = tabListManager;
        return false;
    }

    @Override
    protected void registerVariable0(Plugin plugin, Variable variable) {
        Preconditions.checkNotNull(plugin, "plugin");
        Preconditions.checkNotNull(variable, "variable");
        Preconditions.checkArgument(!Placeholder.thirdPartyDataKeys.containsKey(variable.getName()),
                "Variable name already registered.");
        DataKey<String> dataKey = BTLPBungeeDataKeys.createBungeeThirdPartyVariableDataKey(variable.getName());
        Placeholder.thirdPartyDataKeys.put(variable.getName(), dataKey);
        getProxy().getScheduler().schedule(plugin, () -> {
            for (ConnectedPlayer player : connectedPlayerManager.getPlayers()) {
                try {
                    String replacement = variable.getReplacement(player.getPlayer());
                    if (!Objects.equals(replacement, player.getLocalDataCache().get(dataKey))) {
                        runInMainThread(() -> {
                            player.getLocalDataCache().updateValue(dataKey, replacement);
                        });
                    }
                } catch (Throwable th) {
                    getLogger().log(Level.WARNING, "Failed to resolve Placeholder " + variable.getName(), th);
                }
            }

        }, 1, 1, TimeUnit.SECONDS);
        runInMainThread(this::reloadTablists);
    }

    public void registerPlaceholderProvider0(PlaceholderProvider placeholderProvider) {
        getPlaceholderManager0().internalRegisterPlaceholderProvider(placeholderProvider);
        runInMainThread(this::reloadTablists);
    }

    @Override
    protected CustomTablist createCustomTablist0() {
        return new DefaultCustomTablist();
    }

    /**
     * updates the tabList on all connected clients
     */
    public void resendTabLists() {
        for (ProxiedPlayer player : ProxyServer.getInstance().getPlayers()) {
            resendThread.add(player);
        }
    }

    public void runInMainThread(Runnable runnable) {
        resendThread.execute(runnable);
    }

    public void failIfNotMainThread() {
        if (!resendThread.isInMainThread()) {
            getLogger().log(Level.SEVERE, "Not in main thread", new IllegalStateException("Not in main thread"));
        }
    }

    public void updateTabListForPlayer(ProxiedPlayer player) {
        resendThread.add(player);
    }

    /**
     * Getter for an instance of the PlayerManager. For internal use only.
     *
     * @return an instance of the PlayerManager or null
     */
    public PlayerManager constructPlayerManager(ProxiedPlayer viewer) {
        return new PlayerManagerImpl(this, playerProviders, viewer);
    }

    public SkinManager getSkinManager() {
        return skins;
    }

    /**
     * Getter for the PermissionManager. For internal use only.
     *
     * @return an instance of the PermissionManager or null
     */
    public PermissionManager getPermissionManager() {
        return pm;
    }

    public PlaceholderManagerImpl getPlaceholderManager0() {
        return placeholderManager;
    }

    @Override
    protected FakePlayerManager getFakePlayerManager0() {
        return fakePlayerManager;
    }

    /**
     * Getter for the TabListManager. For internal use only
     *
     * @return an instance of the TabListManager
     */
    public TabListManager getTabListManager() {
        return tabLists;
    }

    /**
     * checks whether a player is hidden from the tablist
     *
     * @param player the player object for which the check should be performed
     * @return true if the player is hidden, false otherwise
     */
    public static boolean isHidden(Player player) {
        if (player.getOpt(BungeeData.BungeeCord_Server).map(BungeeTabListPlus::isHiddenServer).orElse(false)) {
            return true;
        }
        final boolean[] hidden = new boolean[1];
        synchronized (hiddenPlayers) {
            String name = player.getName();
            hidden[0] = hiddenPlayers.contains(name);
        }
        List<String> permanentlyHiddenPlayers = getInstance().config.hiddenPlayers;
        if (permanentlyHiddenPlayers.contains(player.getName())) {
            hidden[0] = true;
        }
        if (permanentlyHiddenPlayers.contains(player.getUniqueID().toString())) {
            hidden[0] = true;
        }
        player.getOpt(BukkitData.VanishNoPacket_IsVanished).ifPresent(b -> hidden[0] |= b);
        player.getOpt(BukkitData.SuperVanish_IsVanished).ifPresent(b -> hidden[0] |= b);
        player.getOpt(BukkitData.Essentials_IsVanished).ifPresent(b -> hidden[0] |= b);

        // check ProxyCore
        if (!hidden[0]) {
            if (ProxyServer.getInstance().getPluginManager().getPlugin("ProxySuite") != null) {
                try {
                    ProxiedPlayer proxiedPlayer = ProxyServer.getInstance().getPlayer(player.getName());
                    if (proxiedPlayer != null) {
                        hidden[0] |= ProxySuiteAPI.isVanished(proxiedPlayer);
                    }
                } catch (Throwable th) {
                    getInstance().getLogger().log(Level.WARNING,
                            "An error occurred while looking up a players vanish status from ProxyCore.", th);
                }
            }
        }

        return hidden[0];
    }

    /**
     * Hides a player from the tablist
     *
     * @param player The player which should be hidden.
     */
    public static void hidePlayer(ProxiedPlayer player) {
        synchronized (hiddenPlayers) {
            String name = player.getName();
            if (!hiddenPlayers.contains(name))
                hiddenPlayers.add(name);
        }
    }

    /**
     * Unhides a previously hidden player from the tablist. Only works if the
     * playe has been hidden via the hidePlayer method. Not works for players
     * hidden by VanishNoPacket
     *
     * @param player the player on which the operation should be performed
     */
    public static void unhidePlayer(ProxiedPlayer player) {
        synchronized (hiddenPlayers) {
            String name = player.getName();
            hiddenPlayers.remove(name);
        }
    }

    public static boolean isHiddenServer(String serverName) {
        return getInstance().config.hiddenServers.contains(serverName);
    }

    /**
     * Getter for BukkitBridge. For internal use only.
     *
     * @return an instance of BukkitBridge
     */
    public BukkitBridge getBridge() {
        return this.bukkitBridge;
    }

    /**
     * Checks whether an update for BungeeTabListPlus is available. Acctually
     * the check is performed in a background task and this only returns the
     * result.
     *
     * @return true if an newer version of BungeeTabListPlus is available
     */
    public boolean isUpdateAvailable() {
        return updateChecker != null && updateChecker.isUpdateAvailable();
    }

    public boolean isNewDevBuildAvailable() {
        return updateChecker != null && updateChecker.isNewDevBuildAvailable();
    }

    public void reportError(Throwable th) {
        plugin.getLogger().log(Level.SEVERE, ChatColor.RED + "An internal error occurred! Please send the "
                + "following StackTrace to the developer in order to help" + " resolving the problem", th);
    }

    public Logger getLogger() {
        return plugin.getLogger();
    }

    public ProxyServer getProxy() {
        return plugin.getProxy();
    }

    public boolean isServer(String s) {
        for (ServerInfo server : ProxyServer.getInstance().getServers().values()) {
            if (s.equalsIgnoreCase(server.getName())) {
                return true;
            }
            int i = s.indexOf('#');
            if (i > 1) {
                if (s.substring(0, i).equalsIgnoreCase(server.getName())) {
                    return true;
                }
            }
        }
        return false;
    }

    private final static Pattern PATTERN_VALID_USERNAME = Pattern.compile("(?:\\p{Alnum}|_){1,16}");

    @Override
    protected Skin getSkinForPlayer0(String nameOrUUID) {
        if (!PATTERN_VALID_USERNAME.matcher(nameOrUUID).matches()) {
            try {
                UUID.fromString(nameOrUUID); // TODO: 02.06.16 this is slow
            } catch (IllegalArgumentException ex) {
                throw new IllegalArgumentException(
                        "Given string is neither valid username nor uuid: " + nameOrUUID);
            }
        }
        Preconditions.checkState(getSkinManager() != null, "BungeeTabListPlus not initialized");
        return getSkinManager().getSkin(nameOrUUID);
    }

    @Override
    protected Skin getDefaultSkin0() {
        return SkinManager.defaultSkin;
    }

    @Override
    protected void requireTabListUpdateInterval0(double interval) {
        requireUpdateInterval(interval);
    }

    @Override
    protected void setCustomTabList0(ProxiedPlayer player, TabListProvider tabListProvider) {
        Preconditions.checkState(getTabListManager() != null, "BungeeTabListPlus not initialized");
        getTabListManager().setCustomTabList(player, new CheckedTabListProvider(tabListProvider));
    }

    @Override
    protected void setCustomTabList0(ProxiedPlayer player, CustomTablist customTablist) {
        ConnectedPlayer connectedPlayer = getConnectedPlayerManager().getPlayerIfPresent(player);
        if (connectedPlayer != null) {
            connectedPlayer.setCustomTablist(customTablist);
        }
        updateTabListForPlayer(player);
    }

    @Override
    protected void removeCustomTabList0(ProxiedPlayer player) {
        Preconditions.checkState(getTabListManager() != null, "BungeeTabListPlus not initialized");
        getTabListManager().removeCustomTabList(player);
        ConnectedPlayer connectedPlayer = getConnectedPlayerManager().getPlayerIfPresent(player);
        if (connectedPlayer != null) {
            connectedPlayer.setCustomTablist(null);
        }
        updateTabListForPlayer(player);
    }

    @Nonnull
    @Override
    protected Icon getIconFromPlayer0(ProxiedPlayer player) {
        LoginResult loginResult = ((UserConnection) player).getPendingConnection().getLoginProfile();
        if (loginResult != null) {
            LoginResult.Property[] properties = loginResult.getProperties();
            if (properties != null) {
                for (LoginResult.Property s : properties) {
                    if (s.getName().equals("textures")) {
                        return new Icon(player.getUniqueId(),
                                new String[][] { { s.getName(), s.getValue(), s.getSignature() } });
                    }
                }
            }
        }
        return new Icon(player.getUniqueId(), new String[0][]);
    }

    @Override
    protected void createIcon0(BufferedImage image, Consumer<Icon> callback) {
        getSkinManager().createIcon(image, callback);
    }

    public void registerTask(float interval, Runnable task) {
        boolean first = !scheduledTasks.containsKey(interval);
        scheduledTasks.computeIfAbsent(interval, f -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
                .add(task);
        if (first) {
            getProxy().getScheduler().schedule(getPlugin(),
                    () -> scheduledTasks.get(interval).forEach(Runnable::run), (long) (interval * 1000),
                    (long) (interval * 1000), TimeUnit.MILLISECONDS);
        }
    }

    public void unregisterTask(float interval, Runnable task) {
        scheduledTasks.get(interval).remove(task);
    }
}