BungeeMain / src / main / java / com / lifeknight / relaymcbungeemain / player / SmartPlayer.java
SmartPlayer.java
Raw
package com.lifeknight.relaymcbungeemain.player;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.lifeknight.relaymcbungeemain.Main;
import com.lifeknight.relaymcbungeemain.commands.CommandUtilities;
import com.lifeknight.relaymcbungeemain.commands.game.DuelAcceptCommand;
import com.lifeknight.relaymcbungeemain.queue.GameServer;
import com.lifeknight.relaymcbungeemain.queue.Queue;
import com.lifeknight.relaymcbungeemain.utilities.*;
import com.lifeknight.relayutils.RelayUtils;
import com.lifeknight.relayutils.basic.Miscellaneous;
import com.lifeknight.relayutils.basic.SwearUtils;
import com.lifeknight.relayutils.basic.Text;
import com.lifeknight.relayutils.data.QueueData;
import com.lifeknight.relayutils.filter.*;
import com.lifeknight.relayutils.game.GameDetails;
import com.lifeknight.relayutils.game.GameType;
import com.lifeknight.relayutils.game.League;
import com.lifeknight.relayutils.game.MainGame;
import com.lifeknight.relayutils.network.Command;
import com.lifeknight.relayutils.network.Message;
import com.lifeknight.relayutils.player.ExtraData;
import com.lifeknight.relayutils.player.Group;
import com.lifeknight.relayutils.player.Nick;
import com.lifeknight.relayutils.player.cosmetics.Cosmetic;
import com.lifeknight.relayutils.player.cosmetics.GameCosmetic;
import com.lifeknight.relayutils.player.data.ProfileColumn;
import com.lifeknight.relayutils.player.data.SmartDatabase;
import com.lifeknight.relayutils.player.punishments.Punishment;
import com.lifeknight.relayutils.player.punishments.PunishmentType;
import com.lifeknight.relayutils.utilities.ComponentBuilder;
import net.md_5.bungee.api.ChatColor;
import net.md_5.bungee.api.CommandSender;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.connection.Connection;
import net.md_5.bungee.api.connection.ProxiedPlayer;
import net.md_5.bungee.api.event.PlayerDisconnectEvent;
import net.md_5.bungee.api.event.PostLoginEvent;
import net.md_5.bungee.protocol.DefinedPacket;
import net.md_5.bungee.protocol.packet.ScoreboardDisplay;
import net.md_5.bungee.protocol.packet.ScoreboardObjective;
import net.md_5.bungee.protocol.packet.ScoreboardScore;
import org.bukkit.Sound;

import java.awt.*;
import java.lang.instrument.Instrumentation;
import java.sql.ResultSet;
import java.util.List;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.lifeknight.relayutils.basic.Text.*;
import static net.md_5.bungee.api.ChatColor.*;

public class SmartPlayer {
    public static final Map<UUID, SmartPlayer> SMART_PLAYERS = new HashMap<>();
    public static final List<SmartPlayer> ONLINE_PLAYERS = new ArrayList<>();

    public static final List<String> BANNED_IPS = new ArrayList<>();
    private static final List<SmartPlayer> LIMBO_QUEUE = new ArrayList<>();

    private final long inception;
    private final UUID uuid;
    private ProxiedPlayer proxiedPlayer;
    private long lastDisconnectTime;
    private final long createTime = System.currentTimeMillis();
    private final List<BaseComponent> queuedMessages = new ArrayList<>();

    private boolean hasJoined = false;

    private String lastName = null;
    private long firstJoinTime = System.currentTimeMillis();
    private long previousLastJoinTime = System.currentTimeMillis();
    private long joinTime = System.currentTimeMillis();
    private boolean isNewLogin;
    private List<UUID> friends = new ArrayList<>();
    private List<UUID> blockedPlayers = new ArrayList<>();
    private List<String> previousUsernames = new ArrayList<>();
    private List<Group> groups = Miscellaneous.getList(Group.DEFAULT);
    private long credits = 0;
    private long xp = 0;
    private long timePlayed = 0;
    private List<Cosmetic> cosmetics = new ArrayList<>();
    private List<Cosmetic> activeCosmetics = new ArrayList<>();

    private Map<MainGame, List<GameCosmetic>> gameCosmetics = new HashMap<>();
    private Map<MainGame, List<GameCosmetic>> activeGameCosmetics = new HashMap<>();

    private List<Punishment> punishments = new ArrayList<>();

    private boolean isNicked = false;
    private Nick nick = null;

    private ChatFilter chatFilter = ChatFilter.MEDIUM;
    private MessageFilter messageFilter = MessageFilter.FRIENDS_OF_FRIENDS;
    private FriendFilter friendFilter = FriendFilter.ANYBODY;
    private PartyFilter partyFilter = PartyFilter.ANYBODY;
    private DuelFilter duelFilter = DuelFilter.ANYBODY;

    private SmartPlayer lastMessageSender = null;

    private final Map<Long, String> lastMessages = new HashMap<>();
    private List<String> latestMessagesWithin3Seconds = new ArrayList<>();
    private int commandsInPast5Seconds = 0;

    private final List<ChatMessage> messages = new ArrayList<>();

    private ExtraData extraData = new ExtraData(new JsonObject());

    private GameType currentQueue;
    private boolean hasUpdatedQueueScoreboard;
    private ChatType chatType = ChatType.ALL;
    private Party currentParty = null;
    private List<Party> invitedParties = new ArrayList<>();
    private Map<SmartPlayer, GameType> invitedDuels = new HashMap<>();
    private List<SmartPlayer> receivedFriendRequests = new ArrayList<>();
    private Server lastGameServer;
    public GameType lastGameType;
    private Server lastServer;
    private ServerInfo connectingServer = null;
    private Server lastHubServer;

    private final Map<MainGame, Long> gameExperience = new HashMap<>();
    private final Map<GameType, Long> gameElo = new HashMap<>();

    private int manhuntQuits = 0;
    private boolean isBannedFromManhunt = false;
    private long lastReportTime;
    private boolean hasReadData = false;

    private boolean receiveReportMessages = true;
    private long lastLevelUp;
    private SmartPlayer rematchOpponent = null;
    private GameType rematchGameType = null;
    private QueueData rematchQueueData = null;
    private boolean acceptRematch = false;
    private QueueData queueData = null;
    private long discordId;
    private int discordEarned = 0;
    private long lastDiscordMessage = System.currentTimeMillis();
    private long lastConnectTime;

    public SmartPlayer(ProxiedPlayer proxiedPlayer) {
        this.inception = System.currentTimeMillis();
        this.proxiedPlayer = proxiedPlayer;
        this.uuid = this.proxiedPlayer.getUniqueId();
        SMART_PLAYERS.put(proxiedPlayer.getUniqueId(), this);
        removeUnneededSmartPlayers();
    }

    public static void removeUnneededSmartPlayers() {
        for (SmartPlayer value : Miscellaneous.getList(SMART_PLAYERS.values())) {
            if (!value.isOnline() && !value.hasOnlineFriend() && Miscellaneous.enoughTimeHasPassed(value.inception, 10, TimeUnit.MINUTES) && Miscellaneous.enoughTimeHasPassed(value.createTime, 3, TimeUnit.HOURS)) {
                SMART_PLAYERS.remove(value.getUUID());
                if (RelayUtils.log) {
                    Main.info("Removed SmartPlayer %s for inactivity.", value.getName());
                }
            }
        }
    }

    public SmartPlayer(UUID uuid) {
        this.inception = System.currentTimeMillis();
        this.uuid = uuid;
        SMART_PLAYERS.put(uuid, this);
        this.getData();
        removeUnneededSmartPlayers();
    }

    public static long getMemoryUsed() {
        Instrumentation inst = Main.getInstrumentation();
        long memory = 0;
        if (inst != null) {
            for (SmartPlayer smartPlayer : getSmartPlayers()) {
                memory += inst.getObjectSize(smartPlayer);
            }
        }

        return memory;
    }

    public void getData() {
        int previousLevel = this.calculateLevel();
        Map<MainGame, Integer> previousLevels = new HashMap<>();
        this.gameExperience.forEach((mainGame, aLong) -> previousLevels.put(mainGame, RelayUtils.calculateLevel(mainGame, aLong)));
        Main.async(() -> {
            try {
                ResultSet resultSet = SmartDatabase.getProfile(this.getUUID()).get(10, TimeUnit.SECONDS);

                Main.synchronous(() -> this.processResultSet(resultSet));

                for (MainGame value : MainGame.values()) {
                    ResultSet stats = SmartDatabase.getStatistics(value, this.getUUID()).get(5, TimeUnit.SECONDS);
                    if (stats.first()) {
                        this.gameExperience.put(value, stats.getLong(ProfileColumn.EXPERIENCE.getName()));
                        //this.gameElo.put(value, stats.getLong("elo"));
                    } else {
                        this.gameExperience.putIfAbsent(value, 0L);
                        //this.gameElo.putIfAbsent(value, 0L);
                    }
                }

                Main.synchronous(() -> {
                    if (this.hasReadData && Miscellaneous.enoughTimeHasPassed(this.lastLevelUp, 5000L)) {
                        this.gameExperience.forEach((mainGame, aLong) -> {
                            int level = RelayUtils.calculateLevel(mainGame, aLong);
                            int previous = previousLevels.getOrDefault(mainGame, 0);
                            if (level > 1 && level > previous) {
                                this.levelUp(mainGame);
                            }
                        });

                        int currentLevel = this.calculateLevel();
                        if (currentLevel > 1 && currentLevel > previousLevel) {
                            this.levelUp(null);
                        }
                    } else {
                        this.hasReadData = true;
                    }
                });
            } catch (TimeoutException exception) {
                Main.warn("Time out exception for profile database: (%s) %s", this.getFormattedName(), exception.getMessage());
            } catch (Exception exception) {
                Main.warn("Error occurred while trying to get data for %s: %s", this.getFormattedName(), exception.getMessage());
            }
        });

    }

    public static void onPlayerLeave(PlayerDisconnectEvent event) {
        getSmartPlayer(event.getPlayer()).onDisconnect();
    }

    public static synchronized SmartPlayer getSmartPlayer(UUID uuid) {
        return SMART_PLAYERS.get(uuid);
    }

    public String getName() {
        if (this.isOnline()) {
            return this.proxiedPlayer.getName();
        }

        if (this.previousUsernames.isEmpty() || Miscellaneous.getLastEntry(this.previousUsernames).isEmpty()) {
            String name = RelayUtils.getName(this.getUUID());
            if (name != null) {
                this.previousUsernames.add(name);
                return name;
            }

            return "";
        }

        return Miscellaneous.getLastEntry(this.previousUsernames);
    }

    public void updateProfile(ProfileColumn column) {
        SmartDatabase.updateProfile(this.getUUID(), column, this.getData(column));
        this.broadcastUpdateMessage();
    }

    public void broadcastUpdateMessage() {
        if (this.isOnline()) {
            this.sendPluginMessage(Command.UPDATE_INFO, this.getUUID());
        }
    }

    private Object getData(ProfileColumn column) {
        switch (column) {
            case ID:
                return this.getUUID();
            case FIRST_LOGIN:
                return this.firstJoinTime;
            case LAST_LOGIN:
                return this.joinTime;
            case PREVIOUS_USERNAMES:
                return toCSV(this.previousUsernames);
            case GROUPS:
                return toCSV(this.groups);
            case FRIENDS:
                return toCSV(this.friends);
            case BLOCKED_PLAYERS:
                return toCSV(this.blockedPlayers);
            case CHAT_FILTER:
                return this.chatFilter.toString();
            case MESSAGE_FILTER:
                return this.messageFilter.toString();
            case FRIEND_FILTER:
                return this.friendFilter.toString();
            case PARTY_FILTER:
                return this.partyFilter.toString();
            case DUEL_FILTER:
                return this.duelFilter.toString();
            case CREDITS:
                return this.credits;
            case IS_NICKED:
                return this.isNicked ? "1" : "0";
            case NICK:
                return this.nick == null ? "" : this.nick.toString();
            case COSMETICS:
                return toCSV(this.cosmetics);
            case ACTIVE_COSMETICS:
                return toCSV(this.activeCosmetics);
            case GAME_COSMETICS:
                return GameCosmetic.getJsonArray(this.gameCosmetics);
            case ACTIVE_GAME_COSMETICS:
                return GameCosmetic.getJsonArray(this.activeGameCosmetics);
            case PUNISHMENTS:
                return Text.escapeSpecialChars(Punishment.getJsonArray(this.punishments).toString());
            case EXPERIENCE:
                return this.xp;
            case TIME_PLAYED:
                return this.timePlayed;
            case EXTRA:
                return Text.escapeSpecialChars(this.extraData.getData().toString());
            case DISCORD_ID:
                return this.discordId;
        }

        return null;
    }

    public static synchronized void playerConnectionChange(SmartPlayer smartPlayer, boolean logOn) {
        Main.doAndLogDuration("PlayerConnectionChange", () -> {
            if (logOn) {
                ONLINE_PLAYERS.add(smartPlayer);
            } else {
                ONLINE_PLAYERS.remove(smartPlayer);
            }
            ComponentBuilder message = new ComponentBuilder(ComponentBuilder.BLUE).append("Friend > ").append(smartPlayer.getComponentFormattedName()).append("%s %s.", YELLOW, logOn ? "joined" : "left");
            for (SmartPlayer onlineSmartPlayer : getOnlineSmartPlayers()) {
                if (onlineSmartPlayer.friends.contains(smartPlayer.getUUID())) {
                    onlineSmartPlayer.sendMessage(message);
                }
            }
        });
    }

    private void onDisconnect() {
        this.lastDisconnectTime = System.currentTimeMillis();
        this.connectingServer = null;

        playerConnectionChange(this, false);

        if (this.lastServer != null) {
            if (!this.lastServer.isHub() || (this.getServer() != null && !this.getServer().isHub())) {
                this.setReconnectServer(this.lastServer.getHub());
            } else if (this.lastServer.isHub()) {
                this.setReconnectServer(this.lastServer);
            }
            this.extraData.setString("lastServer", this.lastServer.getName());
        }

        Server reconnect = Server.getServer(this.proxiedPlayer.getReconnectServer());

        if (reconnect == null || !reconnect.isHub()) {
            this.setReconnectServer(Server.getServerOfType(null, false));
        }

        if (this.isInQueue()) {
            this.leaveSingular();
        }
        this.hasJoined = false;
        this.updateProfile(ProfileColumn.EXTRA);
        Main.schedule(SmartPlayer::checkLimboQueue, 1);
    }

