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

import com.google.common.io.ByteArrayDataInput;
import com.google.common.io.ByteStreams;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.lifeknight.relaymcbungeemain.commands.amenities.*;
import com.lifeknight.relaymcbungeemain.commands.chat.*;
import com.lifeknight.relaymcbungeemain.commands.game.*;
import com.lifeknight.relaymcbungeemain.commands.links.*;
import com.lifeknight.relaymcbungeemain.commands.staff.*;
import com.lifeknight.relaymcbungeemain.commands.staff.punishments.*;
import com.lifeknight.relaymcbungeemain.network.MessageServer;
import com.lifeknight.relaymcbungeemain.network.MessageServerHandler;
import com.lifeknight.relaymcbungeemain.network.ServerMessageProcessor;
import com.lifeknight.relaymcbungeemain.player.SmartPlayer;
import com.lifeknight.relaymcbungeemain.queue.GameServer;
import com.lifeknight.relaymcbungeemain.queue.Queue;
import com.lifeknight.relaymcbungeemain.queue.ServerDetails;
import com.lifeknight.relaymcbungeemain.utilities.*;
import com.lifeknight.relayutils.RelayUtils;
import com.lifeknight.relayutils.basic.Miscellaneous;
import com.lifeknight.relayutils.basic.Text;
import com.lifeknight.relayutils.data.QueueData;
import com.lifeknight.relayutils.game.GameDetails;
import com.lifeknight.relayutils.game.GameType;
import com.lifeknight.relayutils.game.MainGame;
import com.lifeknight.relayutils.network.Command;
import com.lifeknight.relayutils.network.Message;
import com.lifeknight.relayutils.network.Recipient;
import com.lifeknight.relayutils.player.data.SmartDatabase;
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.ProxyServer;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.config.ServerInfo;
import net.md_5.bungee.api.event.PluginMessageEvent;
import net.md_5.bungee.api.plugin.Listener;
import net.md_5.bungee.api.plugin.Plugin;
import net.md_5.bungee.api.plugin.PluginManager;
import net.md_5.bungee.chat.ComponentSerializer;
import net.md_5.bungee.config.Configuration;
import net.md_5.bungee.config.ConfigurationProvider;
import net.md_5.bungee.config.YamlConfiguration;
import net.md_5.bungee.event.EventHandler;

import java.io.*;
import java.lang.instrument.Instrumentation;
import java.util.*;
import java.util.concurrent.TimeUnit;

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

public final class Main extends Plugin implements Listener {
    public static Main instance;
    public static final String CHANNEL = "relay:mc";
    public static int uniquePlayers = 0;
    private static int tickCount = 0;
    private static boolean enabled;

    private final static String CONFIG_FILE = "relaymc.yml";

    public static final long START_OF_2021 = 1609459200000L;
    private static final long SUNDAY_3_13_2022_4_AM = 1647158400000L;
    private static final long DATE_10_1_2023_4AM = 1696147200000L;

    public static final String DISCORD_URL = "https://discord.gg/tsMyACa4Yj";

    public static final List<Map<Long, Integer>> playerCountLog = new ArrayList<>();
    public static final Map<Long, Integer> onlineLog = new HashMap<>();

    private static long nextRestart = SUNDAY_3_13_2022_4_AM;
    private static long restartTime = Long.MIN_VALUE;
    private static boolean restartMessageSent = false;

    public static boolean isRestarting = false;

    public static boolean maintenanceMode = true;
    public static boolean open = true;
    public static List<UUID> whitelistedPlayers = new ArrayList<>();

    private static final Map<MainGame, File> MAIN_GAME_TO_PLUGIN = new HashMap<>();

    private static long lastDailyReset = 0L;
    private static long lastWeeklyReset = 0L;
    private static long lastMonthlyReset = 0L;

    public static boolean queueOpen = true;
    public static String queueCloseMessage;

    public static int maxPlayers = 100;

    public static int infoMessageDelay = 15;
    private static BaseComponent[] latestInfoMessage = null;
    private static List<BaseComponent[]> infoMessages = Miscellaneous.processList(Miscellaneous.getList(
            RelayUtils.format("%sFound a bug with the server? Use %s/bugreport %sor join our Discord server and report it!", RED, DARK_PURPLE, RED),
            RelayUtils.format("%sHave any ideas for new minigames, or suggestions for the server? Join our discord with %s/discord %sand put it in #suggestions!", GREEN, DARK_GREEN, GREEN),
            RelayUtils.format("%sHave any questions about the server? Use %s/discord %sto join our Discord server and ask our staff.", YELLOW, DARK_PURPLE, YELLOW),
            RelayUtils.format("%sCaught a player who appears to be cheating? %sUse %s/DISCORD %sto join our Discord server to report them or use %s/report!", RED, YELLOW, DARK_PURPLE, YELLOW, RED),
            RelayUtils.format("%sCurious on how Manhunt has been enhanced on Relay? Join our Discord server with %s/discord %sto see!", YELLOW, DARK_PURPLE, YELLOW),
            RelayUtils.format("%sBuy ranks on our store (/store) to help us get more servers and expand our network!", YELLOW),
            RelayUtils.format("%sHelp our server grow by voting (/vote)!", GREEN)), s -> new BaseComponent[]{new TextComponent(s)});

    public static long gamesPlayed = 0;
    public static long peakPlayers = 0;
    public static int newPlayers = 0;

    public static final List<Task> SCHEDULED_TASKS = new ArrayList<>();

    private static File file;
    private static Configuration bungeeConfig;
    private static volatile Instrumentation globalInstrumentation;
    private static Map<GameType, Integer> gamesPlayedMap = new HashMap<>();

    public static void premain(final String agentArgs, final Instrumentation inst) {
        globalInstrumentation = inst;
    }

    public static Instrumentation getInstrumentation() {
        return globalInstrumentation;
    }

    public static int getOnNetworkPlayerCount() {
        int i = 0;
        for (SmartPlayer onlineSmartPlayer : SmartPlayer.getOnlineSmartPlayers()) {
            if (onlineSmartPlayer.getServer() != null && !onlineSmartPlayer.getServer().isLimbo()) {
                i++;
            }
        }

        return i;
    }

    public static int getOnlineCount() {
        return SmartPlayer.getOnlineSmartPlayers().size();
    }

    public static void onGamePlayed(GameType gameType1) {
        gamesPlayed++;
        gamesPlayedMap.put(gameType1, gamesPlayedMap.getOrDefault(gameType1, 0) + 1);
    }

    @Override
    public void onEnable() {
        instance = this;
        enabled = true;
        RelayUtils.setName("Proxy");
        RelayUtils.setLogger(this.getLogger());

        PluginManager pluginManager = this.getProxy().getPluginManager();

        pluginManager.registerCommand(this, new RelayMCCommand());
        pluginManager.registerCommand(this, new MaintenanceCommand());
        pluginManager.registerCommand(this, new BroadcastCommand());
        pluginManager.registerCommand(this, new SudoCommand());
        pluginManager.registerCommand(this, new GroupCommand());
        pluginManager.registerCommand(this, new PurchaseCommand());
        pluginManager.registerCommand(this, new CreditsCommand());
        pluginManager.registerCommand(this, new MessagesCommand());
        pluginManager.registerCommand(this, new ManhuntQuitsCommand());

        pluginManager.registerCommand(this, new ServersCommand());
        pluginManager.registerCommand(this, new PunishmentsCommand());
        pluginManager.registerCommand(this, new IPBanCommand());
        pluginManager.registerCommand(this, new BanCommand());
        pluginManager.registerCommand(this, new MuteCommand());
        pluginManager.registerCommand(this, new WarnCommand());

        pluginManager.registerCommand(this, new TempBanCommand());
        pluginManager.registerCommand(this, new TempMuteCommand());
        pluginManager.registerCommand(this, new TempWarnCommand());

        pluginManager.registerCommand(this, new UnBanCommand());
        pluginManager.registerCommand(this, new UnMuteCommand());

        pluginManager.registerCommand(this, new ReportCommand());
        pluginManager.registerCommand(this, new BugReportCommand());
        pluginManager.registerCommand(this, new ReportsCommand());

        pluginManager.registerCommand(this, new HelpCommand());
        pluginManager.registerCommand(this, new StoreCommand());
        pluginManager.registerCommand(this, new PremiumCommand());
        pluginManager.registerCommand(this, new DiscordCommand());
        pluginManager.registerCommand(this, new YouTubeCommand());
        pluginManager.registerCommand(this, new TwitterCommand());
        pluginManager.registerCommand(this, new VoteCommand());

        pluginManager.registerCommand(this, new WhereAmICommand());
        pluginManager.registerCommand(this, new PingCommand());

        pluginManager.registerCommand(this, new PlayCommand());
        pluginManager.registerCommand(this, new CancelCommand());
        pluginManager.registerCommand(this, new RejoinCommand());
        pluginManager.registerCommand(this, new DuelAcceptCommand());
        pluginManager.registerCommand(this, new ManhuntAcceptCommand());
        pluginManager.registerCommand(this, new ManhuntCommand());

        pluginManager.registerCommand(this, new HubCommand());

        pluginManager.registerCommand(this, new FriendCommand());
        pluginManager.registerCommand(this, new FriendListCommand());
        pluginManager.registerCommand(this, new BlockCommand());
        pluginManager.registerCommand(this, new UnblockCommand());
        pluginManager.registerCommand(this, new MessageCommand());
        pluginManager.registerCommand(this, new ReplyCommand());

        pluginManager.registerCommand(this, new PartyCommand());
        pluginManager.registerCommand(this, new PartyListCommand());

        pluginManager.registerCommand(this, new ChatCommand());
        pluginManager.registerCommand(this, new AllChatCommand());
        pluginManager.registerCommand(this, new PartyChatCommand());

        pluginManager.registerCommand(this, new StaffChatCommand());

        pluginManager.registerCommand(this, new SpectateCommand());
        pluginManager.registerCommand(this, new NicknameCommand());

        pluginManager.registerListener(this, this);
        pluginManager.registerListener(this, new Listeners());

        this.getProxy().registerChannel(CHANNEL);

        this.readConfigurationFile();
        this.saveConfigurationFile();
        schedule(() -> {
            setupConfig();
            MessageServer.start(RelayUtils.getPort());
        }, 5);
        SmartDatabase.setup();
        PlayerUtilities.startup();
        BotUtils.startup();

        RelayUtils.prepareProfileDatabase();
        RelayUtils.prepareAllStatisticsTables();
        RelayUtils.prepareLeaderboard();
        RelayUtils.prepareGamePlayerDatabase();
        RelayUtils.prepareArenaDatabase();
        RelayUtils.prepareParkourTables();
        RelayUtils.prepareCustomKitsLeaderboard();
        RelayUtils.prepareCustomMapsLeaderboard();

        updateNextRestartTime();

        async(() -> {
            while (enabled) {
                try {
                    this.onTick();
                } catch (Exception e) {
                    RelayUtils.printError("on tick", e);
                }
                if (enabled) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException exception) {
                        error("Tried to sleep thread, error occurred: %s", exception.getMessage());
                    }
                }
            }
        });

        for (GameType ignored : Miscellaneous.filter(GameType.getGameTypes(), GameType::isEnabled)) {
            playerCountLog.add(new HashMap<>());
        }

        SmartPlayer.getOrCreate("lifeknight");
    }

    public static void addInfoMessage(BaseComponent[] components) {
        infoMessages.add(components);
        save();
    }

    public void readConfigurationFile() {
        try {
            File dataFolder = this.getDataFolder();
            if (!dataFolder.exists()) {
                dataFolder.mkdir();
            }

            File fileConfiguration = new File(dataFolder, CONFIG_FILE);
            ConfigurationProvider configurationProvider = ConfigurationProvider.getProvider(YamlConfiguration.class);

            if (!fileConfiguration.exists()) {
                createPlaceholderServerList();
                this.saveConfigurationFile();
            } else {
                Configuration configuration = configurationProvider.load(fileConfiguration);
                RelayUtils.testing = configuration.getBoolean("testing");
                try {
                    infoMessages = Miscellaneous.processList(
                            configuration.getStringList("infoMessages"),
                            s -> ComponentSerializer.parse(ChatColor.translateAlternateColorCodes('&', s))
                    );
                } catch (Exception exception) {
                    error("An error occurred while reading info messages: %s", exception.getMessage());
                }

                infoMessageDelay = configuration.getInt("infoMessageDelay", infoMessageDelay);
                lastMonthlyReset = configuration.getLong("lastMonthlyReset", System.currentTimeMillis());
                lastWeeklyReset = configuration.getLong("lastWeeklyReset", System.currentTimeMillis());
                lastDailyReset = configuration.getLong("lastDailyReset", System.currentTimeMillis());
                setLastResetTimes();
                maxPlayers = configuration.getInt("maxPlayers", maxPlayers);
                maintenanceMode = configuration.getBoolean("maintenance", true);
                whitelistedPlayers = Miscellaneous.processList(configuration.getStringList("whitelist"), RelayUtils::getUUID);
                Server.clear();
                for (MainGame value : MainGame.values()) {
                    List<String> hubNames = configuration.get(value.getComparableName() + "Hubs", new ArrayList<>());
                    List<String> serverNames = configuration.get(value.getComparableName() + "Servers", new ArrayList<>());
                    for (String hubName : hubNames) {
                        Server.create(hubName, value, ServerType.HUB);
                    }
                    for (String serverName : serverNames) {
                        Server.create(serverName, value, ServerType.GAME);
                    }
                }

                List<String> mainHubNames = configuration.get("mainHubs", new ArrayList<>());
                for (String mainHubName : mainHubNames) {
                    Server.create(mainHubName, null, ServerType.HUB);
                }

                List<String> limboNames = configuration.get("limbos", new ArrayList<>());
                for (String limboName : limboNames) {
                    Server.create(limboName, null, ServerType.LIMBO);
                }
            }

            for (File file : dataFolder.listFiles((dir, name) -> name.contains(".jar"))) {
                for (MainGame mainGame : MainGame.values()) {
                    if (Text.toComparable(file.getName()).contains(mainGame.getComparableName())) {
                        if (MAIN_GAME_TO_PLUGIN.containsKey(mainGame)) {

                            File current = MAIN_GAME_TO_PLUGIN.get(mainGame);
                            Version currentV = Utilities.parseVersion(current.getName());
                            Version newV = Utilities.parseVersion(file.getName());

                            if (newV.isGreater(currentV)) {
                                info("Remapping %s with %s.", file.getName(), mainGame.getComparableName());
                                MAIN_GAME_TO_PLUGIN.put(mainGame, file);
                            }
                        } else {
                            info("Mapping %s with %s.", file.getName(), mainGame.getComparableName());
                            MAIN_GAME_TO_PLUGIN.put(mainGame, file);
                        }
                        break;
                    }
                }

                if (!MAIN_GAME_TO_PLUGIN.containsValue(file)) {
                    info("Setting %s as hub plugin.", file.getName());
                    MAIN_GAME_TO_PLUGIN.put(null, file);
                }
            }
        } catch (Exception exception) {
            error("An error occurred in the reading of the configuration file: %s", exception.getMessage());
            this.saveConfigurationFile();
        }

        updatePlugins();
    }

    private void setLastResetTimes() {
        lastDailyReset = SUNDAY_3_13_2022_4_AM;
        while (lastDailyReset + 24 * 60 * 60 * 1000L < System.currentTimeMillis()) {
            lastDailyReset += 24 * 60 * 60 * 1000L;
        }
        lastWeeklyReset = SUNDAY_3_13_2022_4_AM;
        while (lastWeeklyReset + 7 * 24 * 60 * 60 * 1000L < System.currentTimeMillis()) {
            lastWeeklyReset += 7 * 24 * 60 * 60 * 1000L;
        }
        lastMonthlyReset = DATE_10_1_2023_4AM;
        while (lastMonthlyReset + 30 * 24 * 60 * 60 * 1000L < System.currentTimeMillis()) {
            lastMonthlyReset += 30 * 24 * 60 * 60 * 1000L;
        }
    }

    public static void reload() {
        instance.readConfigurationFile();
    }

    public static void save() {
        instance.saveConfigurationFile();
    }

    public void saveConfigurationFile() {
        try {
            File dataFolder = this.getDataFolder();
            if (!dataFolder.exists()) {
                dataFolder.mkdir();
            }

            File fileConfiguration = new File(dataFolder, CONFIG_FILE);
            ConfigurationProvider configurationProvider = ConfigurationProvider.getProvider(YamlConfiguration.class);

            if (!fileConfiguration.exists()) {
                fileConfiguration.createNewFile();
            }

            Configuration configuration = configurationProvider.load(fileConfiguration);
            configuration.set("port", RelayUtils.PROXY_MESSAGE_SERVER_PORT);
            configuration.set("testing", RelayUtils.testing);
            configuration.set("infoMessages",
                    Miscellaneous.processList(infoMessages, ComponentSerializer::toString));
            configuration.set("infoMessageDelay", infoMessageDelay);
            configuration.set("lastDailyReset", lastDailyReset);
            configuration.set("lastWeeklyReset", lastWeeklyReset);
            configuration.set("lastMonthlyReset", lastMonthlyReset);
            configuration.set("maxPlayers", maxPlayers);
            configuration.set("whitelist", Miscellaneous.processList(whitelistedPlayers, RelayUtils::getName));

            for (MainGame value : MainGame.values()) {
                configuration.set(value.getComparableName() + "Hubs", Miscellaneous.processList(Server.getServersOfType(value, false), Server::getName));
                configuration.set(value.getComparableName() + "Servers", Miscellaneous.processList(Server.getServersOfType(value, true), Server::getName));
            }

            configuration.set("mainHubs", Miscellaneous.processList(Server.getServersOfType(null, false), Server::getName));
            configuration.set("limbos", Miscellaneous.processList(Server.getLimboServers(), Server::getName));
            configurationProvider.save(configuration, fileConfiguration);
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    @Override
    public void onDisable() {
        enabled = false;
        this.getProxy().unregisterChannel(CHANNEL);
        this.logPlayerCounts();
        MessageServer.end();
        SmartDatabase.shutdown();
        BotUtils.shutdown();
        info("Disabled.");
    }

    @EventHandler
    public void onPluginMessageReceived(PluginMessageEvent event) {
        if (event.getTag().equalsIgnoreCase(CHANNEL)) {
            try {
                ByteArrayDataInput in = ByteStreams.newDataInput(event.getData());
                Message message = Message.fromJson(RelayUtils.parseJsonArray(in.readUTF()));
                onMessageReceived(message);
            } catch (Exception exception) {
                error("An error occurred while trying to process message: %s", exception.getMessage());
            }
        }
    }

    public static void onMessageReceived(Message message) {
        Recipient recipient = message.getRecipientRecipient();
        if (!(Recipient.PROXY == recipient || Recipient.NONE == recipient)) {
            try {
                MessageServerHandler.sendMessageToServer(message);
            } catch (Exception exception) {
                error("Attempted to forward a message to a server, no server found (%s): %s", MessageServerHandler.CHANNELS.keySet(), message);
            }
        } else {
            try {
                ServerMessageProcessor.processMessage(message);
            } catch (Exception exception) {
                RelayUtils.printError("process message", exception, 8);
            }
        }
    }

    public void onTick() {
        log("on tick");
        tickCount++;

        if (restartTime != Long.MIN_VALUE && System.currentTimeMillis() >= restartTime && !isRestarting) {
            restart();
            return;
        }

        if (isOfSeconds(10)) {
            broadcastPlayerCounts();
            broadcastHubs();
        }

        if (!RelayUtils.testing && isOfSeconds(5)) {
            BotUtils.updatePlayerCount(isOfSeconds(10));
        }

        if (isOfSeconds(1)) {
            broadcastPing();
            disconnectInactive();

            boolean update = false;
            if (lastDailyReset != 0 && System.currentTimeMillis() - lastDailyReset >= getTimeRequired(0)) {
                update = true;
                this.resetLeaderboard(0);
            }

            if (lastWeeklyReset != 0 && System.currentTimeMillis() - lastWeeklyReset >= getTimeRequired(1)) {
                update = true;
                this.resetLeaderboard(1);
            }

            if (lastMonthlyReset != 0 && System.currentTimeMillis() - lastMonthlyReset >= getTimeRequired(2)) {
                update = true;
                this.resetLeaderboard(2);
            }


            if (update) {
                RelayUtils.prepareLeaderboard();
                RelayUtils.prepareCustomKitsLeaderboard();
                RelayUtils.prepareCustomMapsLeaderboard();
            }
        }

        if (isOfSeconds(180)) {
            sendStatsMessage();
        }

        if (tickCount % (infoMessageDelay * 60 * 20) == 0) {
            synchronous(Main::broadcastInfoMessage);
        }

        if (Miscellaneous.isWithinRange(nextRestart, System.currentTimeMillis(), 30 * 60 * 1000L) && !restartMessageSent) {
            Main.info("Scheduling restart for normal restarts.");
            scheduleRestart();
        } else if (Miscellaneous.getRemainingMemoryMegaBytes() <= 64 && !restartMessageSent) {
            warn("Scheduling restart. (Remaining memory: %d MB)", Miscellaneous.getRemainingMemoryMegaBytes());
            scheduleRestart(10 * 60 * 1000L);
        }

        SmartPlayer.onTick();
        runScheduledTasks();
    }

    private void runScheduledTasks() {
        List<Integer> delete = new ArrayList<>();
        SCHEDULED_TASKS.removeIf(Objects::isNull);

        for (int i = 0; i < SCHEDULED_TASKS.size(); i++) {
            Task scheduledTask = SCHEDULED_TASKS.get(i);
            try {
                if (System.currentTimeMillis() - scheduledTask.initialize >= scheduledTask.waitTime) {
                    delete.add(i);
                    scheduledTask.runnable.run();
                }
            } catch (Exception e) {
                RelayUtils.printError("run a schedule task", e, 5);
                BotUtils.onScheduledTaskError(e);
            }
        }

        for (Integer integer : Miscellaneous.reverse(delete)) {
            SCHEDULED_TASKS.remove((int) integer);
        }
    }

    private void logPlayerCounts() {
        List<GameType> gameTypes = Miscellaneous.filter(GameType.getGameTypes(), GameType::isEnabled);
        long time = System.currentTimeMillis();
        for (int i = 0; i < gameTypes.size(); i++) {
            playerCountLog.get(i).put(time, getPlayerCount(gameTypes.get(i)));
        }
        onlineLog.put(time, SmartPlayer.getOnlineSmartPlayers().size());
        updatePlayerCountLog();
    }

    public static void updatePlayerCountLog() {
        Map<Long, Integer> totals = new HashMap<>();
        for (int j = 0; j < playerCountLog.get(0).size(); j++) {

            for (Map<Long, Integer> longIntegerMap : playerCountLog) {
                Map.Entry<Long, Integer> val = Miscellaneous.getList(longIntegerMap.entrySet()).get(j);
                totals.put(val.getKey(), totals.getOrDefault(val.getKey(), 0) + val.getValue());
            }
        }

        StringBuilder csv = new StringBuilder();
        csv.append("Time (epoch),Online,In-Game");

        for (GameType gameType : Miscellaneous.filter(GameType.getGameTypes(), GameType::isEnabled)) {
            csv.append(",").append(gameType.getFullPrettyName());
        }

        csv.append("\n");

        for (Long aLong : totals.keySet()) {
            csv.append(aLong).append(",");
            csv.append(onlineLog.get(aLong)).append(",");
            csv.append(totals.get(aLong));
            for (Map<Long, Integer> longIntegerMap : playerCountLog) {
                csv.append(",");
                csv.append(longIntegerMap.get(aLong));
            }
            csv.append("\n");
        }

        try {
            File folder = new File(instance.getDataFolder(), "/logs/playercounts");
            if (!folder.exists()) {
                folder.mkdirs();
            }

            File file1 = new File(folder, getDateTimeString(RelayUtils.INITIALIZATION_TIME).replace(":", "-").replace("/", "-").replace(" ", "-") + ".txt");

            if (!file1.exists()) {
                file1.createNewFile();
            }
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file1)));

            writer.write(csv.toString());

            writer.close();
        } catch (Exception exception) {
            error("Could not write to log file: %s", exception.getMessage());
        }

        try {
            File folder = new File(instance.getDataFolder(), "/logs/reports");
            if (!folder.exists()) {
                folder.mkdirs();
            }

            File file1 = new File(folder, "Reports " + getDateTimeString(RelayUtils.INITIALIZATION_TIME).replace(":", "-").replace("/", "-").replace(" ", "-") + ".txt");

            if (!file1.exists()) {
                file1.createNewFile();
            }
            PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file1)));

            writer.write(getReport());

            writer.close();
        } catch (Exception exception) {
            error("Could not write to log file (2): %s", exception.getMessage());
        }
    }

    private static String getReport() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("Uptime: ").append(Text.getShortTextualFormattedTime(System.currentTimeMillis() - RelayUtils.INITIALIZATION_TIME)).append("\n");
        stringBuilder.append("Peak Players: ").append(peakPlayers).append("\n");
        stringBuilder.append("Unique Players: ").append(uniquePlayers).append("\n");
        stringBuilder.append("New Players: ").append(newPlayers).append("\n");
        stringBuilder.append("Games Played: ").append("\n");
        stringBuilder.append("Total: ").append(gamesPlayed).append("\n");
        for (Map.Entry<GameType, Integer> gameTypeIntegerEntry : gamesPlayedMap.entrySet()) {
            stringBuilder.append(" - ").append(gameTypeIntegerEntry.getKey().getFullPrettyName()).append(": ").append(gameTypeIntegerEntry.getValue()).append("\n");
        }

        return stringBuilder.toString();
    }

    public static String getDateTimeString(long milliseconds) {
        return getSimpleDateFormat("MM/dd/yyyy HH:mm:ss").format(milliseconds) + " EST";
    }

    private int getPlayerCount(GameType gameType) {
        int count = 0;
        for (GameServer gameServer : GameServer.GAME_SERVERS) {
            ServerDetails details = gameServer.getServerDetails();
            if (details == null) {
                warn("Details were null: %s", gameServer.getRecipient());
            } else {
                for (GameDetails gameDetail : details.gameDetails) {
                    if (gameDetail != null && gameDetail.getGameType() == gameType) {
                        count += gameDetail.getPlayerCount();
                    }
                }
            }
        }

        return count;
    }

    public static void scheduleRestart(long time) {
        ComponentBuilder componentBuilder = new ComponentBuilder(ComponentBuilder.RED).line().newLine().color(ComponentBuilder.YELLOW);
        componentBuilder.append("The network will be restarting in ").color(ComponentBuilder.RED_ORANGE).append(Text.formatTimeFromMilliseconds(time)).color(ComponentBuilder.YELLOW).append(" to improve performance.").newLine();
        componentBuilder.color(ComponentBuilder.RED).line();
        queueOpen = false;
        queueCloseMessage = RED + "The network will be restarting at " + Text.getTimeString(System.currentTimeMillis() + time);
        restartMessageSent = true;
        restartTime = System.currentTimeMillis() + time;
    }

    public static void restart() {
        info("---------------\nRESTARTING SERVER\n---------------");
        isRestarting = true;
        for (SmartPlayer onlineSmartPlayer : SmartPlayer.getOnlineSmartPlayers()) {
            if (!onlineSmartPlayer.isStaff()) {
                onlineSmartPlayer.kick("The network is restarting. Try joining in 60 seconds.");
            }
        }
        schedule(RelayUtils::restart, 10);
    }

    public static void scheduleRestart() {
        scheduleRestart(30 * 60 * 1000L);
    }

    public static void sendStatsMessage(CommandSender commandSender) {
        ComponentBuilder componentBuilder = new ComponentBuilder();
        componentBuilder.color(ComponentBuilder.GREEN);
        componentBuilder.append("Up Since: %s (%s)", Text.getDateTimeString(RelayUtils.INITIALIZATION_TIME),
                Text.getShortTextualFormattedTime(System.currentTimeMillis() - RelayUtils.INITIALIZATION_TIME)).newLine();
        long restart = restartMessageSent ? restartTime : nextRestart;
        componentBuilder.append("Restart time: %s (in %s)", Text.getDateTimeString(restart), Text.getShortTextualFormattedTime(restart - System.currentTimeMillis())).newLine();
        componentBuilder.append("SmartPlayer count: %d", SmartPlayer.SMART_PLAYERS.size()).newLine();
        componentBuilder.append("Unique Players: %d", uniquePlayers).newLine();
        componentBuilder.append("New Players: %d", newPlayers).newLine();
        componentBuilder.append("Online: %d", SmartPlayer.getOnlineSmartPlayers().size()).newLine();
        componentBuilder.append("In Game: %d", GameServer.getPlayersInGame()).newLine();
        componentBuilder.append("Games Played: %d", gamesPlayed).newLine();
        componentBuilder.append("Peak Players: %d", peakPlayers).newLine();
        componentBuilder.append("MB of memory: %d", Miscellaneous.getRemainingMemoryMegaBytes()).newLine();
        commandSender.sendMessage(componentBuilder.getResult());
    }

    public static void sendStatsMessage() {
        sendStatsMessage(instance.getProxy().getConsole());
    }

    public static void open() {
        open = true;
    }

    public static void async(Runnable runnable) {
        log("async");
        ProxyServer.getInstance().getScheduler().runAsync(instance, runnable);
    }

    public static File getPlugin(Server server) {
        if (server == null) return null;

        if (server.isHub() || server.isLimbo()) {
            return MAIN_GAME_TO_PLUGIN.get(null);
        }

        return MAIN_GAME_TO_PLUGIN.get(server.getMainGame());
    }

    public static void restartGameServers() {
        for (Server gameServer : Server.getGameServers()) {
            if (gameServer.isActive() && gameServer.isEmpty()) {
                gameServer.sendMessage(Command.RESTART);
            }
        }

        info("Restarted empty game servers.");
    }

    public static void restartHubs() {
        for (Server hubServer : Server.getHubServers()) {
            if (hubServer.isActive() && hubServer.isEmpty()) {
                hubServer.sendMessage(Command.RESTART);
            }
        }

        info("Restarted empty hubs.");
    }

    public static void startServers() {
        for (Server server : Miscellaneous.with(Server.getGameServers(), Server.getHubServers())) {
            if (!server.isActive()) {
                server.start();
            }
        }
    }

    public static boolean isSecond() {
        return isOfSeconds(1);
    }

    public static boolean isOfSeconds(int seconds) {
        return tickCount % (20 * seconds) == 0;
    }

    public static void updateNextRestartTime() {
        int iteration = 0;
        while (nextRestart < System.currentTimeMillis()) {
            switch (iteration) {
                case 0:
                case 2:
                    nextRestart += 2 * 24 * 60 * 60 * 1000L;
                    break;
                case 1:
                    nextRestart += 3 * 24 * 60 * 60 * 1000L;
                    break;
            }

            if (iteration == 2) {
                iteration = 0;
            } else {
                iteration++;
            }
        }
    }

    public void resetLeaderboard(int i) {
        String leaderboardName = "";
        if (i == 1) {
            leaderboardName = "weekly";
            lastWeeklyReset = System.currentTimeMillis();
        } else if (i == 0) {
            leaderboardName = "daily";
            lastDailyReset = System.currentTimeMillis();
        } else if (i == 2) {
            leaderboardName = "monthly";
            lastMonthlyReset = System.currentTimeMillis();
        }

        for (String leaderboard : Miscellaneous.getList("leaderboard", SmartDatabase.CUSTOM_MAP_TABLE_NAME, SmartDatabase.CUSTOM_KIT_TABLE_NAME)) {
            SmartDatabase.executeUpdate("TRUNCATE `%s`", leaderboard + "_" + leaderboardName);
        }

        schedule(() -> Utilities.broadcast("%s%s leaderboards %s%shave been reset!", GREEN, i == 1 ? "Weekly" : (i == 0 ? "Daily" : "Monthly"), AQUA, BOLD), 60);

        for (Server gameServer : Server.getGameServers()) {
            if (gameServer.isActive()) {
                gameServer.sendMessage(Command.OTHER, "leaderboard_reset", "leaderboard_" + leaderboardName);
            }
        }

        this.saveConfigurationFile();
    }

    public static long getTimeRequired(int i) {
        if (i == 0) {
            return 24 * 60 * 60 * 1000L;
        }
        if (i == 2) {
            return 30 * 24 * 60 * 60 * 1000L;
        }
        return 7 * 24 * 60 * 60 * 1000L;
    }


    public static void info(String format, Object... data) {
        instance.getLogger().info(RelayUtils.format(format, data));
    }

    public static void warn(String format, Object... data) {
        instance.getLogger().warning(RelayUtils.format(format, data));
    }

    public static void error(String format, Object... data) {
        instance.getLogger().severe(RelayUtils.format(format, data));
    }

    public static void log(String format, Object... data) {
        RelayUtils.log(format, data);
    }

    public static void schedule(Runnable runnable, long seconds) {
        schedule(runnable, seconds, TimeUnit.SECONDS);
    }

    public static void scheduleMillis(Runnable runnable, long milliseconds) {
        schedule(runnable, milliseconds, TimeUnit.MILLISECONDS);
    }

    public static void schedule(Runnable runnable, long time, TimeUnit timeUnit) {
        SCHEDULED_TASKS.add(new Task(runnable, System.currentTimeMillis(), timeUnit.toMillis(time)));
/*
        ProxyServer.getInstance().getScheduler().schedule(instance, () -> {

        }, time, timeUnit);
*/
    }

    public static void synchronous(Runnable runnable) {
        schedule(runnable, 0);
    }

    public static void broadcastPlayerCounts() {
        Map<GameType, Integer> gameTypeToCount = new HashMap<>();
        for (GameType gameType : GameType.getGameTypes(false, false)) {
            gameTypeToCount.put(gameType, 0);
        }

        for (GameServer gameServer : GameServer.GAME_SERVERS) {
            ServerDetails details = gameServer.getServerDetails();
            if (details == null) {
                warn("Details were null: %s", gameServer.getRecipient());
                return;
            }

            for (GameDetails gameDetail : details.gameDetails) {
                if (gameDetail != null && gameDetail.getGameType() != null) {
                    int currentCount = gameTypeToCount.get(gameDetail.getGameType());
                    gameTypeToCount.put(gameDetail.getGameType(), currentCount + gameDetail.getPlayerCount());
                }
            }
        }

        for (GameType gameType : GameType.getGameTypes(false, false)) {
            int count = gameTypeToCount.get(gameType);
            gameTypeToCount.put(gameType, count + Queue.getQueueSize(gameType));
        }

        StringBuilder stringBuilder = new StringBuilder();

        for (GameType gameType : gameTypeToCount.keySet()) {
            stringBuilder.append(gameType.toString()).append(":").append(gameTypeToCount.get(gameType)).append(",");
        }

        sendMessageToHubs(new Message(Command.PLAYER_COUNT, stringBuilder.toString()));
    }

    public static void broadcastHubs() {
        JsonArray jsonArray = new JsonArray();
        for (MainGame mainGame : Miscellaneous.with(Miscellaneous.getList(MainGame.values()), (MainGame) null)) {
            JsonObject hubGroup = new JsonObject();
            hubGroup.addProperty("type", mainGame == null ? "main" : mainGame.getComparableName());
            JsonArray hubsOfType = new JsonArray();
            for (Server server : Server.getAvailableServersOfType(mainGame, false)) {
                JsonObject hub = new JsonObject();
                hub.addProperty("name", server.getName());
                hub.addProperty("count", server.getPlayerCount());
                hubsOfType.add(hub);
            }
            hubGroup.add("hubs", hubsOfType);
            jsonArray.add(hubGroup);
        }

        sendMessageToHubs(new Message(Command.HUBS, jsonArray));
    }

    public static void broadcastPublicQueues() {
        JsonObject games = new JsonObject();
        JsonArray currentGames = new JsonArray();
        JsonArray queuedGames = new JsonArray();

        for (QueueData customKitsDatum : GameServer.getGameData()) {
            currentGames.add(customKitsDatum.toString());
        }

        for (QueueData customKitsDatum : Queue.getQueuedGames()) {
            queuedGames.add(customKitsDatum.toString());
        }

        games.add("current", currentGames);
        games.add("queued", queuedGames);

        sendMessageToHubs(new Message(Command.OTHER, "duelsgames", games));
    }

    public static void broadcastPing() {
        sendMessageToGameServers(new Message(Command.OTHER, "ping"));
        sendMessageToHubs(new Message(Command.OTHER, "ping"));
    }

    public static void disconnectInactive() {
        for (MessageServerHandler messageServerHandler : getMessageServerHandlers()) {
            if (Miscellaneous.enoughTimeHasPassed(messageServerHandler.getLastPing(), 3, TimeUnit.SECONDS)) {
                messageServerHandler.closeConnection();
            }
        }
    }

    public static void broadcastInfoMessage() {
        BaseComponent[] next = Miscellaneous.getNextIndex(infoMessages, latestInfoMessage);

        if (next != null) {
            for (SmartPlayer onlineSmartPlayer : SmartPlayer.getOnlineSmartPlayers()) {
                if (onlineSmartPlayer.isOnHub()) {
                    onlineSmartPlayer.sendMessage(next);
                }
            }
        }

        latestInfoMessage = next;
    }

    public static void setMaintenanceMode(boolean mode) {
        maintenanceMode = mode;

        if (maintenanceMode) {
            ComponentBuilder componentBuilder = new ComponentBuilder();
            componentBuilder.color(ComponentBuilder.YELLOW);
            componentBuilder.line().newLine();
            componentBuilder.color(ComponentBuilder.RED_ORANGE).append("Server is entering").color(ComponentBuilder.ORANGE_YELLOW).bold().append(" MAINTENANCE MODE!").newLine();
            componentBuilder.color(ComponentBuilder.YELLOW).line();
            Utilities.broadcast(componentBuilder);

            schedule(() -> {
                ComponentBuilder kickMessage = new ComponentBuilder();
                kickMessage.color(ComponentBuilder.BLUE).append("Relay").newLine().newLine();
                kickMessage.color(ComponentBuilder.YELLOW).append("Relay has entered maintenance mode.").newLine();
                kickMessage.color(DARK_PURPLE).append("Discord: ").link(Main.DISCORD_URL, true);
                for (SmartPlayer onlineSmartPlayer : SmartPlayer.getOnlineSmartPlayers()) {
                    if (!(onlineSmartPlayer.isStaff() || whitelistedPlayers.contains(onlineSmartPlayer.getUUID()))) {
                        onlineSmartPlayer.kick(kickMessage);
                    }
                }
            }, 3);
        }
    }

    public static List<MessageServerHandler> getMessageServerHandlers() {
        List<MessageServerHandler> list = Miscellaneous.getList(MessageServerHandler.CHANNELS.values());
        list.removeIf(Objects::isNull);
        return list;
    }

    public static void updatePlugins() {
        List<MessageServerHandler> messageServerHandlers = getMessageServerHandlers();
        for (int i = 0; i < messageServerHandlers.size(); i++) {
            int finalI = i;
            scheduleMillis(() -> {
                MessageServerHandler messageServerHandler = messageServerHandlers.get(finalI);
                messageServerHandler.getPluginVersion();
            }, 500L * i);
        }
    }

    public static void sendMessageToHubs(Message message) {
        for (MessageServerHandler messageServerHandler : getMessageServerHandlers()) {
            if (messageServerHandler.hasServer() && messageServerHandler.getServer().isHub()) {
                messageServerHandler.sendMessage(message);
            }
        }
    }

    public static void sendMessageToGameServers(Message message) {
        for (MessageServerHandler messageServerHandler : getMessageServerHandlers()) {
            if (messageServerHandler.hasServer() && !messageServerHandler.getServer().isHub()) {
                messageServerHandler.sendMessage(message);
            }
        }
    }

    public static void createPlaceholderServerList() {
        createPlaceholderServers(null, "hub", 3, ServerType.HUB);
        createPlaceholderServers(null, "z", 3, ServerType.LIMBO);

        for (MainGame value : MainGame.values()) {
            String prefix = getPrefix(value.getName());
            createPlaceholderServers(value, prefix + "lobby", 3, ServerType.HUB);
            createPlaceholderServers(value, prefix, 10, ServerType.GAME);
        }
    }

    public static String getPrefix(String name) {
        StringBuilder stringBuilder = new StringBuilder();
        for (String s : name.split(" ")) {
            stringBuilder.append(Character.toLowerCase(s.charAt(0)));
        }

        return stringBuilder.toString();
    }

    public static void createPlaceholderServers(MainGame mainGame, String prefix, int count, ServerType serverType) {
        for (int i = 0; i < count; i++) {
            Server.create(prefix + get1000Number(i + 1), mainGame, serverType);
        }
    }

    public static String get1000Number(int number) {
        if (number < 10) {
            return "00" + number;
        }
        if (number < 100) {
            return "0" + number;
        }

        return String.valueOf(number);
    }

    public static void addToConfig(ServerInfo serverInfo) {
        if (bungeeConfig == null) {
            return;
        }

        bungeeConfig.set("servers." + serverInfo.getName() + ".motd", serverInfo.getMotd().replace(ChatColor.COLOR_CHAR, '&'));
        bungeeConfig.set("servers." + serverInfo.getName() + ".address", Utilities.socketAddressToString(serverInfo.getSocketAddress()));
        bungeeConfig.set("servers." + serverInfo.getName() + ".restricted", serverInfo.isRestricted());
        saveBungeeConfig();
    }

    public static void removeFromConfig(ServerInfo serverInfo) {
        removeFromConfig(serverInfo.getName());
    }

    public static void removeFromConfig(String name) {
        if (bungeeConfig == null) {
            return;
        }

        bungeeConfig.set("servers." + name, null);
        saveBungeeConfig();
    }

    private static void saveBungeeConfig() {
        if (bungeeConfig == null) {
            return;
        }

        try {
            YamlConfiguration.getProvider(YamlConfiguration.class).save(bungeeConfig, file);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void setupConfig() {
        FileInputStream fis = null;
        InputStreamReader isr = null;
        try {
            file = new File(ProxyServer.getInstance().getPluginsFolder().getParentFile(), "config.yml");

            fis = new FileInputStream(file);
            isr = new InputStreamReader(fis);

            bungeeConfig = YamlConfiguration.getProvider(YamlConfiguration.class).load(isr);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }

                if (isr != null) {
                    isr.close();
                }
            } catch (IOException ignored) {
            }
        }
    }

    public static boolean networkIsFull() {
        return networkIsFull(false);
    }

    public static boolean networkIsFull(boolean excludeOne) {
        if (excludeOne) {
            return Main.getOnNetworkPlayerCount() > Main.maxPlayers || Server.allHubsAreFull();
        }
        return Main.getOnNetworkPlayerCount() >= Main.maxPlayers || Server.allHubsAreFull();
    }

    public static void doAndLogDuration(String name, Runnable action) {
        long startTime = System.currentTimeMillis();
        action.run();
        if (RelayUtils.log) {
            info("Time taken for %s: %s", name, Text.addCommasToNumber(System.currentTimeMillis() - startTime));
        }
    }

    private record Task(Runnable runnable, long initialize, long waitTime) {

    }
}