    private void onJoin(ProxiedPlayer proxiedPlayer) {
        this.proxiedPlayer = proxiedPlayer;
        String name = this.getName();

        this.getData();

        if (Miscellaneous.enoughTimeHasPassed(this.hasReadData ? this.lastDisconnectTime : 0L, 6, TimeUnit.HOURS)) {
            Main.schedule(() -> this.sendPluginMessage(Command.OTHER, "dailyjoin", this.getUUID()), 1);
        }

        Main.scheduleMillis(() -> {
            if (Main.networkIsFull(true)) {
                if (!this.isInGroupOrHigher(Group.CHAMPION)) {
                    Server server = Server.getLimboServer();
                    if (server == null || !server.isMessageClientActive() || server.getPlayerCount() > 50) {
                        ComponentBuilder componentBuilder = new ComponentBuilder();
                        componentBuilder.color(ComponentBuilder.YELLOW);
                        componentBuilder.line().newLine();
                        componentBuilder.color(ComponentBuilder.RED_ORANGE).append("Relay network is currently at maximum capacity.").newLine();
                        componentBuilder.color(ComponentBuilder.ORANGE_YELLOW).append("Please try reconnecting later.");
                        componentBuilder.color(ComponentBuilder.YELLOW).line();
                        this.kick(componentBuilder);
                    } else {
                        this.enterLimboQueue();
                        this.sendToLimbo();
                    }
                } else if (this.getHighestGroup() == Group.CHAMPION) {
                    for (SmartPlayer onlineSmartPlayer : getOnlineSmartPlayers()) {
                        if (onlineSmartPlayer.isInLimboQueue()) {
                            onlineSmartPlayer.sendMessage(new ComponentBuilder(this.getComponentFormattedName()).color(ComponentBuilder.GREEN).append(" has skipped the limbo queue with ").color(ComponentBuilder.AQUA).append("Premium ").color(GREEN).append("rank!"));
                        }
                    }
                }
            }
        }, 750);

        Main.schedule(() -> {
            if (this.lastName == null || !this.lastName.equals(name)) {
                Miscellaneous.addIfAbsent(this.previousUsernames, name);
                this.updateProfile(ProfileColumn.PREVIOUS_USERNAMES);
            }

            this.joinTime = System.currentTimeMillis();

            this.updateProfile(ProfileColumn.LAST_LOGIN);

            if (this.isNewLogin) {
                ComponentBuilder componentBuilder = new ComponentBuilder();
                componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA_BLUE, ComponentBuilder.BLUE).newLine();
                componentBuilder.color(ComponentBuilder.GREEN).append("Welcome to ").color(ComponentBuilder.AQUA_BLUE).bold().append("Relay!").newLine();
                componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA_BLUE, ComponentBuilder.BLUE);
                if (RelayUtils.testing) {
                    this.addToGroup(Group.CHAMPION);
                }
                this.sendMessage(componentBuilder);
                this.isNewLogin = false;
            }

            for (BaseComponent queuedMessage : this.queuedMessages) {
                this.sendMessage(queuedMessage);
            }

            this.queuedMessages.clear();
        }, 2);

        Main.peakPlayers = Math.max(Main.peakPlayers, getOnlineSmartPlayers().size());

        playerConnectionChange(this, true);
    }

    private void enterLimboQueue() {
        Miscellaneous.addIfAbsent(LIMBO_QUEUE, this);

        this.sendLimboQueuePosition(true);
    }

    private void processResultSet(ResultSet resultSet) {
        try {
            if (resultSet.first()) {
                long firstLogin = resultSet.getLong(ProfileColumn.FIRST_LOGIN.getName());
                long lastLogin = resultSet.getLong(ProfileColumn.LAST_LOGIN.getName());
                String previousUsernames = resultSet.getString(ProfileColumn.PREVIOUS_USERNAMES.getName());
                String groups = resultSet.getString(ProfileColumn.GROUPS.getName());
                String friends = resultSet.getString(ProfileColumn.FRIENDS.getName());
                String blockedPlayers = resultSet.getString(ProfileColumn.BLOCKED_PLAYERS.getName());
                int chatFilter = resultSet.getInt(ProfileColumn.CHAT_FILTER.getName());
                int messageFilter = resultSet.getInt(ProfileColumn.MESSAGE_FILTER.getName());
                int friendFilter = resultSet.getInt(ProfileColumn.FRIEND_FILTER.getName());
                int partyFilter = resultSet.getInt(ProfileColumn.PARTY_FILTER.getName());
                int duelFilter = resultSet.getInt(ProfileColumn.DUEL_FILTER.getName());
                long credits = resultSet.getLong(ProfileColumn.CREDITS.getName());
                boolean isNicked = resultSet.getBoolean(ProfileColumn.IS_NICKED.getName());
                String nickedJson = resultSet.getString(ProfileColumn.NICK.getName());
                String cosmetics = resultSet.getString(ProfileColumn.COSMETICS.getName());
                String activeCosmetics = resultSet.getString(ProfileColumn.ACTIVE_COSMETICS.getName());
                String gameCosmetics = resultSet.getString(ProfileColumn.GAME_COSMETICS.getName());
                String activeGameCosmetics = resultSet.getString(ProfileColumn.ACTIVE_GAME_COSMETICS.toString());
                String punishments = resultSet.getString(ProfileColumn.PUNISHMENTS.toString());
                long xp = resultSet.getLong(ProfileColumn.EXPERIENCE.getName());
                long timePlayed = resultSet.getLong(ProfileColumn.TIME_PLAYED.getName());
                String extraData = resultSet.getString(ProfileColumn.EXTRA.getName());
                long discordId = resultSet.getLong(ProfileColumn.DISCORD_ID.getName());

                this.firstJoinTime = firstLogin;
                this.previousLastJoinTime = lastLogin;

                if (RelayUtils.INITIALIZATION_TIME - this.previousLastJoinTime > 0 && this.isOnline()) {
                    Main.uniquePlayers++;
                }

                this.timePlayed = timePlayed;

                this.previousUsernames.clear();
                boolean updatePreviousUsernames = false;
                for (String previousUsername : separateCSV(previousUsernames)) {
                    if (!Miscellaneous.addIfAbsent(this.previousUsernames, previousUsername)) {
                        updatePreviousUsernames = true;
                    }
                }

                if (updatePreviousUsernames) {
                    this.updateProfile(ProfileColumn.PREVIOUS_USERNAMES);
                }

                this.lastName = Miscellaneous.getLastEntry(this.previousUsernames);

                this.groups.clear();
                boolean shouldUpdateGroups = false;
                for (String permission : separateCSV(groups)) {
                    if (!Miscellaneous.addIfAbsent(this.groups, Group.getGroup(permission))) {
                        shouldUpdateGroups = true;
                    }
                }

                if (Miscellaneous.addIfAbsent(this.groups, Group.DEFAULT)) {
                    shouldUpdateGroups = true;
                }

                if (shouldUpdateGroups) {
                    this.updateProfile(ProfileColumn.GROUPS);
                }

                boolean updateFriends = false;
                for (String friend : separateCSV(friends)) {
                    if (!Miscellaneous.addIfAbsent(this.friends, UUID.fromString(friend))) {
                        updateFriends = true;
                    }
                }

                if (updateFriends) {
                    this.updateProfile(ProfileColumn.FRIENDS);
                }

                boolean updateBlocked = false;
                for (String blockedPlayer : separateCSV(blockedPlayers)) {
                    if (!Miscellaneous.addIfAbsent(this.blockedPlayers, UUID.fromString(blockedPlayer))) {
                        updateBlocked = true;
                    }
                }

                if (updateBlocked) {
                    this.updateProfile(ProfileColumn.BLOCKED_PLAYERS);
                }

                this.chatFilter = ChatFilter.getChatFilterLevel(chatFilter);
                this.messageFilter = MessageFilter.getMessageFilter(messageFilter);
                this.friendFilter = FriendFilter.getFriendFilter(friendFilter);
                this.partyFilter = PartyFilter.getPartyFilter(partyFilter);
                this.duelFilter = DuelFilter.getDuelFilter(duelFilter);

                this.credits = credits;

                this.isNicked = isNicked;
                this.nick = Nick.fromString(nickedJson);

                this.cosmetics.clear();
                boolean updateCosmetics = false;
                for (String cosmetic : separateCSV(cosmetics)) {
                    if (!Miscellaneous.addIfAbsent(this.cosmetics, Cosmetic.getCosmetic(cosmetic))) {
                        updateCosmetics = true;
                    }
                }

                if (updateCosmetics) {
                    this.updateProfile(ProfileColumn.COSMETICS);
                }

                this.activeCosmetics.clear();
                boolean updateActiveCosmetics = false;
                for (String activeCosmetic : separateCSV(activeCosmetics)) {
                    if (!Miscellaneous.addIfAbsent(this.activeCosmetics, Cosmetic.getCosmetic(activeCosmetic))) {
                        updateActiveCosmetics = true;
                    }
                }

                if (updateActiveCosmetics) {
                    this.updateProfile(ProfileColumn.ACTIVE_COSMETICS);
                }

                JsonObject gameCosmeticsJson = RelayUtils.parseJsonObject(gameCosmetics);
                if (gameCosmeticsJson != null) {
                    this.gameCosmetics = GameCosmetic.getMap(gameCosmeticsJson);
                }
                JsonObject activeGameCosmeticsJson = RelayUtils.parseJsonObject(activeGameCosmetics);
                if (activeGameCosmeticsJson != null) {
                    this.activeGameCosmetics = GameCosmetic.getMap(activeGameCosmeticsJson);
                }

                JsonArray jsonArray = RelayUtils.parseJsonArray(punishments);
                if (jsonArray != null) {
                    this.punishments = Punishment.getPunishments(jsonArray);
                    this.punishments.removeIf(Objects::isNull);
                }

                this.xp = xp;

                if (this.isOnline()) {
                    for (UUID friend : this.friends) {
                        getOrCreate(friend);
                    }
                }

                for (UUID blockedPlayer : this.blockedPlayers) {
                    getOrCreate(blockedPlayer);
                }

                if (this.firstJoinTime < Main.START_OF_2021 && this.isOnline()) {
                    this.firstJoinTime = System.currentTimeMillis();
                    this.isNewLogin = true;
                    this.updateProfile(ProfileColumn.FIRST_LOGIN);
                    Main.newPlayers++;
                }

                JsonObject jsonObject = RelayUtils.parseJsonObject(extraData);
                if (jsonObject != null) {
                    this.extraData = new ExtraData(jsonObject);
                    this.discordEarned = this.extraData.getInt("discordEarned", this.discordEarned);
                }
                this.discordId = discordId;
                if (!this.hasReadData) {
                    this.manhuntQuits = this.extraData.getInt("manhuntQuits", 0);
                    this.isBannedFromManhunt = this.extraData.getBoolean("manhuntBanned", false);
                    this.checkIfManhuntBanned(false);
                }
            } else {
                String sqlColumns = this.getSQLColumns();
                String sql = this.getSQL();
                SmartDatabase.addToProfileTable(sqlColumns, sql);
                this.isNewLogin = true;
                Main.newPlayers++;
                Main.uniquePlayers++;
            }

            this.onProfileRead();
        } catch (Exception exception) {
            Main.error("Attempted to process result set, error occurred: (%s) %s", this.getFormattedName(), exception.getMessage());
        }
    }

    public void onProfileRead() {
        this.checkIfBanned();
        BotUtils.onPlayerDataRetrieved(this);
        if (this.discordEarned > 0) {
            this.sendDiscordRewardsMessage();
        }
    }

    public void onJoinServer(ServerInfo target) {
        if (!this.hasJoined) {
            Server server = this.lastServer;
            if (server == null) {
                server = Server.getServer(this.extraData.getString("lastServer", "hub001"));
            }
            if (server != null) {
                if (!server.isHub()) {
                    server = server.getHub();
                }
                if (server == null || !server.isActive()) {
                    server = Server.getServerOfType(server == null ? null : server.getMainGame(), false);
                    if (server == null || !server.isActive()) {
                        server = Server.getServerOfType(null, false);
                    }
                }
            }

            Server targetServer = Server.getServer(target);

            if (targetServer != null && targetServer.isLimbo()) {
                Main.synchronous(this::sendToHub);
            } else {
                if (targetServer == null || !targetServer.isHub() || (server != null && targetServer.getMainGame() != server.getMainGame())) {
                    this.setReconnectServer(server);

                    if (server != null && target != server.getServerInfo() && !server.isLimbo()) {
                        Server finalServer = server;
                        Main.synchronous(() -> this.sendToServer(finalServer));
                    }
                }
            }

            Main.schedule(() -> {
                if (this.getServer() != null && !this.getServer().isHub() && !this.isInGame()) {
                    this.sendToHub();
                }
            }, 5);
        }

        this.hasJoined = true;
        this.checkIfBanned();
        Main.schedule(() -> {
            this.sendPartyData();
            this.sendRequeueData();
        }, 1);
        if (this.isInQueue()) {
            for (int i = 0; i < 25; i++) {
                Main.scheduleMillis(() -> {
                    this.hasUpdatedQueueScoreboard = false;
                    this.updateQueueScoreboard();
                }, i * 400);
            }
        }
    }

    private boolean isInGame() {
        for (GameServer gameServer : GameServer.GAME_SERVERS) {
            for (GameDetails gameDetail : gameServer.getServerDetails().gameDetails) {
                if (gameDetail.getPlayers().contains(this.getUUID()) || gameDetail.getSpectators().contains(this.getUUID())) {
                    return true;
                }
            }
        }

        return false;
    }

    public GameServer getGameServer() {
        for (GameServer gameServer : GameServer.GAME_SERVERS) {
            for (GameDetails gameDetail : gameServer.getServerDetails().gameDetails) {
                if (gameDetail.getPlayers().contains(this.getUUID()) || gameDetail.getSpectators().contains(this.getUUID())) {
                    return gameServer;
                }
            }
        }

        return null;
    }

    public void checkIfBanned() {
        if (this.isOnline()) {
            if (this.isBanned()) {
                this.proxiedPlayer.disconnect(this.getBanMessage());
                Punishment ipBan = this.getLatestActivePunishment(PunishmentType.IP_BAN);
                if (ipBan != null) {
                    if (!ipBan.hasExpired() || ipBan.isActive()) {
                        Miscellaneous.addIfAbsent(BANNED_IPS, ipBan.getData());
                    } else {
                        BANNED_IPS.remove(ipBan.getData());
                    }
                }
            } else if (BANNED_IPS.contains(Utilities.parseAddress(this.proxiedPlayer.getSocketAddress()))) {
                this.proxiedPlayer.disconnect(new TextComponent(RED + "This IP is currently banned."));
            }
        }
    }

    public synchronized void updateScoreboard() {
        Main.synchronous(() -> {
            if (this.isOnline()) {
                if (this.isInQueue()) {
                    this.updateQueueScoreboard();
                } else if (this.getServerInfo() == null || this.isOnHub()) {
                    this.sendPluginMessage(Command.SCOREBOARD, this.getUUID());
                }
            }
        });
    }

    public void updateQueueScoreboard() {
        int[] queueData = this.getQueueInformation();
        int queueSize = -1;
        int queuePosition = -1;
        int specificSize = -1;
        int specificPosition = -1;
        int typeSize = -1;
        boolean availableServer = false;

        if (queueData.length == 7) {
            queueSize = queueData[0];
            queuePosition = queueData[1];
            specificSize = queueData[2];
            specificPosition = queueData[3];
            typeSize = queueData[4];
            availableServer = queueData[6] == 1;
        }

        this.sendPartyData();
        if (this.hasUpdatedQueueScoreboard) {
            this.sendPluginMessage(Command.OTHER, "queueposition", this.getUUID(), queuePosition, queueSize, specificPosition, specificSize, typeSize, availableServer);
        } else {
            this.sendQueueScoreboardMessage();
            this.hasUpdatedQueueScoreboard = true;
        }
    }

    public void sendPartyData() {
        this.sendPluginMessage(Command.OTHER, "partydata", this.getUUID(), this.isInParty() ? this.currentParty.getPartyData() : "none");
    }

    public void sendRequeueData() {
        if (this.lastGameType != null) {
            this.sendPluginMessage(Command.OTHER, "requeue", this.getUUID());
        }
    }

    public void sendQueueScoreboardMessage() {
        int[] queueData = this.getQueueInformation();
        int queueSize = -1;
        int queuePosition = -1;
        int specificSize = -1;
        int specificPosition = -1;
        int typeSize = -1;
        boolean fastQueue = false;
        boolean availableServer = false;
        if (queueData.length == 7) {
            queueSize = queueData[0];
            queuePosition = queueData[1];
            specificSize = queueData[2];
            specificPosition = queueData[3];
            typeSize = queueData[4];
            fastQueue = queueData[5] == 1;
            availableServer = queueData[6] == 1;
        }
        this.sendPluginMessage(Command.OTHER, "queuescoreboard", this.getUUID(), this.currentQueue, queuePosition, queueSize, specificPosition, specificSize, typeSize, fastQueue, availableServer, this.queueData);
    }

    public boolean isFastQueue() {
        return this.isInQueue() && Queue.isInFastQueue(this);
    }

    public int getQueuePosition() {
        if (!this.isInQueue()) {
            return -1;
        }

        return Queue.getQueuePosition(this);
    }

    public int getQueueSize() {
        if (!this.isInQueue()) {
            return -1;
        }

        return Queue.getQueueSize(this);
    }

    public int[] getQueueInformation() {
        if (!this.isInQueue()) {
            return new int[0];
        }

        return Queue.getQueueInformation(this);
    }

    public void sendPacket(DefinedPacket definedPacket) {
        this.unsafe().sendPacket(definedPacket);
    }

    public void test() {
        ScoreboardObjective objective = new ScoreboardObjective();
        objective.setName("test");
        objective.setAction((byte) 0);
        objective.setValue("YouTube");
        objective.setType(ScoreboardObjective.HealthDisplay.INTEGER);
        this.sendPacket(objective);

        ScoreboardDisplay display = new ScoreboardDisplay();
        display.setPosition((byte) 1);
        display.setName("test");
        this.sendPacket(display);

        ScoreboardScore score = new ScoreboardScore();
        score.setItemName("Random");
        score.setValue(1);
        score.setScoreName("test");
        score.setAction((byte) 0);
        this.sendPacket(score);
    }

    public Connection.Unsafe unsafe() {
        return this.proxiedPlayer.unsafe();
    }

    public void setQueue(GameType newQueue) {
        this.currentQueue = newQueue;
        this.lastGameType = newQueue;
        this.hasUpdatedQueueScoreboard = false;
    }

    public void emptyQueue() {
        this.currentQueue = null;
        this.updateScoreboard();
    }

    public boolean isInQueue() {
        return this.currentQueue != null;
    }

    public boolean isInQueue(GameType gameType) {
        return this.currentQueue == gameType;
    }

    public boolean isInParty() {
        return this.currentParty != null;
    }

    public boolean isPartyOwner() {
        return this.currentParty.getOwner().equals(this);
    }

    public void createParty() {
        this.currentParty = new Party(this);
        if (this.isInQueue()) {
            this.leaveQueue();
        }
    }

    public boolean isOnline() {
        return this.proxiedPlayer != null && this.proxiedPlayer.isConnected();
    }

    public boolean isInParty(Party party) {
        return party.equals(this.currentParty);
    }

    public Party getCurrentParty() {
        return this.currentParty;
    }

    public void setLastGame(Server lastGameServer, GameType gameType) {
        this.lastGameServer = lastGameServer;
        this.lastGameType = gameType;
    }

    public Server getLastGameServer() {
        return this.lastGameServer;
    }

    public void tryRejoin() {
        if (this.lastGameServer.isMessageClientActive()) {
            this.lastGameServer.sendMessage(Command.REJOIN, this.getUUID());
        } else {
            this.sendErrorMessage("The game you are trying to rejoin has ended.");
        }
    }

    public SmartPlayer getLastMessageSender() {
        return this.lastMessageSender;
    }

    public void setLastMessageSender(SmartPlayer lastMessageSender) {
        this.lastMessageSender = lastMessageSender;
    }

    public void leaveQueue() {
        if (this.isInParty()) {
            if (this.currentParty.canStartGame(this)) {
                Queue.removePartyFromQueue(this.currentQueue, this, this.currentParty);
            } else {
                this.sendPartyErrorMessage("Only party leaders can leave the queue!");
            }
        } else {
            if (this.currentQueue == null) {
                Main.error("Attempted to leave queue, is null. (%s)", this.getFormattedName());
                return;
            }
            Queue.removePlayerFromQueue(this.currentQueue, this);
        }
    }

    public void leaveSingular() {
        Queue.removePlayerFromQueue(this.currentQueue, this);
    }

    public void leaveQueue(GameType gameType) {
        if (this.isInQueue(gameType)) {
            this.leaveQueue();
        } else {
            this.sendErrorMessage("You are not in that queue!");
        }
    }

    public void messagePlayer(SmartPlayer messageRecipient, String message) {
        if (this.canMessagePlayer(messageRecipient)) {
            if (SwearUtils.canSendMessageAtLevel(message, messageRecipient.chatFilter)) {
                ComponentBuilder toStart = new ComponentBuilder(BLUE.toString()).append("To ").append(messageRecipient.getComponentFormattedName()).color(BLUE).append(" > ").color(WHITE);
                this.sendChatMessage(toStart, this, message);
                ComponentBuilder fromStart = new ComponentBuilder(RED.toString()).append("From ").append(this.getComponentFormattedName()).color(RED).append(" > ").color(WHITE);
                messageRecipient.sendChatMessage(fromStart, this, message);
                messageRecipient.setLastMessageSender(this);
                this.onSendMessage(message, ChatType.PRIVATE, messageRecipient);
            } else {
                this.sendErrorMessage("%s%s chat filter level does not allow your message.", messageRecipient.getPossessiveFormattedName(), RED);
            }
        } else {
            this.sendErrorMessage("You cannot message that player!");
        }
    }

    private boolean canMessagePlayer(SmartPlayer messageRecipient) {
        if (messageRecipient.blockedPlayers.contains(this.getUUID())) {
            return false;
        } else if (this.blockedPlayers.contains(messageRecipient.getUUID())) {
            return false;
        } else if (messageRecipient.messageFilter == MessageFilter.FRIENDS || messageRecipient.messageFilter == MessageFilter.FRIENDS_OF_FRIENDS) {
            if (messageRecipient.friends.contains(this.getUUID())) {
                return true;
            }
            if (!this.isMuted()) {
                if (messageRecipient.messageFilter == MessageFilter.FRIENDS_OF_FRIENDS) {
                    for (UUID friend : this.friends) {
                        if (messageRecipient.friends.contains(friend)) {
                            return true;
                        }
                    }
                }
            }
        } else if (messageRecipient.messageFilter == MessageFilter.ANYBODY && !this.isMuted()) {
            return true;
        }

        if (this.isStaff()) {
            return true;
        }

        return false;
    }

    public void sendMessage(ComponentBuilder componentBuilder) {
        this.sendMessage(componentBuilder.getResult());
    }

    public void sendMessage(boolean queueIfOffline, BaseComponent baseComponent) {
        if (this.isOnline()) {
            this.proxiedPlayer.sendMessage(baseComponent);
        } else if (queueIfOffline) {
            this.queuedMessages.add(baseComponent);
        }
    }

    public void sendMessage(BaseComponent baseComponent) {
        this.sendMessage(false, baseComponent);
    }

    public void sendMessage(boolean queueIfOffline, String format, Object... arguments) {
        if (this.isOnline()) {
            this.sendMessage(new TextComponent(RelayUtils.format(format, arguments)));
        } else if (queueIfOffline) {
            this.queueMessageForJoin(format, arguments);
        }
    }

    public void sendMessage(String format, Object... arguments) {
        this.sendMessage(false, format, arguments);
    }

    public void sendInfoMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(YELLOW.getColor()).append(format, arguments));
    }

    public void sendErrorMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(RED.getColor()).append(format, arguments));
    }

    public void sendSuccessMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(GREEN.getColor()).append(format, arguments));
    }

    public void sendUsageMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(DARK_GREEN.getColor()).append(format, arguments));
    }

    public void sendPartyInfoMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(ComponentBuilder.RED).append("Party > ").color(YELLOW).append(format, arguments));
    }

    public void sendPartyErrorMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(ComponentBuilder.RED).append("Party > ").color(RED).append(format, arguments));
    }

    public void sendPartySuccessMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(ComponentBuilder.RED).append("Party > ").color(GREEN).append(format, arguments));
    }

    public void sendPartyUsageMessage(String format, Object... arguments) {
        this.sendMessage(new ComponentBuilder(ComponentBuilder.RED).append("Party > ").color(DARK_GREEN).append(format, arguments));
    }

    public void queueMessageForJoin(String format, Object... data) {
        this.queueMessageForJoin(new TextComponent(RelayUtils.format(format, data)));
    }

    public void queueMessageForJoin(BaseComponent baseComponent) {
        this.queuedMessages.add(baseComponent);
    }

    public void queueInfoMessageForJoin(String format, Object... data) {
        this.queueMessageForJoin(YELLOW + format, data);
    }

    public void queueSuccessMessageForJoin(String format, Object... data) {
        this.queueMessageForJoin(GREEN + format, data);
    }

    public void queueUsageMessageForJoin(String format, Object... data) {
        this.queueMessageForJoin(DARK_GREEN + format, data);
    }

    public void queueErrorMessageForJoin(String format, Object... data) {
        this.queueMessageForJoin(RED + format, data);
    }

    public static void onTick() {
        if (Main.isSecond()) {
            checkLimboQueue();
            if (Main.isOfSeconds(5)) {
                if (Main.isOfSeconds(60)) {
                    for (SmartPlayer smartPlayer : getOnlineSmartPlayers()) {
                        smartPlayer.checkForUpdates();
                    }
                }

                for (SmartPlayer smartPlayer : LIMBO_QUEUE) {
                    smartPlayer.sendLimboQueuePosition(false);
                }
            }
        }
    }

    public static void checkLimboQueue() {
        if (!Main.networkIsFull()) {
            if (!LIMBO_QUEUE.isEmpty()) {
                SmartPlayer smartPlayer = LIMBO_QUEUE.get(0);
                LIMBO_QUEUE.remove(0);
                smartPlayer.sendToHub();
            }
        }
    }

    public static SmartPlayer getSmartPlayer(ProxiedPlayer proxiedPlayer) {
        if (proxiedPlayer == null) return null;

        SmartPlayer smartPlayer = getSmartPlayer(proxiedPlayer.getUniqueId());

        if (smartPlayer == null) {
            return new SmartPlayer(proxiedPlayer);
        }

        return smartPlayer;
    }

    public static SmartPlayer getSmartPlayer(CommandSender commandSender) {
        if (commandSender instanceof ProxiedPlayer) {
            return getSmartPlayer((ProxiedPlayer) commandSender);
        }

        return null;
    }

    public static boolean isPlayer(CommandSender commandSender) {
        return getSmartPlayer(commandSender) != null;
    }

    public static SmartPlayer getSmartPlayer(String name) {
        if (name.length() > 16) {
            try {
                return getSmartPlayer(UUID.fromString(name));
            } catch (Exception exception) {
                Main.error("Attempted to parse UUID to get smartplayer: %s", name);
                return null;
            }
        }

        for (SmartPlayer smartPlayer : getSmartPlayers()) {
            if (smartPlayer.getName().equalsIgnoreCase(name)) {
                return smartPlayer;
            }
        }

        return null;
    }

    public static SmartPlayer getSmartPlayerOrNick(String name) {
        SmartPlayer smartPlayer = getSmartPlayer(name);

        if (smartPlayer == null) {
            return getFromNickname(name);
        }

        return smartPlayer;
    }

    public static SmartPlayer getOrCreate(String name) {
        if (name.length() > 16) {
            try {
                UUID uuid = UUID.fromString(name);
                SmartPlayer smartPlayer = getSmartPlayer(uuid);
                if (smartPlayer == null) {
                    return new SmartPlayer(uuid);
                } else {
                    return smartPlayer;
                }
            } catch (Exception ignored) {
            }
        } else {
            SmartPlayer smartPlayer = getSmartPlayerOrNick(name);

            if (smartPlayer == null) {
                UUID uuid = RelayUtils.getUUID(name);
                if (uuid != null) {
                    return new SmartPlayer(uuid);
                }
            }

            return smartPlayer;
        }

        return null;
    }

    public static SmartPlayer getOrCreate(UUID uuid) {
        if (uuid == null) {
            return null;
        }

        return getOrCreate(String.valueOf(uuid));
    }

    public static Collection<SmartPlayer> getSmartPlayers() {
        return Miscellaneous.getList(SMART_PLAYERS.values());
    }

    public static Collection<SmartPlayer> getOnlineSmartPlayers() {
        Collection<SmartPlayer> smartPlayers = getSmartPlayers();

        smartPlayers.removeIf(smartPlayer -> !smartPlayer.isOnline());

        return smartPlayers;
    }

    public static Collection<UUID> getUUIDs() {
        return Miscellaneous.getList(SMART_PLAYERS.keySet());
    }

    public static void onPlayerJoin(PostLoginEvent event) {
        getSmartPlayer(event.getPlayer()).onJoin(event.getPlayer());
    }

    public void sendChatMessage(String start, SmartPlayer sender, String message) {
        if (!this.blockedPlayers.contains(sender.getUUID())) {
            String processed = this.processChatMessage(message);
            if (!processed.isEmpty()) {
                if (processed.toLowerCase().contains(this.getName().toLowerCase()) || (this.isNicked && processed.toLowerCase().contains(this.getNickname().toLowerCase()))) {
                    this.playSound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP);
                }
                this.sendMessage(start + processed);
            }
        }
    }

    public void sendChatMessage(ComponentBuilder start, SmartPlayer sender, String message) {
        if (!this.blockedPlayers.contains(sender.getUUID())) {
            String processed = this.processChatMessage(message);
            if (!processed.isEmpty()) {
                if (processed.toLowerCase().contains(this.getName().toLowerCase()) || (this.isNicked && processed.toLowerCase().contains(this.getNickname().toLowerCase()))) {
                    this.playSound(Sound.ENTITY_EXPERIENCE_ORB_PICKUP);
                }
                this.sendMessage(start.clone().append(processed));
            }
        }
    }

    public String processChatMessage(String message) {
        String processed = SwearUtils.processChatMessage(message, this.chatFilter);
        String patternString = "(?i)(" + this.getName() + ")";
        if (this.isNicked) {
            patternString += "|(" + this.getNickname() + ")";
        }
        Pattern pattern = Pattern.compile(patternString);
        Matcher matcher = pattern.matcher(processed);
        while (matcher.find()) {
            String group = matcher.group();
            processed = processed.replace(group, YELLOW + group + RESET);
        }

        return processed;
    }

    public boolean canSendMessage(String message) {
        if (this.isMuted()) {
            this.sendErrorMessage("You are currently muted. Your mute expires at %s%s%s.", YELLOW, this.getExpiryDate(this.getLatestActivePunishment(PunishmentType.MUTE)), RED);
            return false;
        }

        if (this.isStaff()) {
            return true;
        }

        boolean canSendChatMessage = SwearUtils.canSendMessageAtLevel(message, this.chatFilter) && !PlayerUtilities.containsWebsiteOrData(message);

        if (!canSendChatMessage) {
            this.sendErrorMessage("Your message, '%s%s%s' is not suitable for chat.", YELLOW, message, RED);
            return false;
        }

        if (message.replace("\\s", "").isEmpty()) {
            this.sendErrorMessage("Please enter a message to send.");
            return false;
        }


        if (!this.lastMessagesMatch(message)) {
            if (!(this.latestMessagesWithin3Seconds.size() >= 5)) {
                this.removeLastMessage();
                long time = System.currentTimeMillis();
                this.lastMessages.put(time, message);
                Main.schedule(() -> this.lastMessages.remove(time), 5, TimeUnit.MINUTES);
                this.latestMessagesWithin3Seconds.add(message);
                Main.schedule(() -> this.latestMessagesWithin3Seconds.remove(message), 3);
                return true;
            } else {
                this.sendErrorMessage("You are sending messages too fast!");
            }
        } else {
            this.sendErrorMessage("You cannot send the same message twice!");
        }

        return false;
    }

    public void onSendMessage(String message, ChatType chatType) {
        this.onSendMessage(message, chatType, null);
    }

    public void onSendMessage(String message, ChatType chatType, SmartPlayer recipient) {
        ChatMessage chatMessage;
        if (recipient != null) {
            chatMessage = new PrivateChatMessage(message, ChatType.PRIVATE, recipient);
        } else {
            chatMessage = new ChatMessage(message, chatType);
        }
        this.messages.add(chatMessage);
    }

    private void removeLastMessage() {
        if (this.lastMessages.size() >= 5) {
            this.lastMessages.remove(Miscellaneous.getList(this.lastMessages.keySet()).get(this.lastMessages.size() - 1));
        }
    }

    private boolean lastMessagesMatch(String message) {
        if (true) {
            return false;
        }

        for (Map.Entry<Long, String> entry : this.lastMessages.entrySet()) {
            String previousMessage = entry.getValue();
            if ((Miscellaneous.isWithinRange(message.length(), previousMessage.length(), 2) && previousMessage.toLowerCase().contains(message.toLowerCase())) || previousMessage.equalsIgnoreCase(message)) {
                if (!Miscellaneous.enoughSecondsHavePassed(entry.getKey(), 3)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean canBeInvitedToParty(SmartPlayer inviter) {
        return !(this.eitherAreBlocked(inviter) || !this.canReceivePartyInvite(inviter));
    }

    public boolean inviteToParty(SmartPlayer inviter, boolean isOwner, Party party) {
        if (this.eitherAreBlocked(inviter) || !this.canReceivePartyInvite(inviter) || this.invitedParties.contains(party)) {
            return false;
        } else {
            this.invitedParties.add(party);
            ComponentBuilder componentBuilder = new ComponentBuilder();
            componentBuilder.gradientLine(ComponentBuilder.RED, ComponentBuilder.RED_ORANGE, ComponentBuilder.RED).newLine();
            componentBuilder.color(GREEN).append(inviter.getComponentFormattedName()).append(GREEN).append(" has invited you to join %s %sparty! ", isOwner ? "their" : party.getOwner().getPossessiveFormattedName(), GREEN);
            componentBuilder.command(GOLD + "Click here.", "/party join " + inviter.getName(), RelayUtils.format("%sClick here to join %s%s party.", GREEN, inviter.getPossessiveFormattedName(), GREEN));
            componentBuilder.color(YELLOW).append(" You have 60 seconds to accept.").newLine();
            componentBuilder.gradientLine(ComponentBuilder.RED, ComponentBuilder.RED_ORANGE, ComponentBuilder.RED);
            BaseComponent textComponent = componentBuilder.getResult();
            ClickEvent clickEvent = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/party join " + inviter.getName());
            textComponent.setClickEvent(clickEvent);
            String hover = RelayUtils.format("%sClick here to join %s%s party.", GREEN, inviter.getPossessiveFormattedName(), GREEN);
            List<String> lines = Miscellaneous.getList(hover.split("\n"));
            HoverEvent hoverEvent = new HoverEvent(net.md_5.bungee.api.chat.HoverEvent.Action.SHOW_TEXT, Miscellaneous.processList(lines, net.md_5.bungee.api.chat.hover.content.Text::new));
            textComponent.setHoverEvent(hoverEvent);
            this.sendMessage(textComponent);
            return true;
        }
    }

    public void sendMessage(BaseComponent... baseComponents) {
        this.proxiedPlayer.sendMessage(baseComponents);
    }

    public boolean canReceivePartyInvite(SmartPlayer smartPlayer) {
        switch (this.partyFilter) {
            case ANYBODY:
                return true;
            case FRIENDS:
                return this.friends.contains(smartPlayer.getUUID());
            case FRIENDS_OF_FRIENDS:
                return this.hasMutualFriends(smartPlayer);
        }

        return !this.isInLimboQueue();
    }

    public void invitePlayerToDuel(SmartPlayer invited, GameType duelType) {
        if (invited.inviteToDuel(this, duelType)) {
            this.sendSuccessMessage("Invited %s%s to a %s%s%s. They have 60 seconds to accept.", invited.getFormattedNameOrNick(), GREEN, GOLD, duelType.getFullPrettyName(), GREEN);
        } else {
            this.sendErrorMessage("You cannot invite that player to a duel!");
        }
    }

    public boolean inviteToDuel(SmartPlayer inviter, GameType duelType) {
        if (this.invitedDuels.containsKey(inviter) || this.eitherAreBlocked(inviter) || !this.canReceiveDuel(inviter)) {
            return false;
        }
        this.invitedDuels.put(inviter, duelType);
        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.gradientLine(ComponentBuilder.GREEN, ComponentBuilder.YELLOW_GREEN, ComponentBuilder.GREEN).newLine();
        componentBuilder.color(GREEN).append(inviter.getComponentFormattedName()).append("%s has invited you to %s%s%s! ", GREEN, GOLD, duelType.getFullPrettyName(), GREEN);
        componentBuilder.command(GOLD + "Click here.", "/duelaccept " + inviter.getName(), RelayUtils.format("%sClick here to accept %s%s duel request.", AQUA, inviter.getPossessiveFormattedName(), AQUA));
        componentBuilder.color(YELLOW).append(" You have 60 seconds to accept.").newLine();
        componentBuilder.gradientLine(ComponentBuilder.GREEN, ComponentBuilder.YELLOW_GREEN, ComponentBuilder.GREEN);
        BaseComponent textComponent = componentBuilder.getResult();
        ClickEvent clickEvent = new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/duelaccept " + inviter.getName());
        textComponent.setClickEvent(clickEvent);
        String hover = RelayUtils.format("%sClick here to accept %s%s duel request.", AQUA, inviter.getPossessiveFormattedName(), AQUA);
        List<String> lines = Miscellaneous.getList(hover.split("\n"));
        HoverEvent hoverEvent = new HoverEvent(net.md_5.bungee.api.chat.HoverEvent.Action.SHOW_TEXT, Miscellaneous.processList(lines, net.md_5.bungee.api.chat.hover.content.Text::new));
        textComponent.setHoverEvent(hoverEvent);
        this.sendMessage(textComponent);
        Main.schedule(() -> {
            if (this.invitedDuels.containsKey(this)) {
                this.invitedDuels.remove(this);
                inviter.sendInfoMessage("The duels invite to %s%s has expired.", this.getFormattedNameOrNick(), YELLOW);
                this.sendInfoMessage("The duels invite from %s%s has expired.", inviter.getFormattedName(), YELLOW);
            }
        }, 60);
        return true;
    }

    public boolean canReceiveDuel(SmartPlayer smartPlayer) {
        switch (this.duelFilter) {
            case ANYBODY:
                return true;
            case FRIENDS:
                return this.friends.contains(smartPlayer.getUUID());
            case FRIENDS_OF_FRIENDS:
                return this.hasMutualFriends(smartPlayer);
        }

        return false;
    }

    public void invitePlayerToManhunt(SmartPlayer invited) {
        if (invited.inviteToManhunt(this)) {
            this.sendSuccessMessage("Invited %s%s to a %s%s%s. They have 60 seconds to accept.", invited.getFormattedName(), GREEN, GOLD, GameType.MANHUNT_1V1.getFullPrettyName(), GREEN);
        } else {
            this.sendErrorMessage("You cannot invite that player to a duel!");
        }
    }

    public boolean inviteToManhunt(SmartPlayer inviter) {
        if (this.invitedDuels.containsKey(inviter) || this.eitherAreBlocked(inviter) || !this.canReceiveDuel(inviter)) {
            return false;
        }
        this.invitedDuels.put(inviter, GameType.MANHUNT_1V1);
        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.gradientLine(ComponentBuilder.GREEN, ComponentBuilder.YELLOW_GREEN, ComponentBuilder.GREEN).newLine();
        componentBuilder.color(GREEN).append(inviter.getComponentFormattedName()).append("%s has invited you to %s%s%s! ", GREEN, GOLD, GameType.MANHUNT_1V1.getFullPrettyName(), GREEN);
        componentBuilder.command(GOLD + "Click here.", "/manhuntaccept " + inviter.getName(), RelayUtils.format("%sClick here to accept %s%s manhunt request.", AQUA, inviter.getPossessiveFormattedName(), AQUA));
        componentBuilder.color(YELLOW).append(" You have 60 seconds to accept.").newLine();
        componentBuilder.gradientLine(ComponentBuilder.GREEN, ComponentBuilder.YELLOW_GREEN, ComponentBuilder.GREEN);
        this.sendMessage(componentBuilder);
        Main.schedule(() -> {
            if (this.invitedDuels.containsKey(this)) {
                this.invitedDuels.remove(this);
                inviter.sendInfoMessage("The manhunt invite to %s%s has expired.", this.getFormattedName(), YELLOW);
                this.sendInfoMessage("The manhunt invite from %s%s has expired.", inviter.getFormattedName(), YELLOW);
            }
        }, 60);
        return true;
    }

    public boolean hasMutualFriends(SmartPlayer smartPlayer) {
        if (this.friends.contains(smartPlayer.getUUID())) return true;

        for (UUID friend : this.friends) {
            if (smartPlayer.friends.contains(friend)) return true;
        }

        return false;
    }

    public boolean eitherAreBlocked(SmartPlayer other) {
        return this.blockedPlayers.contains(other.getUUID()) || other.blockedPlayers.contains(this.getUUID());
    }

    public void tryAcceptDuelRequest(SmartPlayer toAccept) {
        if (this.invitedDuels.containsKey(toAccept)) {
            if (!toAccept.isOnline()) {
                this.invitedDuels.remove(toAccept);
                this.sendErrorMessage("That player is not online!");
            } else {
                GameType gameType = this.invitedDuels.remove(toAccept);
                DuelAcceptCommand.doDuel(toAccept, this, gameType);
            }
        } else {
            this.sendErrorMessage("That player has not invited you to a duel!");
        }
    }

    public List<String> getPartiesToJoin() {
        List<String> parties = new ArrayList<>();

        for (Party party : this.invitedParties) {
            parties.add(party.getOwner().getName());
        }

        return parties;
    }

    public void addFriend(String playerName) {
        SmartPlayer smartPlayer = SmartPlayer.getSmartPlayer(playerName);

        if (this.friends.size() == 100) {
            this.sendErrorMessage("You have reached the maximum of 100 friends!");
            return;
        }

        if (smartPlayer == null) {
            if (isNickname(playerName)) {
                this.sendErrorMessage("You cannot send a friend request to that player!");
            } else {
                this.noPlayerFound(playerName);
            }
        } else if (!smartPlayer.isOnline()) {
            this.sendErrorMessage("That player is not online!");
        } else if (smartPlayer == this) {
            this.sendErrorMessage("You cannot friend yourself!");
        } else {
            UUID playerUUID = smartPlayer.getUUID();
            if (this.friends.contains(playerUUID)) {
                this.sendErrorMessage("You already have %s%s friended!", smartPlayer.getFormattedName(), RED);
            } else {
                if (smartPlayer.receiveFriendRequest(this)) {
                    this.sendSuccessMessage("Sent a friend request to %s%s. They have 60 seconds to accept.", smartPlayer.getFormattedName(), GREEN);
                } else {
                    this.sendErrorMessage("You cannot send a friend request to that player!");
                }
            }
        }
    }

    public boolean receiveFriendRequest(SmartPlayer requester) {
        if (this.receivedFriendRequests.contains(requester) || this.eitherAreBlocked(requester) || !this.canReceiveFriendRequest(requester)) {
            return false;
        }

        this.receivedFriendRequests.add(requester);
        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA_BLUE, ComponentBuilder.BLUE).newLine();
        componentBuilder.color(GREEN).append(requester.getFormattedName()).append(GREEN).append(" sent you a friend request! ");
        componentBuilder.command(GOLD + "Click here.", "/friend accept " + requester.getName(), RelayUtils.format("%sClick here to accept %s%s friend request.", GREEN, requester.getPossessiveFormattedName(), GREEN));
        componentBuilder.color(YELLOW).append(" You have 60 seconds to accept.").newLine();
        componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA_BLUE, ComponentBuilder.BLUE);
        this.sendMessage(componentBuilder);
        Main.schedule(() -> {
            if (this.receivedFriendRequests.remove(requester)) {
                requester.sendInfoMessage("The friend request to %s%s has expired.", this.getFormattedName(), YELLOW);
                this.sendInfoMessage("The duels invite from %s%s has expired.", requester.getFormattedName(), YELLOW);
            }
        }, 60);

        return true;
    }

    public boolean canReceiveFriendRequest(SmartPlayer smartPlayer) {
        switch (this.friendFilter) {
            case ANYBODY:
                return true;
            case FRIENDS_OF_FRIENDS:
                return this.hasMutualFriends(smartPlayer);
        }

        return false;
    }

    public void unfriendPlayer(String playerName) {
        SmartPlayer smartPlayer = SmartPlayer.getSmartPlayer(playerName);

        if (smartPlayer == null) {
            if (isNickname(playerName)) {
                this.sendErrorMessage("You do not have that player friended!");
            } else {
                this.noPlayerFound(playerName);
            }
        } else if (smartPlayer == this) {
            this.sendErrorMessage("You cannot unfriend yourself!");
        } else {
            UUID playerUUID = smartPlayer.getUUID();
            if (this.friends.remove(playerUUID)) {
                smartPlayer.friends.remove(this.getUUID());
                smartPlayer.sendInfoMessage("%s%s removed you from their friend list!", this.getFormattedName(), YELLOW);
                this.sendSuccessMessage("Removed %s%s from your friend list.", smartPlayer.getFormattedName(), GREEN);
                smartPlayer.updateProfile(ProfileColumn.FRIENDS);
                this.updateProfile(ProfileColumn.FRIENDS);
            } else {
                this.sendErrorMessage("You do not have that player friended!");
            }
        }
    }

    public boolean hasOnlineFriend() {
        for (UUID friend : this.friends) {
            SmartPlayer smartPlayer = getSmartPlayer(friend);
            if (smartPlayer != null && smartPlayer.isOnline()) {
                return true;
            }
        }

        return false;
    }

    public void listFriends(int page) {
        if (this.friends.isEmpty()) {
            this.sendInfoMessage("Your friends list is empty.");
        } else {
            List<UUID> orderedFriendList = this.getOrderedFriendList();
            if (orderedFriendList.size() - ((page - 1) * 8) > 0) {

                ComponentBuilder componentBuilder = new ComponentBuilder();

                String baseCommand = "/friend list ";

                if (page > 1) {
                    componentBuilder.command(GOLD + "<<", baseCommand + (page - 1), AQUA + "Click here to view the previous page.");
                } else {
                    componentBuilder.append(GOLD + "||");
                }
                componentBuilder.append(" ");

                componentBuilder.gradientLine(PlayerUtilities.HALF_LINE_LENGTH, ComponentBuilder.BLUE, ComponentBuilder.AQUA).append(" %s[%d] ", GOLD, page).gradientLine(PlayerUtilities.HALF_LINE_LENGTH, ComponentBuilder.AQUA, ComponentBuilder.BLUE).append(" ");

                if (orderedFriendList.size() - (page * 8) > 0) {
                    componentBuilder.command(GOLD + ">>", baseCommand + (page + 1), AQUA + "Click here to view the next page.");
                } else {
                    componentBuilder.append(GOLD + "||");
                }
                for (int i = 8 * (page - 1) + 1; i < 8 * page; i++) {
                    int x = orderedFriendList.size() - i;
                    if (x == -1) {
                        break;
                    }
                    componentBuilder.newLine();
                    componentBuilder.append(getPlayerInfo(orderedFriendList.get(x)));
                }

                componentBuilder.newLine();

                componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA, ComponentBuilder.BLUE);

                this.sendMessage(componentBuilder);
            } else {
                this.sendErrorMessage("The page number %s%d%s is out of bounds.", YELLOW, page, RED);
            }
        }
    }

    public static BaseComponent getPlayerInfo(UUID uuid) {
        SmartPlayer smartPlayer = getOrCreate(uuid);

        if (smartPlayer == null) {
            Main.error("Could not find smart player for player info: %s", uuid);
            return new ComponentBuilder().getResult();
        }

        return smartPlayer.getFriendListInfo();
    }

    public BaseComponent getFriendListInfo() {
        if (this.isOnline()) {
            return this.getLocationForInfo();
        }

        return new ComponentBuilder(this.getComponentFormattedNameColor()).append(this.getName()).color(RED).append(" is offline").getResult();
    }

    public List<UUID> getOrderedFriendList() {
        List<UUID> offline = new ArrayList<>(this.friends);
        List<UUID> online = new ArrayList<>();

        for (UUID uuid : offline) {
            SmartPlayer smartPlayer = getSmartPlayer(uuid);
            if (smartPlayer != null && smartPlayer.isOnline()) {
                online.add(uuid);
            }
        }

        offline.removeAll(online);

        List<UUID> result = sortByValue(PlayerUtilities.getUUIDToNameMapFromUUIDs(offline));

        result.addAll(sortByValue(PlayerUtilities.getUUIDToNameMapFromUUIDs(online)));

        return result;
    }

    private static <K, V> List<K> sortByValue(Map<K, V> unsortMap) {
        List<Map.Entry<K, V>> list = new LinkedList<>(unsortMap.entrySet());
        Collections.sort(list, new Comparator<Object>() {
            @SuppressWarnings("unchecked")
            public int compare(Object o1, Object o2) {

                boolean o1Null = o1 == null || ((Map.Entry<K, V>) o1).getValue() == null;
                boolean o2Null = o2 == null || ((Map.Entry<K, V>) o2).getValue() == null;

                if (o1Null && o2Null) return 0;

                if (o1Null) return -1;

                if (o2Null) return 1;

                V a = ((Map.Entry<K, V>) o1).getValue();

                V b = ((Map.Entry<K, V>) o2).getValue();

                return ((Comparable<V>) a).compareTo(b);
            }
        });

        Map<K, V> result = new LinkedHashMap<>();
        for (Map.Entry<K, V> entry : list) {
            result.put(entry.getKey(), entry.getValue());
        }

        return new ArrayList<>(result.keySet());
    }

    public void tryAcceptFriendRequest(String playerName) {
        SmartPlayer smartPlayer = SmartPlayer.getSmartPlayer(playerName);

        if (smartPlayer == null) {
            this.noPlayerFound(playerName);
        } else {
            if (this.receivedFriendRequests.remove(smartPlayer)) {
                this.acceptFriend(smartPlayer);
                smartPlayer.acceptFriend(this);
            } else {
                this.sendErrorMessage("That player has not sent you a friend request!");
            }
        }
    }

    public void acceptFriend(SmartPlayer smartPlayer) {
        this.friends.add(smartPlayer.getUUID());
        this.sendSuccessMessage("You are now friends with %s%s.", smartPlayer.getFormattedName(), GREEN);
        this.updateProfile(ProfileColumn.FRIENDS);
    }

    public void blockPlayer(String playerName) {
        SmartPlayer smartPlayer = SmartPlayer.getSmartPlayer(playerName);

        if (this.blockedPlayers.size() == 100) {
            this.sendErrorMessage("You have reached the maximum of 100 blocked players!");
            return;
        }

        if (smartPlayer == null) {
            if (isNickname(playerName)) {
                smartPlayer = getFromNickname(playerName);
                UUID playerUUID = smartPlayer.getUUID();
                if (this.blockedPlayers.contains(playerUUID)) {
                    this.sendErrorMessage("You already have already blocked %s%s.", smartPlayer.getFormattedNickname(), RED);
                } else {
                    this.blockedPlayers.add(playerUUID);
                    this.sendSuccessMessage("Blocked %s%s.", smartPlayer.getFormattedNickname(), GREEN);
                    this.updateProfile(ProfileColumn.BLOCKED_PLAYERS);
                }
            } else {
                this.noPlayerFound(playerName);
            }
        } else if (smartPlayer == this) {
            this.sendErrorMessage("You cannot block yourself!");
        } else {
            UUID playerUUID = smartPlayer.getUUID();
            if (this.blockedPlayers.contains(playerUUID)) {
                this.sendErrorMessage("You already have already blocked %s%s.", smartPlayer.getFormattedName(), RED);
            } else {
                this.blockedPlayers.add(playerUUID);
                this.sendSuccessMessage("Blocked %s%s.", smartPlayer.getFormattedName(), GREEN);
                this.updateProfile(ProfileColumn.BLOCKED_PLAYERS);
            }
        }
    }

    public void unblockPlayer(String playerName) {
        SmartPlayer smartPlayer = SmartPlayer.getSmartPlayer(playerName);

        if (smartPlayer == null) {
            if (isNickname(playerName)) {
                this.sendErrorMessage("You do not have %s%s blocked.", getActualNickname(playerName), RED);
            } else {
                this.noPlayerFound(playerName);
            }
        } else if (smartPlayer == this) {
            smartPlayer.sendErrorMessage("You cannot unblock yourself!");
        } else {
            UUID playerUUID = smartPlayer.getUUID();
            if (this.blockedPlayers.remove(playerUUID)) {
                this.sendSuccessMessage("Unblocked %s%s.", smartPlayer.getFormattedName(), GREEN);
                this.updateProfile(ProfileColumn.BLOCKED_PLAYERS);
            } else {
                this.sendErrorMessage("You do not have %s%s blocked.", smartPlayer.getFormattedName(), RED);
            }
        }
    }

    public void listBlockedPlayers(int page) {
        if (this.blockedPlayers.isEmpty()) {
            this.sendInfoMessage("Your block list is empty.");
        } else {
            List<UUID> orderedUUIDs = new ArrayList<>(sortByValue(PlayerUtilities.getUUIDToNameMapFromUUIDs(this.blockedPlayers)));
            if (orderedUUIDs.size() - ((page - 1) * 8) > 0) {
                ComponentBuilder componentBuilder = new ComponentBuilder();

                String baseCommand = "/block list ";

                if (page > 1) {
                    componentBuilder.command(GOLD + "<<", baseCommand + (page - 1), AQUA + "Click here to view the previous page.");
                } else {
                    componentBuilder.append(GOLD + "||");
                }
                componentBuilder.append(" ");

                componentBuilder.gradientLine(PlayerUtilities.HALF_LINE_LENGTH, ComponentBuilder.RED, ComponentBuilder.ORANGE).append(" %s[%d] ", GOLD, page).gradientLine(PlayerUtilities.HALF_LINE_LENGTH, ComponentBuilder.ORANGE, ComponentBuilder.RED).append(" ");

                if (orderedUUIDs.size() - (page * 8) > 0) {
                    componentBuilder.command(GOLD + ">>", baseCommand + (page + 1), AQUA + "Click here to view the next page.");
                } else {
                    componentBuilder.append(GOLD + "||");
                }

                for (int i = 8 * (page - 1) + 1; i < 8 * page; i++) {
                    int x = orderedUUIDs.size() - i;
                    if (x == -1) {
                        break;
                    }
                    componentBuilder.newLine().color(YELLOW);
                    componentBuilder.append(getOrCreate(orderedUUIDs.get(x)).getName());
                }

                componentBuilder.newLine();

                componentBuilder.gradientLine(ComponentBuilder.RED, ComponentBuilder.ORANGE, ComponentBuilder.RED);

                this.sendMessage(componentBuilder);
            } else {
                this.sendErrorMessage("The page number %s%d%s is out of bounds.", YELLOW, page, RED);
            }
        }
    }

    public ProxiedPlayer getProxiedPlayer() {
        return this.proxiedPlayer;
    }

    public boolean setChatType(ChatType chatType) {
        switch (chatType) {
            case ALL:
                if (this.chatType == ChatType.ALL) {
                    this.sendErrorMessage("You are already using all chat!");
                } else {
                    this.chatType = ChatType.ALL;
                    this.sendSuccessMessage("You are now using all chat.");
                }
                break;
            case PARTY:
                if (this.chatType == ChatType.PARTY) {
                    this.sendErrorMessage("You are already using party chat!");
                } else {
                    if (this.isInParty()) {
                        this.chatType = ChatType.PARTY;
                        this.sendSuccessMessage("You are now using party chat.");
                    } else {
                        this.sendErrorMessage("You are not in a party!");
                    }
                }
                break;
            case STAFF:
                if (this.isStaff()) {
                    if (this.chatType == ChatType.STAFF) {
                        this.sendErrorMessage("You are already using staff chat!");
                    } else {
                        this.chatType = ChatType.STAFF;
                        this.sendSuccessMessage("You are now using staff chat.");
                    }
                } else {
                    return false;
                }
                break;
        }
        return true;
    }

    public boolean cannotSendCommand() {
        if (this.isStaff()) return false;
        if (this.commandsInPast5Seconds <= 3) {
            this.commandsInPast5Seconds++;
            Main.schedule(() -> this.commandsInPast5Seconds--, 5);
            return false;
        }
        return true;
    }

    public String getFormattedName() {
        Group highest = this.getHighestGroup();

        if (highest == null) {
            return Group.DEFAULT.formatName(this.getName(), this.getChampionTier());
        }

        return highest.formatName(this.getName(), this.getChampionTier());
    }

    public BaseComponent getComponentFormattedName() {
        Group highest = this.getHighestGroup();

        if (highest == null) {
            return Group.DEFAULT.formatComponentName(this.getName(), this.getChampionTier());
        }

        return highest.formatComponentName(this.getName(), this.getChampionTier());
    }

    public BaseComponent getComponentFormattedNameOrNick() {
        if (this.isAndHasNick()) {
            Group highest = this.nick.group;


            return highest.formatComponentName(this.nick.name, 0);
        }
        return this.getComponentFormattedName();
    }

    public String getPossessiveFormattedName() {
        return com.lifeknight.relayutils.basic.Text.getPossessiveName(this.getFormattedName());
    }

    public String getFormattedNickname() {
        return this.nick.getFormattedName();
    }

    public ChatColor getFormattedNameColor() {
        return this.getHighestGroup().getColor();
    }

    public ChatColor getFormattedNickColor() {
        return this.nick.group.getColor();
    }

    public Group getHighestGroup() {
        return Group.getHighestGroup(this.groups);
    }

    public String getServerName() {
        return this.getServerInfo().getName();
    }

    public boolean isOnServer(String serverName) {
        return this.getServerName().equalsIgnoreCase(serverName);
    }

    public boolean isOnSameServer(SmartPlayer smartPlayer) {
        return this.isOnline() && this.getServerInfo() == (smartPlayer.getServerInfo());
    }

    public void queueGame(GameType gameType) {
        if (gameType.getTotalPlayerCount() == 1) {
            this.queueGame(gameType, new QueueData().setParty());
        } else {
            this.queueGame(gameType, new QueueData());
        }
    }

    public void queueGame(GameType gameType, QueueData queueData) {
        if (gameType == null) return;
        if (!gameType.isEnabled()) {
            this.sendErrorMessage("%s%s%s is currently disabled. Visit our Discord server for more information.", YELLOW, gameType.getFullPrettyName(), RED);
            return;
        }

        if (gameType == GameType.TERMINATOR_SOLOS) {
            if (!this.hasChampion()) {
                this.sendChampionPurchaseMessage();
                return;
            }
        }

        if (gameType.isExplicitParty()) {
            queueData.setParty();
        }

        if (queueData.isParty() && gameType.getTotalPlayerCount() > 1) {
            if (!this.isInParty() || this.currentParty.getSize() < 2) {
                queueData.attachBoolean("party", false);
            }
        }

        if (!Main.queueOpen) {
            ComponentBuilder componentBuilder = new ComponentBuilder();
            componentBuilder.color(ComponentBuilder.YELLOW).line().newLine();
            componentBuilder.color(RED).append("Game queues are currently closed. Message from Relay:").newLine();
            componentBuilder.color(WHITE).append(Main.queueCloseMessage == null ? "None" : Main.queueCloseMessage).newLine();
            componentBuilder.color(ComponentBuilder.YELLOW).line();
            this.sendMessage(componentBuilder);
            return;
        }

        if (gameType.isExplicitParty()) {
            if (!this.isInParty()) {
                this.sendErrorMessage("This game can only be played in parties of 2 or more!");
            } else {
                if (this.currentParty.canStartGame(this)) {
                    if (this.currentParty.getSize() < 2) {
                        this.sendErrorMessage("You can only play party games with party sizes of 2 or more!");
                    } else {
                        if (this.currentParty.hasOfflinePlayers()) {
                            this.sendPartyErrorMessage("You cannot join a queue while your party has players offline!");
                        } else {
                            if (gameType.getMainGame() == MainGame.MANHUNT) {
                                if (this.isBannedFromManhunt) {
                                    this.sendErrorMessage("You are currently banned from Manhunt for quitting before the game ends!");
                                    this.sendManhuntBanTimeRemaining();
                                } else {
                                    if (this.currentParty.getSize() > 16 && !this.isAdministrator()) {
                                        this.sendErrorMessage("You can play Manhunt with a max of 16 players!");
                                    } else {
                                        this.sendQueue(gameType, queueData);
                                    }
                                }
                            } else {
                                this.sendQueue(gameType, queueData);
                            }
                        }
                    }
                } else {
                    this.sendErrorMessage("Only party leaders can join games!");
                }
            }
        } else {
            if (this.isInParty()) {
                if (!this.getCurrentParty().canStartGame(this)) {
                    this.sendPartyErrorMessage("Only party leaders can join games!");
                } else if (this.currentParty.hasOfflinePlayers()) {
                    this.sendPartyErrorMessage("You cannot join a queue while your party has players offline!");
                } else {
/*
                    if (gameType.isRankedType() && this.currentParty.getSize() > gameType.getTeamSize()) {
                        boolean single = gameType.getTeamSize() == 1;
                        if (single) {
                            this.sendErrorMessage("This game cannot be played in a party!");
                        } else {
                            this.sendErrorMessage("This game can be played with maximum party size of %s%d%s!", YELLOW, RED);
                        }
                    } else {
*/
                    if (gameType.getMainGame() == MainGame.MANHUNT) {
                        if (this.isBannedFromManhunt) {
                            this.sendErrorMessage("You are currently banned from Manhunt for quiting before the game ends!");
                            this.sendManhuntBanTimeRemaining();
                        } else {
                            for (SmartPlayer player : this.currentParty.getPlayers()) {
                                if (player.isBannedFromManhunt) {
                                    this.sendErrorMessage("One of the players in your party is banned from Manhunt for quitting games!");
                                    player.sendErrorMessage("Your party leader could not queue because you are banned from Manhunt.");
                                    player.sendManhuntBanTimeRemaining();
                                    return;
                                }
                            }

                            this.sendQueue(gameType, queueData);
                        }
                    } else {
                        this.sendQueue(gameType, queueData);
                    }
//                    }
                }
            } else {
                if (gameType.getMainGame() == MainGame.MANHUNT && this.isBannedFromManhunt) {
                    this.sendErrorMessage("You are currently banned from Manhunt for quiting before the game ends!");
                } else {
                    this.sendQueue(gameType, queueData);
                }
            }
        }
    }

    private void sendManhuntBanTimeRemaining() {
        this.sendInfoMessage("Time remaining on your Manhunt ban: %s%s", RED, this.getFormattedManhuntBanRemainingTime());
    }

    private void sendQueue(GameType gameType, QueueData queueData) {
        if (this.isInParty()) {
            if (gameType == GameType.DUELS_CUSTOM) {
                queueData.attachData("owner", this.getUUID());
            }
            Queue.addPartyToQueue(gameType, queueData, this, this.currentParty);
        } else {
            Queue.addPlayerToQueue(gameType, queueData, this);
        }

        this.queueData = queueData;
    }

    public boolean removeFromGroup(Group group) {
        if (this.groups.remove(group)) {
            this.groupUpdateCheck();
            this.updateProfile(ProfileColumn.GROUPS);
            return true;
        }
        return false;
    }

    public void groupUpdateCheck() {
        if (!this.isInGroupOrHigher(Group.TIERIII) && this.isAndHasNick()) {
            this.isNicked = false;
            this.onNickChange();
        }
    }

    public boolean addToGroup(Group group) {
        if (Miscellaneous.addIfAbsent(this.groups, group)) {
            this.updateProfile(ProfileColumn.GROUPS);
            return true;
        }
        return false;
    }

    public synchronized boolean isInGroup(Group group) {
        return this.groups.contains(group);
    }

    public boolean isStaff() {
        return this.isInGroupOrHigher(Group.DEVELOPER);
    }

    public boolean hasChampion() {
        return this.isInGroupOrHigher(Group.CHAMPION);
    }

    public boolean hasMedia() {
        return this.isInGroupOrHigher(Group.TIKTOK);
    }

    public boolean isInGroupOrHigher(Group group) {
        if (this.isInGroup(group)) return true;

        return this.isInHigherGroup(group);
    }

    public synchronized boolean isInHigherGroup(Group group) {
        for (Group group1 : this.groups) {
            if (group1.isHigherThan(group)) {
                return true;
            }
        }

        return false;
    }

    public boolean isAdministrator() {
        return this.isInGroupOrHigher(Group.ADMIN);
    }

    public void sendInsufficientPermissionsMessage() {
        CommandUtilities.sendInsufficientPermissionsMessage(this.proxiedPlayer);
    }

    public void sendCommandsMessage(List<String> commands) {
        CommandUtilities.sendCommandsMessage(this.proxiedPlayer, commands);
    }

    public void noPlayerFound(String name) {
        CommandUtilities.noPlayerFound(this.proxiedPlayer, name);
    }

    public void sendToPlayer(SmartPlayer smartPlayer) {
        this.sendToServer(smartPlayer.getServerInfo());
    }

    public void sendToPlayer(ProxiedPlayer proxiedPlayer) {
        this.sendToServer(proxiedPlayer.getServer().getInfo());
    }

    public synchronized void sendToServer(ServerInfo serverInfo) {
        if (serverInfo != null) {
            this.lastServer = Server.getServer(serverInfo);
            if (this.getServerInfo() != serverInfo) {
                if (this.connectingServer != serverInfo || Miscellaneous.enoughSecondsHavePassed(lastConnectTime, 1)) {
                    this.connectingServer = serverInfo;
                    this.lastConnectTime = System.currentTimeMillis();
                    Main.synchronous(() -> this.proxiedPlayer.connect(serverInfo));
                    RelayUtils.log("Connecting %s to %s", this.getName(), serverInfo.getName());
                }
            }
        } else {
            Main.error("Could not send to null serverinfo: %s", this.getName());
        }
    }

    public void sendToServer(Server server) {
        if (server != null) {
            if (server.isLimbo()) {
                this.lastHubServer = this.getServer();
            }
/*
            if (!this.isInGroupOrHigher(Group.CHAMPION) && server.isHub() && !server.hasSufficientSpace()) {
                if (!this.getServer().isHub()) {
                    this.sendToLimbo();
                    this.sendErrorMessage("The server you are trying to connect to is full. Sending you to limbo.");
                } else {
                    this.sendErrorMessage("The server you are trying to connect to is full.");
                }
            } else {
*/
            this.sendToServer(server.getServerInfo());
//           }
        } else {
            Main.error("Could not connect %s to null server.", this.getName());
            BotUtils.onNullServer();
            this.sendErrorMessage("The server you are trying to connect to is currently restarting. Try again later.");
        }
    }

    public void sendToServer(String serverName) {
        if (Text.comparablyEquals(serverName, "limbo")) {
            this.sendToServer(Server.getLimboServer());
            Main.schedule(() -> this.sendPluginMessage(Command.OTHER, "limbo", this.getUUID()), 1);
            return;
        }

        if (Text.comparablyEquals(serverName, "afk")) {
            this.sendToServer(Server.getLimboServer());
        }

        Server server = Server.getServer(serverName);
        if (server == null) {
            try {
                this.sendToServer(PlayerUtilities.getHubServer(serverName));
            } catch (Exception e) {
                this.sendToServer(Server.getServerOfType(null, false));
            }
        } else {
            this.sendToServer(Server.getServer(serverName));
        }
    }

    public void sendToServer(MainGame mainGame) {
        if (this.lastHubServer != null && this.lastHubServer.getMainGame() == mainGame && this.lastHubServer.isViable(1)) {
            this.sendToServer(this.lastHubServer);
        } else {
            Server server = Server.getServerOfType(mainGame, false);
            if (server == null) {
                this.sendErrorMessage("The lobby you are trying to connect to is currently down. Please try again later. Contact an administrator if this issue persists.");
            } else if (server.getMainGame() != mainGame) {
                if (server == this.getServer()) {
                    this.sendErrorMessage("The lobby you are trying to connect to is restarting. Use the %s/queue %scommand if needed. Contact an administrator if this issue persists.", YELLOW, RED);
                } else {
                    this.sendErrorMessage("The lobby you are trying to connect to is currently restarting. Sending you to the main hub. Contact an administrator if this issue persists.");
                }
            }
            this.sendToServer(server);
        }
    }

    public void spectatePlayer(SmartPlayer toSpectate) {
        ProxiedPlayer toSpectateProxiedPlayer = toSpectate.getProxiedPlayer();
        if (toSpectate.isOnHub()) {
            this.sendToPlayer(toSpectateProxiedPlayer);
        } else {
            Server server = toSpectate.getServer();
            if (server.isMessageClientActive()) {
                server.sendMessage(Command.SPECTATE, this.getUUID(), toSpectate.getUUID());
            } else {
                this.sendErrorMessage("An error occurred while attempting to spectate that player. Please try again in 30 seconds.");
            }
        }
    }

    public UUID getUUID() {
        return this.isOnline() ? this.proxiedPlayer.getUniqueId() : this.uuid;
    }

    public void updateReconnectServer(ServerInfo serverInfo) {
        Server server = Server.getServer(serverInfo);
        try {
            this.setReconnectServer(server.getHub());
        } catch (Exception exception) {
            Main.error("An error occurred while attempting to change the reconnect server for %s: %s", this.getFormattedName(), exception.getMessage());
        }
    }

    public void setReconnectServer(ServerInfo serverInfo) {
        this.proxiedPlayer.setReconnectServer(serverInfo);
    }

    public void setReconnectServer(Server server) {
        if (server != null) {
            this.setReconnectServer(server.getServerInfo());
        }
    }

    public String getSQL() {
        StringBuilder stringBuilder = new StringBuilder("(");

        ProfileColumn[] columns = ProfileColumn.values();

        for (int i = 0; i < columns.length; i++) {
            Object data = this.getData(columns[i]);

            data = "'" + data + "'";

            stringBuilder.append(data);
            if (i != columns.length - 1) {
                stringBuilder.append(", ");
            }
        }

        return stringBuilder.append(")").toString();
    }

    public String getSQLColumns() {
        StringBuilder stringBuilder = new StringBuilder("(");

        String[] columnNames = toStringList(ProfileColumn.values()).toArray(new String[0]);

        for (int i = 0; i < columnNames.length; i++) {
            String columnName = columnNames[i];

            stringBuilder.append("`").append(columnName).append("`");
            if (i != columnNames.length - 1) {
                stringBuilder.append(", ");
            }
        }

        return stringBuilder.append(")").toString();
    }

    public ServerInfo getServerInfo() {
        if (!this.isOnline()) {
            return null;
        }

        net.md_5.bungee.api.connection.Server server = this.proxiedPlayer.getServer();
        if (server == null) {
            return null;
        }
        return server.getInfo();
    }

    public List<String> getNamesOfPlayersOnServer() {
        List<String> names = Miscellaneous.processList(this.getServerInfo().getPlayers(), proxiedPlayer1 -> getSmartPlayer(proxiedPlayer1).getNameOrNick());
        names.remove(this.getNameOrNick());
        return names;
    }

    public BaseComponent getLocationForInfo() {
        ComponentBuilder base = new ComponentBuilder(this.getComponentFormattedNameColor()).append(this.getName()).append(YELLOW).append(" ");

        if (this.isOnHub()) {
            base.append("is in the ").append(this.getServer().getNameForInfo()).append(" lobby");
        } else if (this.getServer().isLimbo()) {
            base.append("is ").append(this.isInLimboQueue() ? "waiting in Limbo" : "AFK");
        } else {
            base.append("is in a ").append(this.lastGameType == null ? this.getServer().getNameForInfo() : this.lastGameType.getFullPrettyName()).append(" game");
        }

        return base.getResult();
    }

    private Color getComponentFormattedNameColor() {
        if (this.getHighestGroup() == Group.CHAMPION) {
            return RelayUtils.getChampionTierColor(this.getChampionTier());
        }

        return this.getHighestGroup().getOther();
    }

    public boolean isOnHub() {
        return this.getServer() == null || this.getServer().isHub();
    }

    public Server getServer() {
        return Server.getServer(this.getServerInfo());
    }

    public boolean hasNick() {
        return this.nick != null;
    }

    public boolean isAndHasNick() {
        return this.isInGroupOrHigher(Group.CHAMPION) && this.isNicked && this.hasNick();
    }

    public void setNick(Nick nick) {
        this.isNicked = true;
        this.nick = nick;
        this.sendSuccessMessage("Your nick:");
        this.sendInfoMessage("Name: %s", nick.getFormattedName());
        if (nick.textures != null) {
            this.sendInfoMessage("Skin: %s%s", ChatColor.GREEN, nick.texturesOwnerName);
        }
        this.onNickChange();
    }

    public void onNickChange() {
        this.proxiedPlayer.setDisplayName(this.getName());
        this.updateProfile(ProfileColumn.NICK);
        this.updateProfile(ProfileColumn.IS_NICKED);
    }

    public boolean canNick() {
        if (GameServer.findRejoin(this.getUUID()) != null) {
            this.sendErrorMessage("You can not rejoin while you can rejoin a match!");
            return false;
        }

        if (!this.getHighestGroup().isHigherThan(Group.CHAMPION)) {
            if (this.hasNick()) {
                Nick nick = this.nick;
                long timeNeededToElapse = (this.getHighestGroup() == Group.CHAMPION ? 5 : 60) * 60 * 1000L;
                long timeRemaining = nick.time + timeNeededToElapse - System.currentTimeMillis();
                if (timeRemaining > 0) {
                    if (this.isInGroup(Group.CHAMPION)) {
                        this.sendErrorMessage("You must wait 5 minutes before changing your nickname (%s)!", formatTimeFromMilliseconds(timeRemaining));
                    } else {
                        this.sendErrorMessage("You must wait 6 hours before changing your nickname (%s)! Upgrade to Premium rank to wait 5 minutes!", formatTimeFromMilliseconds(timeRemaining));
                    }
                    return false;
                }
            }
        }

        if (this.isStaff()) {
            return true;
        }

        if (!this.isOnHub()) {
            this.sendErrorMessage("You can only nick in lobbies!");
            return false;
        }

        return true;
    }

    public String getFormattedManhuntBanRemainingTime() {
        return formatTimeFromMilliseconds(this.extraData.getLong("manhuntBanStart", 0) + this.getManhuntBanDuration() - System.currentTimeMillis());
    }

    public void manhuntQuit() {
        if (this.isStaff()) return;

        this.manhuntQuits++;
        if (this.manhuntQuits == 1) {
            this.sendMessage(true, "%sIf you continue to abandon Manhunt games, you will be banned from the queue.", RED);
        }

        this.checkIfManhuntBanned(true);

    }

    public void checkIfManhuntBanned(boolean increase) {
        if (!this.isBannedFromManhunt) {
            if (increase && this.manhuntQuits > 1) {
                this.sendMessage(true, "%sFor abandoning Manhunt games, you have been banned from the Manhunt public queue for %d hour%s!", RED, this.getManhuntBanHours(), this.getManhuntBanHours() == 1 ? "" : "s");
                this.isBannedFromManhunt = true;
                this.extraData.setNumber("manhuntBanStart", System.currentTimeMillis());
                this.extraData.setBoolean("manhuntBanned", this.isBannedFromManhunt);
                return;
            }

            if (this.manhuntQuits > 0 && Miscellaneous.enoughTimeHasPassed(this.extraData.getLong("manhuntBanStart", System.currentTimeMillis()), 1, TimeUnit.DAYS)) {
                this.manhuntQuits = 0;
                this.sendMessage(true, "%sFor not abandoning games in the past 24 hours, your quits have been reset.", GREEN);
            }
        } else {
            long banStart = this.extraData.getLong("manhuntBanStart", System.currentTimeMillis());
            long duration = this.getManhuntBanDuration();

            if (Miscellaneous.enoughTimeHasPassed(banStart, duration)) {
                this.sendMessage(true, "%sYou have been unbanned from the Manhunt public queue.", GREEN);
                this.unBanFromManhunt();
                this.extraData.setBoolean("manhuntBanned", this.isBannedFromManhunt);
            }
        }
        if (increase) {
            this.extraData.setNumber("manhuntQuits", this.manhuntQuits);
            this.updateProfile(ProfileColumn.EXTRA);
        }
    }

    public int getManhuntBanHours() {
        switch (this.manhuntQuits) {
            case 0:
            case 1:
                return 0;
            case 2:
                return 3;
            case 3:
                return 8;
            default:
                return 24;
        }
    }

    public long getManhuntBanDuration() {
        int hours = this.getManhuntBanHours();

        return hours * 60 * 60 * 1000L;
    }

    public void unBanFromManhunt() {
        this.isBannedFromManhunt = false;
    }

    public static boolean isNickname(String name) {
        return getFromNickname(name) != null;
    }

    public static SmartPlayer getFromNickname(String name) {
        for (SmartPlayer smartPlayer : getSmartPlayers()) {
            if (smartPlayer.isAndHasNick() && smartPlayer.nick.getName().equalsIgnoreCase(name)) {
                return smartPlayer;
            }
        }

        return null;
    }

    public static String getActualNickname(String name) {
        SmartPlayer smartPlayer = getFromNickname(name);
        if (smartPlayer != null) {
            return smartPlayer.nick.getName();
        }
        return null;
    }

    public List<ChatMessage> getMessages(ChatType chatType, int page) {
        List<ChatMessage> queried = getMessagesOfType(chatType);
        List<ChatMessage> results = new ArrayList<>();
        int startingIndex = PUNISHMENTS_PAGE_LENGTH * (page - 1);

        for (int i = 0; i < PUNISHMENTS_PAGE_LENGTH; i++) {
            int index = startingIndex + i;
            if (index >= queried.size()) {
                break;
            }
            results.add(queried.get(index));
        }

        return results;
    }

    public List<ChatMessage> getMessagesOfType(ChatType chatType) {
        List<ChatMessage> queried = chatType == null ? Miscellaneous.getList(this.messages) : Miscellaneous.filter(this.messages, chatMessage -> chatMessage.getChatType() == chatType);
        return Miscellaneous.reverse(queried);
    }

    public BaseComponent getMessagesComponent(ChatType chatType, int page) {
        ComponentBuilder componentBuilder = new ComponentBuilder();

        String baseCommand = "/messages " + this.getName() + " ";
        if (chatType != null) {
            baseCommand += toComparable(chatType);
        }

        if (page > 1) {
            componentBuilder.command(GOLD + "<<", baseCommand + (page - 1), AQUA + "Click here to view the previous page.");
        } else {
            componentBuilder.append(GOLD + "||");
        }
        componentBuilder.append(" ");

        componentBuilder.gradientLine(PlayerUtilities.HALF_LINE_LENGTH, ComponentBuilder.PURPLE, ComponentBuilder.PINK).append(" %s[%d] ", GOLD, page).gradientLine(PlayerUtilities.HALF_LINE_LENGTH, ComponentBuilder.PINK, ComponentBuilder.PURPLE).append(" ");

        if (page < this.getMaxMessagePage(chatType)) {
            componentBuilder.command(GOLD + ">>", baseCommand + (page + 1), AQUA + "Click here to view the next page.");
        } else {
            componentBuilder.append(GOLD + "||");
        }

        componentBuilder.newLine();

        for (ChatMessage message : this.getMessages(chatType, page)) {
            componentBuilder.append(message.getComponent(chatType == null)).newLine();
        }

        componentBuilder.gradientLine(ComponentBuilder.PURPLE, ComponentBuilder.PINK, ComponentBuilder.PURPLE);

        return componentBuilder.getResult();
    }

    public int getMaxMessagePage(ChatType chatType) {
        return (int) Math.ceil(this.getMessagesOfType(chatType).size() / (double) PUNISHMENTS_PAGE_LENGTH);
    }

    public boolean hasActivePunishment(PunishmentType punishmentType) {
        return this.getLatestActivePunishment(punishmentType) != null;
    }

    public boolean isBanned() {
        if (this.isAdministrator()) {
            return false;
        }

        return this.hasActivePunishment(PunishmentType.BAN) || this.hasActivePunishment(PunishmentType.IP_BAN);
    }

    public boolean isMuted() {
        if (this.isAdministrator()) {
            return false;
        }

        return this.hasActivePunishment(PunishmentType.MUTE);
    }

    public void mute(SmartPlayer punisher, long duration, String reason) {
        this.createPunishment(punisher, PunishmentType.MUTE, duration, reason);
        this.sendMessage(true, this.getLatestMuteReason());
    }

    public void unMute(SmartPlayer revoker, String reason) {
        this.revokeLatestPunishment(PunishmentType.MUTE, revoker, reason);
        this.sendSuccessMessage("You are no longer muted.");
    }

    public void ban(SmartPlayer punisher, long duration, String reason) {
        this.createPunishment(punisher, PunishmentType.BAN, duration, reason);
        if (this.isOnline()) {
            this.proxiedPlayer.disconnect(this.getLatestBanReason());
        }
    }

    public void ipBan(SmartPlayer smartPlayer, long duration, String reason) {
        this.createPunishment(smartPlayer, PunishmentType.IP_BAN, duration, reason);
        if (this.isOnline()) {
            this.proxiedPlayer.disconnect(this.getLatestBanReason());
        }
    }

    public void unBan(SmartPlayer revoker, String reason) {
        if (this.hasActivePunishment(PunishmentType.BAN)) {
            this.revokeLatestPunishment(PunishmentType.BAN, revoker, reason);
        } else {
            this.revokeLatestPunishment(PunishmentType.IP_BAN, revoker, reason);
        }
        this.queueSuccessMessageForJoin("You are no longer banned.");
    }

    public void warn(SmartPlayer punisher, long duration, String reason) {
        this.createPunishment(punisher, PunishmentType.WARNING, duration, reason);
        this.sendMessage(true, this.getLatestWarnReason());
    }

    private static final int PUNISHMENTS_PAGE_LENGTH = 5;

    public List<Punishment> getPunishments(PunishmentType punishmentType, boolean activeOnly, int page) {
        List<Punishment> queried = getPunishmentsOfType(punishmentType, activeOnly);
        List<Punishment> results = new ArrayList<>();
        int startingIndex = PUNISHMENTS_PAGE_LENGTH * (page - 1);

        for (int i = 0; i < PUNISHMENTS_PAGE_LENGTH; i++) {
            int index = startingIndex + i;
            if (index >= queried.size()) {
                break;
            }
            results.add(queried.get(index));
        }

        return results;
    }

    public List<Punishment> getPunishmentsOfType(PunishmentType punishmentType, boolean activeOnly) {
        List<Punishment> queried = punishmentType == null ? Miscellaneous.filter(this.punishments, punishment -> !activeOnly || punishment.isActive()) : Miscellaneous.filter(this.punishments, punishment -> punishment.getType() == punishmentType && (!activeOnly || punishment.isActive()));
        return Miscellaneous.reverse(queried);
    }

    public BaseComponent getPunishmentsComponent(PunishmentType punishmentType, boolean activeOnly, int page) {
        ComponentBuilder componentBuilder = new ComponentBuilder();

        String baseCommand = "/punishments " + (activeOnly ? "view" : "viewactive") + " " + this.getName() + " ";
        if (punishmentType != null) {
            baseCommand += toComparable(punishmentType) + " ";
        }

        if (page > 1) {
            componentBuilder.command(GOLD + "<<", baseCommand + (page - 1), AQUA + "Click here to view the previous page.");
        } else {
            componentBuilder.append(GOLD + "||");
        }
        componentBuilder.append(" ");

        componentBuilder.gradientLine(30, ComponentBuilder.PINK_RED, ComponentBuilder.RED_ORANGE).append(" %s[%d] ", GOLD, page).gradientLine(30, ComponentBuilder.RED_ORANGE, ComponentBuilder.PINK_RED).append(" ");

        if (page < this.getMaxPunishmentPage(punishmentType, activeOnly)) {
            componentBuilder.command(GOLD + ">>", baseCommand + (page + 1), AQUA + "Click here to view the next page.");
        } else {
            componentBuilder.append(GOLD + "||");
        }

        componentBuilder.newLine();

        for (Punishment punishment : this.getPunishments(punishmentType, activeOnly, page)) {
            componentBuilder.append(getPunishmentComponent(punishment)).newLine();
        }

        componentBuilder.gradientLine(ComponentBuilder.PINK_RED, ComponentBuilder.RED_ORANGE, ComponentBuilder.PINK_RED);

        return componentBuilder.getResult();
    }

    public static BaseComponent getPunishmentComponent(Punishment punishment) {
        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.append(GRAY).append("[").append(WHITE).append(punishment.getID()).append(GRAY).append("] [");
        componentBuilder.color(ComponentBuilder.RED).append(Text.prettify(punishment.getType()).toUpperCase()).append(GRAY).append("] [");
        componentBuilder.color(ComponentBuilder.AQUA).append(Text.getDateTimeString(punishment.getTime())).append(GRAY).append("]");
        componentBuilder.newLine();
        componentBuilder.color(ComponentBuilder.RED_ORANGE).append(" - ID: ").color(Color.PINK).append(punishment.getBanID()).newLine();
        SmartPlayer punisher = SmartPlayer.getSmartPlayer(punishment.getPunisher());
        componentBuilder.color(ComponentBuilder.RED_ORANGE).append(" - Punisher: ").color(ComponentBuilder.GREEN).append(punishment.wasPunishedByConsole() ? "Console" : (punisher == null ? punishment.getPunisher() : punisher.getFormattedName())).newLine().color(ComponentBuilder.RED_ORANGE).append(" - Duration: ").color(Color.PINK).append(punishment.isIndefinite() ? "Permanent" : getShortTextualFormattedTime(punishment.getDuration())).newLine();
        if (!punishment.isIndefinite()) {
            componentBuilder.color(ComponentBuilder.RED_ORANGE).append(" - Expires: ").color(ComponentBuilder.YELLOW).append(Text.getDateTimeString(punishment.getTime() + punishment.getDuration())).newLine();
        }
        componentBuilder.color(ComponentBuilder.RED_ORANGE).append(" - Reason: ").color(ComponentBuilder.WHITE).append(punishment.hasReason() ? punishment.getReason() : "None");

        if (!punishment.isActive()) {
            componentBuilder.newLine();
            SmartPlayer revoker = SmartPlayer.getSmartPlayer(punishment.getRevoker());
            componentBuilder.color(ComponentBuilder.YELLOW_GREEN).append(" - Revoker: ").color(ComponentBuilder.YELLOW).append(punishment.wasRevokedByConsole() ? "Console" : (revoker == null ? punishment.getRevoker() : revoker.getFormattedName())).newLine();
            componentBuilder.color(ComponentBuilder.YELLOW_GREEN).append(" - Revoked: ").color(Color.PINK).append(Text.getDateString(punishment.getRevokeTime())).append(" ").append(Text.getTimeString(punishment.getRevokeTime()));
        }

        return componentBuilder.getResult();
    }

    public int getMaxPunishmentPage(PunishmentType punishmentType, boolean activeOnly) {
        return (int) Math.ceil(this.getPunishmentsOfType(punishmentType, activeOnly).size() / (double) PUNISHMENTS_PAGE_LENGTH);
    }

    public void kick() {
        this.kick("Goodbye.");
    }

    public void kick(String format, Object... data) {
        if (this.isOnline()) {
            this.proxiedPlayer.disconnect(new TextComponent(RelayUtils.format(format, data)));
        }
    }

    public void kick(BaseComponent baseComponent) {
        if (this.isOnline()) {
            this.proxiedPlayer.disconnect(baseComponent);
        }
    }

    public void kick(ComponentBuilder componentBuilder) {
        this.kick(componentBuilder.getResult());
    }

    public void createPunishment(SmartPlayer smartPlayer, PunishmentType punishmentType, long duration, String reason) {
        Punishment punishment = new Punishment(this.punishments.size(), smartPlayer == null ? null : smartPlayer.getUUID(), this.getUUID(), punishmentType, duration, reason, (this.proxiedPlayer != null && punishmentType == PunishmentType.IP_BAN) ? Utilities.parseAddress(this.proxiedPlayer.getSocketAddress()) : "");
        this.punishments.add(punishment);
        BotUtils.punish(punishment);
        this.onPunishmentsChange();
    }

    public BaseComponent getBanMessage() {
        Punishment punishment = this.getLatestActivePunishment(PunishmentType.BAN);
        if (punishment == null) {
            punishment = this.getLatestActivePunishment(PunishmentType.IP_BAN);
        }

        ComponentBuilder componentBuilder = new ComponentBuilder();
        long timeRemaining = punishment.getDuration() - (System.currentTimeMillis() - punishment.getTime());
        if (punishment.isIndefinite()) {
            componentBuilder.color(YELLOW).append("Duration: ");
            componentBuilder.color(ComponentBuilder.PINK).append("Permanent");
        } else {
            componentBuilder.color(YELLOW).append("Time Remaining: ");
            long daysRemaining = timeRemaining / (24 * 60 * 60 * 1000L);
            String timeRemainingString = daysRemaining > 0 ? (daysRemaining + " days") : Text.getTextualFormattedTime(timeRemaining, false);
            componentBuilder.color(ComponentBuilder.PINK).append(timeRemainingString);
        }
        componentBuilder.newLine();
        componentBuilder.color(ComponentBuilder.PINK_RED).append("Reason: ").color(WHITE).append(punishment.hasReason() ? punishment.getReason() : "None");
        componentBuilder.newLine();
        componentBuilder.color(ComponentBuilder.RED_ORANGE).append("%s ID: ", Text.prettify(punishment.getType())).color(Color.PINK).append("#").append(punishment.getBanID());
        componentBuilder.newLine();
        componentBuilder.color(ComponentBuilder.AQUA).append("Appeal: ").link(DARK_PURPLE + Main.DISCORD_URL, Main.DISCORD_URL, false);

        return componentBuilder.getResult();
    }

    public String getExpiryDate(Punishment punishment) {
        return Text.getDateTimeString(punishment.getTime() + punishment.getDuration());
    }

    public BaseComponent getLatestBanReason() {
        Punishment ban = this.getLatestActivePunishment(PunishmentType.BAN);
        if (ban == null) {
            ban = this.getLatestActivePunishment(PunishmentType.IP_BAN);
        }

        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.color(ComponentBuilder.BLUE).append(BOLD).append("Relay").newLine().newLine();
        componentBuilder.color(RED);
        componentBuilder.append("You have been banned.").newLine();
        componentBuilder.appendComponent(this.getPunishmentComponent(ban, true, true, true, true));
        return componentBuilder.getResult();
    }

    public BaseComponent getPunishmentComponent(Punishment punishment, boolean showDuration, boolean showID, boolean showAppeal, boolean showLink) {
        ComponentBuilder componentBuilder = new ComponentBuilder();
        if (showDuration) {
            componentBuilder.color(YELLOW).append("Duration: ");
            componentBuilder.color(ComponentBuilder.PINK).append(punishment.isIndefinite() ? "Permanent" : getShortTextualFormattedTime(punishment.getDuration())).newLine();
        }
        componentBuilder.color(ComponentBuilder.PINK_RED).append("Reason: ").color(WHITE).append(punishment.hasReason() ? punishment.getReason() : "None");
        if (showID) {
            componentBuilder.newLine();
            componentBuilder.color(ComponentBuilder.RED_ORANGE).append("%s ID: ", Text.prettify(punishment.getType())).color(Color.PINK).append("#").append(punishment.getBanID());
        }
        if (showAppeal) {
            componentBuilder.newLine();
            componentBuilder.color(ComponentBuilder.AQUA).append("Appeal: ").link(showLink ? DARK_PURPLE + Main.DISCORD_URL : DARK_PURPLE + UNDERLINE.toString() + "Discord", Main.DISCORD_URL, false);
        }

        return componentBuilder.getResult();
    }

    public BaseComponent getLatestMuteReason() {
        Punishment mute = this.getLatestActivePunishment(PunishmentType.MUTE);

        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.color(Color.RED).line().newLine();
        componentBuilder.color(ComponentBuilder.RED);
        componentBuilder.append("You have been muted.").newLine();
        componentBuilder.append((this.getPunishmentComponent(mute, true, true, true, false))).newLine();
        componentBuilder.color(Color.RED).line();

        return componentBuilder.getResult();
    }

    public BaseComponent getLatestWarnReason() {
        Punishment warn = this.getLatestActivePunishment(PunishmentType.WARNING);

        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.gradientLine(ComponentBuilder.ORANGE, ComponentBuilder.YELLOW, ComponentBuilder.ORANGE).newLine();
        componentBuilder.color(ComponentBuilder.RED);
        componentBuilder.append("You have been warned.").newLine();
        componentBuilder.append(this.getPunishmentComponent(warn, false, false, false, false)).newLine();
        componentBuilder.gradientLine(ComponentBuilder.ORANGE, ComponentBuilder.YELLOW, ComponentBuilder.ORANGE);

        return componentBuilder.getResult();
    }

    public Punishment getLatestActivePunishment(PunishmentType punishmentType) {
        for (Punishment punishment : Miscellaneous.reverse(this.punishments)) {
            if (punishment.getType() == punishmentType && punishment.isActive() && !punishment.hasExpired()) {
                return punishment;
            }
        }

        return null;
    }

    public void revokeLatestPunishment(PunishmentType punishmentType, SmartPlayer revoker, String reason) {
        if (this.hasActivePunishment(punishmentType)) {
            this.revokePunishment(this.getLatestActivePunishment(punishmentType), revoker, reason);
        }
    }

    public List<Punishment> getPunishments(PunishmentType punishmentType, boolean activeOnly) {
        List<Punishment> punishments = new ArrayList<>();

        for (Punishment punishment : this.punishments) {
            if ((punishmentType == null || punishment.getType() == punishmentType) && (!activeOnly || punishment.isActive())) {
                punishments.add(punishment);
            }
        }

        return punishments;
    }

    public void clearPunishments() {
        if (!this.punishments.isEmpty()) {
            this.punishments.clear();
            this.onPunishmentsChange();
        }
    }

    public void onPunishmentsChange() {
        this.updateProfile(ProfileColumn.PUNISHMENTS);
    }

    public void checkForUpdates() {
        boolean updated = false;
        for (Punishment punishment : this.punishments) {
            if (punishment.checkIfExpire()) {
                punishment.revokeForExpiry();
                if (punishment.getType() == PunishmentType.MUTE) {
                    this.sendSuccessMessage("Your mute has expired.");
                }
                updated = true;
            }
        }

        if (updated) {
            this.onPunishmentsChange();
        }

        this.checkIfManhuntBanned(false);

        if (this.isOnline()) {
            if (this.discordEarned > 0) {
                this.sendDiscordRewardsMessage();
            }
        }
    }

    public void sendDiscordRewardsMessage() {
        long tokens = RelayUtils.getTokens(this.discordEarned * DISCORD_TOKENS, this.getHighestGroup(), this.getChampionTier());
        long xp = RelayUtils.getXP(this.discordEarned * DISCORD_EXP, this.getHighestGroup(), this.getChampionTier());
        this.sendSuccessMessage("You have received %s%d%s tokens and %s%d%s network EXP for your Discord activity.", GOLD, tokens, GREEN, AQUA, xp, GREEN);
        this.discordEarned = 0;
        this.extraData.setInt("discordEarned", this.discordEarned);
        this.updateProfile(ProfileColumn.EXTRA);
    }

    public boolean hasPunishment(int id) {
        return this.getPunishment(id) != null;
    }

    public Punishment getPunishment(int id) {
        for (Punishment punishment : this.punishments) {
            if (punishment.getID() == id) {
                return punishment;
            }
        }

        return null;
    }

    public void revokePunishment(Punishment punishment, SmartPlayer punisher, String reason) {
        punishment.revoke(punisher == null ? null : punisher.getUUID(), reason);
        this.onPunishmentsChange();
    }

    public List<Integer> getPunishmentIds() {
        return Miscellaneous.processList(this.punishments, Punishment::getID);
    }

    public String getNickname() {
        return this.nick == null ? null : this.nick.getName();
    }

    public void sendPluginMessage(Message message) {
        MessageUtils.sendByPlayer(this.proxiedPlayer, message);
    }

    public void sendPluginMessage(Command command, Object... arguments) {
        MessageUtils.sendByPlayer(this.proxiedPlayer, command, arguments);
    }

    public void playSound(Sound sound) {
        this.sendPluginMessage(Command.SOUND, this.getUUID(), sound);
    }

    public void onGameEnd() {
        this.updateProfile(ProfileColumn.TIME_PLAYED);
        if (this.lastGameType == GameType.DUELS_CUSTOM) {
            Main.broadcastPublicQueues();
        }
    }

    public void levelUp(MainGame mainGame) {
        this.lastLevelUp = System.currentTimeMillis();

        this.broadcastUpdateMessage();
        int newLevel = this.calculateLevel(mainGame);
        ComponentBuilder message = new ComponentBuilder();
        if (mainGame == null) {
            message.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA, ComponentBuilder.BLUE).newLine();
            message.color(GOLD, BOLD).append("LEVEL UP!").newLine();
            message.color(ComponentBuilder.GREEN).append("You are now Network %s%sLEVEL %d!", AQUA, BOLD, newLevel);
            List<Cosmetic> unlockedCosmetics = Cosmetic.getCosmeticRewardsForLevel(newLevel);
            for (Cosmetic unlockedCosmetic : unlockedCosmetics) {
                message.newLine();
                message.color(ComponentBuilder.GREEN).append(" + ").append(unlockedCosmetic.getPrettyName());
                Miscellaneous.addIfAbsent(this.cosmetics, unlockedCosmetic);
            }
            for (GameCosmetic gameCosmetic : GameCosmetic.getGameCosmeticRewardsForLevel(newLevel)) {
                message.newLine();
                message.color(ComponentBuilder.GREEN).append(" + ").append(gameCosmetic.getPrettyName());
                for (MainGame game : gameCosmetic.getMainGames()) {
                    Miscellaneous.addIfAbsent(this.getGameCosmeticList(game), gameCosmetic);
                }
            }
            message.newLine();
            message.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA, ComponentBuilder.BLUE);
            this.sendMessage(message);
        } else {
/*
            message.color(ComponentBuilder.YELLOW).line().newLine();
            message.append("You are now %s level %s%s%d!", mainGame.getName(), AQUA, BOLD, this.calculateLevel(mainGame)).newLine();
            message.color(ComponentBuilder.YELLOW).line();
*/
        }
    }

    public List<GameCosmetic> getGameCosmeticList(MainGame mainGame) {
        return this.gameCosmetics.get(mainGame);
    }

    public List<GameCosmetic> getActiveGameCosmeticList(MainGame mainGame) {
        return this.activeGameCosmetics.get(mainGame);
    }

    public int calculateLevel(MainGame mainGame) {
        return RelayUtils.calculateLevel(mainGame, this.getXp(mainGame));
    }

    public long getXp(MainGame mainGame) {
        if (mainGame == null) {
            return this.xp;
        }

        return this.gameExperience.getOrDefault(mainGame, 0L);
    }

    public long getElo(MainGame mainGame) {
        return this.gameElo.getOrDefault(mainGame, 0L);
    }

    public int calculateLevel() {
        return this.calculateLevel(null);
    }

    public League getLeague(MainGame mainGame) {
        return League.getLeague(this.getElo(mainGame));
    }

    public void sendToHub() {
        if (this.getServer() != null) {
            if (this.lastGameType != null && !this.getServer().isHub() && this.lastGameType.getMainGame() == MainGame.PARTY) {
                this.sendToServer(Server.getServerOfType(null, false));
            } else {
                this.sendToServer(this.getServer().getHub());
            }
        } else {
            this.sendToServer((MainGame) null);
        }
    }

    public long getLastDisconnectTime() {
        return this.lastDisconnectTime;
    }

    public void setLastDisconnectTime(long lastDisconnectTime) {
        this.lastDisconnectTime = lastDisconnectTime;
    }

    public List<BaseComponent> getQueuedMessages() {
        return this.queuedMessages;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public long getFirstJoinTime() {
        return this.firstJoinTime;
    }

    public void setFirstJoinTime(long firstJoinTime) {
        this.firstJoinTime = firstJoinTime;
    }

    public long getPreviousLastJoinTime() {
        return this.previousLastJoinTime;
    }

    public void setPreviousLastJoinTime(long previousLastJoinTime) {
        this.previousLastJoinTime = previousLastJoinTime;
    }

    public long getJoinTime() {
        return this.joinTime;
    }

    public void setJoinTime(long joinTime) {
        this.joinTime = joinTime;
    }

    public boolean isNewLogin() {
        return this.isNewLogin;
    }

    public void setNewLogin(boolean newLogin) {
        isNewLogin = newLogin;
    }

    public List<UUID> getFriends() {
        return this.friends;
    }

    public void setFriends(List<UUID> friends) {
        this.friends = friends;
    }

    public List<UUID> getBlockedPlayers() {
        return this.blockedPlayers;
    }

    public void setBlockedPlayers(List<UUID> blockedPlayers) {
        this.blockedPlayers = blockedPlayers;
    }

    public List<String> getPreviousUsernames() {
        return this.previousUsernames;
    }

    public void setPreviousUsernames(List<String> previousUsernames) {
        this.previousUsernames = previousUsernames;
    }

    public synchronized List<Group> getGroups() {
        return this.groups;
    }

    public void setGroups(List<Group> groups) {
        this.groups = groups;
    }

    public long getCredits() {
        return this.credits;
    }

    public void setCredits(long credits) {
        this.credits = credits;
    }

    public long getXp() {
        return this.xp;
    }

    public void setXp(long xp) {
        this.xp = xp;
    }

    public List<Cosmetic> getCosmetics() {
        return this.cosmetics;
    }

    public void setCosmetics(List<Cosmetic> cosmetics) {
        this.cosmetics = cosmetics;
    }

    public List<Cosmetic> getActiveCosmetics() {
        return this.activeCosmetics;
    }

    public void setActiveCosmetics(List<Cosmetic> activeCosmetics) {
        this.activeCosmetics = activeCosmetics;
    }

    public boolean isNicked() {
        return this.isNicked;
    }

    public void setNicked(boolean nicked) {
        isNicked = nicked;
    }

    public Nick getNick() {
        return this.nick;
    }

    public ChatFilter getChatFilter() {
        return this.chatFilter;
    }

    public void setChatFilter(ChatFilter chatFilter) {
        this.chatFilter = chatFilter;
    }

    public MessageFilter getMessageFilter() {
        return this.messageFilter;
    }

    public void setMessageFilter(MessageFilter messageFilter) {
        this.messageFilter = messageFilter;
    }

    public FriendFilter getFriendFilter() {
        return this.friendFilter;
    }

    public void setFriendFilter(FriendFilter friendFilter) {
        this.friendFilter = friendFilter;
    }

    public PartyFilter getPartyFilter() {
        return this.partyFilter;
    }

    public void setPartyFilter(PartyFilter partyFilter) {
        this.partyFilter = partyFilter;
    }

    public DuelFilter getDuelFilter() {
        return this.duelFilter;
    }

    public void setDuelFilter(DuelFilter duelFilter) {
        this.duelFilter = duelFilter;
    }

    public List<String> getLatestMessagesWithin3Seconds() {
        return this.latestMessagesWithin3Seconds;
    }

    public void setLatestMessagesWithin3Seconds(List<String> latestMessagesWithin3Seconds) {
        this.latestMessagesWithin3Seconds = latestMessagesWithin3Seconds;
    }

    public int getCommandsInPast5Seconds() {
        return this.commandsInPast5Seconds;
    }

    public void setCommandsInPast5Seconds(int commandsInPast5Seconds) {
        this.commandsInPast5Seconds = commandsInPast5Seconds;
    }

    public GameType getCurrentQueue() {
        return this.currentQueue;
    }

    public void setCurrentQueue(GameType currentQueue) {
        this.currentQueue = currentQueue;
    }

    public boolean isHasUpdatedQueueScoreboard() {
        return this.hasUpdatedQueueScoreboard;
    }

    public void setHasUpdatedQueueScoreboard(boolean hasUpdatedQueueScoreboard) {
        this.hasUpdatedQueueScoreboard = hasUpdatedQueueScoreboard;
    }

    public ChatType getChatType() {
        return this.chatType;
    }

    public void setCurrentParty(Party currentParty) {
        this.currentParty = currentParty;
        this.sendPartyData();
    }

    public List<Party> getInvitedParties() {
        return this.invitedParties;
    }

    public void setInvitedParties(List<Party> invitedParties) {
        this.invitedParties = invitedParties;
    }

    public Map<SmartPlayer, GameType> getInvitedDuels() {
        return this.invitedDuels;
    }

    public void setInvitedDuels(Map<SmartPlayer, GameType> invitedDuels) {
        this.invitedDuels = invitedDuels;
    }

    public List<SmartPlayer> getReceivedFriendRequests() {
        return this.receivedFriendRequests;
    }

    public void setReceivedFriendRequests(List<SmartPlayer> receivedFriendRequests) {
        this.receivedFriendRequests = receivedFriendRequests;
    }

    public Server getLastServer() {
        return this.lastServer;
    }

    public void setLastServer(Server lastServer) {
        this.lastServer = lastServer;
    }

    public int getManhuntQuits() {
        return this.manhuntQuits;
    }

    public void setManhuntQuits(int manhuntQuits) {
        this.manhuntQuits = manhuntQuits;
        this.checkIfManhuntBanned(true);
    }

    public boolean isBannedFromManhunt() {
        return this.isBannedFromManhunt;
    }

    public void setBannedFromManhunt(boolean bannedFromManhunt) {
        this.isBannedFromManhunt = bannedFromManhunt;
    }

    public void report(SmartPlayer toReport, ReportType reportType, String reason) {
        if (reason != null) {
            if (!SwearUtils.canSendMessageAtLevel(reason, ChatFilter.HIGH)) {
                this.sendErrorMessage("Your reason must not contain vulgar language.");
                return;
            }
        } else if (reportType == ReportType.BUG) {
            this.sendErrorMessage("You must explain a bug report!");
            return;
        }

        Report report = new Report(this, toReport, reportType, this.getServer(), reason);

        this.lastReportTime = System.currentTimeMillis();
        if (reportType == ReportType.BUG) {
            this.sendSuccessMessage("Submitted bug report.");
            return;
        }

        this.sendSuccessMessage("Submitted report on %s%s for %s%s.", toReport.getFormattedNameOrNick(), GREEN, reportType.toString(), reason != null ? " (" + reason + ")" : "");
    }

    public boolean canReport() {
        if (this.isAdministrator()) {
            return true;
        }
        return !this.isMuted() && Miscellaneous.enoughTimeHasPassed(this.lastReportTime, 5, TimeUnit.MINUTES);
    }

    public String getFormattedNameOrNick() {
        return this.isAndHasNick() ? this.getFormattedNickname() : this.getFormattedName();
    }

    public String getNameOrNick() {
        return this.isAndHasNick() ? this.getNickname() : this.getName();
    }

    public boolean isReceiveReportMessages() {
        return this.extraData.getBoolean("receiveReportMessages", true);
    }

    public void toggleReceiveReportMessages() {
        this.receiveReportMessages = !this.receiveReportMessages;
        this.extraData.setBoolean("receiveReportMessages", this.receiveReportMessages);
        this.updateProfile(ProfileColumn.EXTRA);
    }

    public long getLastReportTime() {
        return this.lastReportTime;
    }

    public void onReportDismiss(Report report) {
        ComponentBuilder componentBuilder = new ComponentBuilder();
        if (report.isAccepted()) {
            componentBuilder.color(ComponentBuilder.GREEN);
        } else {
            componentBuilder.color(ComponentBuilder.YELLOW);
        }
        componentBuilder.line().newLine();
        componentBuilder.color(ComponentBuilder.YELLOW);

        componentBuilder.append("After review by staff, your report on %s ", report.getReported().getFormattedNameOrNick()).color(ComponentBuilder.YELLOW).append("for ").color(report.getReportType().getColor()).append(report.getReportType().toString()).color(ComponentBuilder.YELLOW).append(" has been ");
        if (report.isAccepted()) {
            componentBuilder.color(ComponentBuilder.GREEN).append("accepted");
        } else {
            componentBuilder.color(ComponentBuilder.RED).append("denied");
        }
        componentBuilder.color(ComponentBuilder.YELLOW).append(".").newLine();
        if (report.hasDismissMessage()) {
            componentBuilder.color(Color.PINK).append("Message: ").color(ComponentBuilder.WHITE).append(report.getDismissMessage()).newLine();
        }
        if (report.getTokensRewarded() != 0) {
            componentBuilder.append(GOLD).append("Tokens: %d", report.getTokensRewarded()).newLine();
            this.credits += RelayUtils.getTokens(report.getTokensRewarded(), this.getHighestGroup(), this.getChampionTier());
            this.updateProfile(ProfileColumn.CREDITS);
        }
        componentBuilder.color(ComponentBuilder.YELLOW).line();

        this.sendMessage(true, componentBuilder.getResult());
    }

    public void sendChampionPurchaseMessage() {
        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA_BLUE, ComponentBuilder.BLUE).newLine();
        if (this.isInGroup(Group.CHAMPION)) {
            componentBuilder.color(java.awt.Color.YELLOW).append("In order to use this feature, you must upgrade to a higher Premium tier.").newLine();
            componentBuilder.color(GOLD).append("Upgrade: ").link("https://store.relaymc.net/package/5897406").newLine();
        } else {
            componentBuilder.color(java.awt.Color.YELLOW).append("In order to use this feature, you must have Premium rank.").newLine();
            componentBuilder.color(GOLD).append("Buy Premium: ").link(RelayUtils.CHAMPION_PURCHASE_LINK).newLine();
        }
        componentBuilder.gradientLine(ComponentBuilder.BLUE, ComponentBuilder.AQUA_BLUE, ComponentBuilder.BLUE);

        this.sendMessage(componentBuilder);
    }

    public void giveMonthlyChampionTokens() {
        int tokens = new int[]{0, 5000, 7500, 9000, 10000, 11000, 12000, 12500, 13000, 13500, 13750, 14000, 14250, 14500, 14750, 15000, 15250, 15500, 15750, 15600, 16250, 16500, 16750, 17000, 17250}[Math.min(this.getChampionTier(), 24)];
        this.sendSuccessMessage("You received %s%d%s tokens!", GOLD, tokens, GREEN);
        this.credits += tokens;
        this.updateProfile(ProfileColumn.CREDITS);
    }

    public boolean equals(SmartPlayer smartPlayer) {
        return smartPlayer != null && smartPlayer.uuid.equals(this.uuid);
    }

    public void reQueue() {
        if (this.lastGameType != null) {
            this.queueGame(this.lastGameType);
        } else {
            this.sendErrorMessage("There game for you to requeue!");
        }
    }

    public void duelsRematchOption(SmartPlayer opponent, GameType gameType, String queueData) {
        this.rematchOpponent = opponent;
        this.rematchGameType = gameType;
        this.rematchQueueData = QueueData.fromString(queueData);
        this.acceptRematch = false;
        ComponentBuilder message = new ComponentBuilder(Color.BLUE).line().newLine();
        message.color(ComponentBuilder.AQUA).append("Rematch ").append(opponent.getFormattedNameOrNick()).color(ComponentBuilder.AQUA).append("?").newLine();
        message.command(GOLD + "Click here.", "/rematch " + opponent.getNameOrNick()).newLine();
        message.color(Color.BLUE).line();
        this.sendMessage(message);
    }

    public void rematch(SmartPlayer opponent) {
        if (opponent != null && opponent.rematchOpponent == this) {
            if (this.acceptRematch) {
                this.sendErrorMessage("You have already invited this player to a rematch!");
                return;
            }
            this.acceptRematch = true;
            if (opponent.acceptRematch) {
                Queue.rematch(this, opponent, rematchGameType, rematchQueueData);
            } else {
                this.sendSuccessMessage("Rematch request sent.");
                Main.schedule(() -> {
                    if (!opponent.acceptRematch) {
                        ComponentBuilder message = new ComponentBuilder(GOLD.toString()).line().newLine();
                        message.append(this.getFormattedNameOrNick()).color(ComponentBuilder.AQUA).append(" would like to rematch!").newLine();
                        message.command(GOLD + "Click here to accept.", "/rematch " + this.getNameOrNick()).newLine();
                        message.color(GOLD).line();
                        opponent.sendMessage(message);
                    }
                }, 2);
            }
        } else {
            this.sendErrorMessage("Your rematch opportunity has expired.");
        }
    }

    public void sendToLimbo() {
        this.sendToServer("limbo");
    }

    public void sendLimboQueuePosition(boolean info) {
        this.sendPluginMessage(Command.OTHER, "limboqueueposition", this.getUUID(), LIMBO_QUEUE.indexOf(this) + 1, LIMBO_QUEUE.size());
        if (info) {
            ComponentBuilder componentBuilder = new ComponentBuilder().gradientLine(Color.GREEN, ComponentBuilder.YELLOW_GREEN, Color.GREEN).newLine();
            componentBuilder.color(Color.RED).append("The network is currently at capacity. You must wait until a player leaves to log on. Premium rank users bypass this restriction.").newLine();
            //componentBuilder.bold().color(GOLD).append("YOUR POSITION: ").reset().color(Color.WHITE).append(LIMBO_QUEUE.indexOf(this) + 1).color(ComponentBuilder.AQUA).append("/%d", LIMBO_QUEUE.size()).newLine();
            componentBuilder.gradientLine(ComponentBuilder.GREEN, ComponentBuilder.YELLOW_GREEN, Color.GREEN);
            this.sendMessage(componentBuilder);
        }
    }

    public void onMoveAFK() {
        if (!this.isInGroupOrHigher(Group.CHAMPION)) {
            if (Main.networkIsFull()) {
                this.enterLimboQueue();
                return;
            }
        }

        if (this.lastHubServer != null) {
            this.sendToServer(this.lastHubServer.getMainGame());
        } else {
            this.sendToHub();
        }
    }

    public void onAFKAgain() {
        this.sendErrorMessage("You have gone AFK for too long. Your position in the queue has been reset.");
        LIMBO_QUEUE.remove(this);
    }

    public boolean isInLimboQueue() {
        return LIMBO_QUEUE.contains(this);
    }

    public void skipLimboQueue() {
        LIMBO_QUEUE.remove(this);
        this.sendToHub();
    }

    public void chat(String message) {
        this.sendPluginMessage(Command.OTHER, "chat", this.getUUID(), message);
    }

    public void playSuccessSound() {
        this.playSound(Sound.BLOCK_NOTE_BLOCK_BELL);
    }

    public boolean hasDiscordLinked() {
        return this.discordId != 0;
    }

    public void setDiscordId(long id) {
        this.discordId = id;
        this.updateProfile(ProfileColumn.DISCORD_ID);
    }

    public void unlinkDiscord() {
        BotUtils.ID_TO_UUID.remove(this.discordId);
        this.discordId = 0;
        this.updateProfile(ProfileColumn.DISCORD_ID);
    }

    public Long getDiscordId() {
        return this.discordId;
    }

    public void onDiscordMessage(String message) {
        int messagesSent = this.extraData.getInt("discordRewardMessagesSent", 0);
        if (Miscellaneous.enoughSecondsHavePassed(this.lastDiscordMessage, RelayUtils.testing ? 1 : 10)) {
            this.lastDiscordMessage = System.currentTimeMillis();
            messagesSent++;
            if (messagesSent % (RelayUtils.testing ? 10 : 25) == 0) {
                this.discordMessagesReward();
            }

            this.extraData.setInt("discordRewardMessagesSent", messagesSent);
            this.updateProfile(ProfileColumn.EXTRA);
        }
    }

    private static final long DISCORD_TOKENS = RelayUtils.testing ? 125 : 5;
    private static final long DISCORD_EXP = RelayUtils.testing ? 5000 : 15;

    public void discordMessagesReward() {
        this.credits += DISCORD_TOKENS;
        this.xp += DISCORD_EXP;
        this.discordEarned++;
        this.extraData.setInt("discordEarned", this.discordEarned);
        this.updateProfile(ProfileColumn.EXTRA);
        this.updateProfile(ProfileColumn.CREDITS);
        this.updateProfile(ProfileColumn.EXPERIENCE);
    }

    public void onPurchaseChampion() {
        this.extraData.setInt("championTier", this.getChampionTier() + 1);
        this.updateProfile(ProfileColumn.EXTRA);

        this.giveMonthlyChampionTokens();
    }

    public int getChampionTier() {
        int i = this.extraData.has("championTier") ? this.extraData.getInt("championTier", 0) : 0;

        if (i == 0 && this.hasChampion()) {
            return 1000;
        }

        return i;
    }

    public void onPurchaseSkipTier() {
        this.extraData.setInt("championTier", this.getChampionTier() + 1);
        if (!this.isInGroup(Group.CHAMPION)) {
            this.addToGroup(Group.CHAMPION);
            this.updateProfile(ProfileColumn.GROUPS);
        }

        this.updateProfile(ProfileColumn.EXTRA);

        this.sendMessage(true, "%sYou have skipped a tier and are now %s.", GREEN, this.getFormattedName());
    }
}