From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: William Blake Galbreath <Blake.Galbreath@GMail.com>
Date: Sat, 12 Dec 2020 21:19:05 -0600
Subject: [PATCH] Implement TPSBar


diff --git a/src/main/java/net/minecraft/commands/Commands.java b/src/main/java/net/minecraft/commands/Commands.java
index a5712108fbbe5ddc61cdfd390f667ab013ba609d..73953ea69776bfe1dcb1504cd14a0f003f1b5766 100644
--- a/src/main/java/net/minecraft/commands/Commands.java
+++ b/src/main/java/net/minecraft/commands/Commands.java
@@ -201,6 +201,7 @@ public class Commands {
             net.pl3x.purpur.command.CreditsCommand.register(this.dispatcher); // Purpur
             net.pl3x.purpur.command.DemoCommand.register(this.dispatcher); // Purpur
             net.pl3x.purpur.command.PingCommand.register(this.dispatcher); // Purpur
+            net.pl3x.purpur.command.TPSBarCommand.register(this.dispatcher); // Purpur
         }
 
         if (environment.includeIntegrated) {
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 1622eedaaada332f589eddfdb1dae38387ce0a0c..a456c7f6eedb2cff1b58ec6f0ef330c74dfcdaed 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -1119,6 +1119,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
         this.safeShutdown(flag, false);
     }
     public void safeShutdown(boolean flag, boolean isRestarting) {
+        net.pl3x.purpur.task.BossBarTask.stopAll(); // Purpur
         this.isRestarting = isRestarting;
         this.hasLoggedStop = true; // Paper
         if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper
diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
index 5bb730ea49a5ecc5e2ded4fa8dec8c4656321f07..a36750cc08fbda22362211507a10f9e1d89e0a8a 100644
--- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java
@@ -351,6 +351,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
                 DedicatedServer.LOGGER.info("JMX monitoring enabled");
             }
 
+            net.pl3x.purpur.task.BossBarTask.startAll(); // Purpur
+
             return true;
         }
     }
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 8c93a84f9b44316fedc1544a309d27ef14e7d6d7..b09cd5f750b6789ac21ed7d585d72ad393141a97 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -260,6 +260,7 @@ public class ServerPlayer extends Player {
     public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper
     public boolean purpurClient = false; // Purpur
     public boolean acceptingResourcePack = false; // Purpur
+    private boolean tpsBar = false; // Purpur
 
     public double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks
     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
@@ -482,6 +483,7 @@ public class ServerPlayer extends Player {
             }
         }
 
+        if (nbt.contains("Purpur.TPSBar")) { this.tpsBar = nbt.getBoolean("Purpur.TPSBar"); } // Purpur
     }
 
     @Override
@@ -542,6 +544,7 @@ public class ServerPlayer extends Player {
         }
         this.getBukkitEntity().setExtraData(nbt); // CraftBukkit
 
+        nbt.putBoolean("Purpur.TPSBar", this.tpsBar); // Purpur
     }
 
     // CraftBukkit start - World fallback code, either respawn location or global spawn
@@ -2548,5 +2551,13 @@ public class ServerPlayer extends Player {
             this.server.getPlayerList().moveToWorld(this, toLevel, true, to, !toLevel.paperConfig.disableTeleportationSuffocationCheck);
         }
     }
+
+    public boolean tpsBar() {
+        return this.tpsBar;
+    }
+
+    public void tpsBar(boolean tpsBar) {
+        this.tpsBar = tpsBar;
+    }
     // Purpur end
 }
diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java
index 2381d75295554c0310e36a26f5332063b126adc9..a387b3b5aa510dcd99d71446d723ce79c82dad33 100644
--- a/src/main/java/net/minecraft/server/players/PlayerList.java
+++ b/src/main/java/net/minecraft/server/players/PlayerList.java
@@ -483,6 +483,7 @@ public abstract class PlayerList {
             scoreboard.addPlayerToTeam(player.getScoreboardName(), collideRuleTeam);
         }
         // Paper end
+        net.pl3x.purpur.task.BossBarTask.addToAll(player); // Purpur
         // CraftBukkit - Moved from above, added world
         PlayerList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", player.getName().getString(), s1, player.getId(), worldserver1.serverLevelData.getLevelName(), player.getX(), player.getY(), player.getZ());
     }
@@ -593,6 +594,8 @@ public abstract class PlayerList {
     }
     public net.kyori.adventure.text.Component disconnect(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) {
         // Paper end
+        net.pl3x.purpur.task.BossBarTask.removeFromAll(entityplayer.getBukkitEntity()); // Purpur
+
         ServerLevel worldserver = entityplayer.getLevel();
 
         entityplayer.awardStat(Stats.LEAVE_GAME);
diff --git a/src/main/java/net/pl3x/purpur/PurpurConfig.java b/src/main/java/net/pl3x/purpur/PurpurConfig.java
index ad4908c00ea98ea26aa211436c572b4ec04cd030..da4d5b05cf2daa31ecd70d2815017bbb6e771313 100644
--- a/src/main/java/net/pl3x/purpur/PurpurConfig.java
+++ b/src/main/java/net/pl3x/purpur/PurpurConfig.java
@@ -331,6 +331,29 @@ public class PurpurConfig {
         disableGiveCommandDrops = getBoolean("settings.disable-give-dropping", disableGiveCommandDrops);
     }
 
+    public static String commandTPSBarTitle = "<gray>TPS<yellow>:</yellow> <tps> MSPT<yellow>:</yellow> <mspt> Ping<yellow>:</yellow> <ping>ms";
+    public static BossBar.Overlay commandTPSBarProgressOverlay = BossBar.Overlay.NOTCHED_20;
+    public static TPSBarTask.FillMode commandTPSBarProgressFillMode = TPSBarTask.FillMode.MSPT;
+    public static BossBar.Color commandTPSBarProgressColorGood = BossBar.Color.GREEN;
+    public static BossBar.Color commandTPSBarProgressColorMedium = BossBar.Color.YELLOW;
+    public static BossBar.Color commandTPSBarProgressColorLow = BossBar.Color.RED;
+    public static String commandTPSBarTextColorGood = "<gradient:#55ff55:#00aa00><text></gradient>";
+    public static String commandTPSBarTextColorMedium = "<gradient:#ffff55:#ffaa00><text></gradient>";
+    public static String commandTPSBarTextColorLow = "<gradient:#ff5555:#aa0000><text></gradient>";
+    public static int commandTPSBarTickInterval = 20;
+    private static void commandSettings() {
+        commandTPSBarTitle = getString("settings.command.tpsbar.title", commandTPSBarTitle);
+        commandTPSBarProgressOverlay = BossBar.Overlay.valueOf(getString("settings.command.tpsbar.overlay", commandTPSBarProgressOverlay.name()));
+        commandTPSBarProgressFillMode = TPSBarTask.FillMode.valueOf(getString("settings.command.tpsbar.fill-mode", commandTPSBarProgressFillMode.name()));
+        commandTPSBarProgressColorGood = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.good", commandTPSBarProgressColorGood.name()));
+        commandTPSBarProgressColorMedium = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.medium", commandTPSBarProgressColorMedium.name()));
+        commandTPSBarProgressColorLow = BossBar.Color.valueOf(getString("settings.command.tpsbar.progress-color.low", commandTPSBarProgressColorLow.name()));
+        commandTPSBarTextColorGood = getString("settings.command.tpsbar.text-color.good", commandTPSBarTextColorGood);
+        commandTPSBarTextColorMedium = getString("settings.command.tpsbar.text-color.medium", commandTPSBarTextColorMedium);
+        commandTPSBarTextColorLow = getString("settings.command.tpsbar.text-color.low", commandTPSBarTextColorLow);
+        commandTPSBarTickInterval = getInt("settings.command.tpsbar.tick-interval", commandTPSBarTickInterval);
+    }
+
     public static boolean barrelSixRows = false;
     public static boolean enderChestSixRows = false;
     public static boolean enderChestPermissionRows = false;
diff --git a/src/main/java/net/pl3x/purpur/command/TPSBarCommand.java b/src/main/java/net/pl3x/purpur/command/TPSBarCommand.java
new file mode 100644
index 0000000000000000000000000000000000000000..2eca7b78907007be9683d87bf2346c87816ec3f0
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/command/TPSBarCommand.java
@@ -0,0 +1,21 @@
+package net.pl3x.purpur.command;
+
+import com.mojang.brigadier.CommandDispatcher;
+import net.minecraft.commands.CommandSourceStack;
+import net.minecraft.commands.Commands;
+import net.minecraft.server.level.ServerPlayer;
+import net.pl3x.purpur.task.TPSBarTask;
+
+public class TPSBarCommand {
+    public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
+        dispatcher.register(Commands.literal("tpsbar")
+                .requires(listener -> listener.hasPermission(2))
+                .executes(context -> {
+                    ServerPlayer player = context.getSource().getPlayerOrException();
+                    boolean result = TPSBarTask.instance().togglePlayer(player.getBukkitEntity());
+                    player.tpsBar(result);
+                    return 1;
+                })
+        ).setPermission("bukkit.command.tpsbar");
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/task/BossBarTask.java b/src/main/java/net/pl3x/purpur/task/BossBarTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..89122d7bcfd037a22d277e562f5300f2f3eab2db
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/task/BossBarTask.java
@@ -0,0 +1,109 @@
+package net.pl3x.purpur.task;
+
+import net.kyori.adventure.bossbar.BossBar;
+import net.minecraft.server.level.ServerPlayer;
+import org.bukkit.Bukkit;
+import org.bukkit.craftbukkit.scheduler.MinecraftInternalPlugin;
+import org.bukkit.entity.Player;
+import org.bukkit.scheduler.BukkitRunnable;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.UUID;
+
+public abstract class BossBarTask extends BukkitRunnable {
+    private final Map<UUID, BossBar> bossbars = new HashMap<>();
+    private boolean started;
+
+    abstract BossBar createBossBar();
+
+    abstract void updateBossBar(BossBar bossbar, Player player);
+
+    @Override
+    public void run() {
+        Iterator<Map.Entry<UUID, BossBar>> iter = bossbars.entrySet().iterator();
+        while (iter.hasNext()) {
+            Map.Entry<UUID, BossBar> entry = iter.next();
+            Player player = Bukkit.getPlayer(entry.getKey());
+            if (player == null) {
+                iter.remove();
+                continue;
+            }
+            updateBossBar(entry.getValue(), player);
+        }
+    }
+
+    @Override
+    public void cancel() {
+        super.cancel();
+        new HashSet<>(this.bossbars.keySet()).forEach(uuid -> {
+            Player player = Bukkit.getPlayer(uuid);
+            if (player != null) {
+                removePlayer(player);
+            }
+        });
+        this.bossbars.clear();
+    }
+
+    public boolean removePlayer(Player player) {
+        BossBar bossbar = this.bossbars.remove(player.getUniqueId());
+        if (bossbar != null) {
+            player.hideBossBar(bossbar);
+            return true;
+        }
+        return false;
+    }
+
+    public void addPlayer(Player player) {
+        removePlayer(player);
+        BossBar bossbar = createBossBar();
+        this.bossbars.put(player.getUniqueId(), bossbar);
+        this.updateBossBar(bossbar, player);
+        player.showBossBar(bossbar);
+    }
+
+    public boolean hasPlayer(UUID uuid) {
+        return this.bossbars.containsKey(uuid);
+    }
+
+    public boolean togglePlayer(Player player) {
+        if (removePlayer(player)) {
+            return false;
+        }
+        addPlayer(player);
+        return true;
+    }
+
+    public void start() {
+        stop();
+        this.runTaskTimerAsynchronously(new MinecraftInternalPlugin(), 1, 1);
+        started = true;
+    }
+
+    public void stop() {
+        if (started) {
+            cancel();
+        }
+    }
+
+    public static void startAll() {
+        TPSBarTask.instance().start();
+    }
+
+    public static void stopAll() {
+        TPSBarTask.instance().stop();
+    }
+
+    public static void addToAll(ServerPlayer player) {
+        Player bukkit = player.getBukkitEntity();
+        if (player.tpsBar()) {
+            TPSBarTask.instance().addPlayer(bukkit);
+        }
+    }
+
+    public static void removeFromAll(Player player) {
+        TPSBarTask.instance().removePlayer(player);
+    }
+}
diff --git a/src/main/java/net/pl3x/purpur/task/TPSBarTask.java b/src/main/java/net/pl3x/purpur/task/TPSBarTask.java
new file mode 100644
index 0000000000000000000000000000000000000000..cafb140ff57e5697d7c3705ff9be6411c0ad82d9
--- /dev/null
+++ b/src/main/java/net/pl3x/purpur/task/TPSBarTask.java
@@ -0,0 +1,142 @@
+package net.pl3x.purpur.task;
+
+import net.kyori.adventure.bossbar.BossBar;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.minimessage.Template;
+import net.pl3x.purpur.PurpurConfig;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+public class TPSBarTask extends BossBarTask {
+    private static TPSBarTask instance;
+    private double tps = 20.0D;
+    private double mspt = 0.0D;
+    private int tick = 0;
+
+    public static TPSBarTask instance() {
+        if (instance == null) {
+            instance = new TPSBarTask();
+        }
+        return instance;
+    }
+
+    @Override
+    BossBar createBossBar() {
+        return BossBar.bossBar(Component.text(""), 0.0F, instance().getBossBarColor(), PurpurConfig.commandTPSBarProgressOverlay);
+    }
+
+    @Override
+    void updateBossBar(BossBar bossbar, Player player) {
+        bossbar.progress(getBossBarProgress());
+        bossbar.color(getBossBarColor());
+        bossbar.name(MiniMessage.get().parse(PurpurConfig.commandTPSBarTitle,
+                Template.of("tps", getTPSColor()),
+                Template.of("mspt", getMSPTColor()),
+                Template.of("ping", getPingColor(player.getPing()))
+        ));
+    }
+
+    @Override
+    public void run() {
+        if (++tick < PurpurConfig.commandTPSBarTickInterval) {
+            return;
+        }
+        tick = 0;
+
+        this.tps = Math.max(Math.min(Bukkit.getTPS()[0], 20.0D), 0.0D);
+        this.mspt = Bukkit.getAverageTickTime();
+
+        super.run();
+    }
+
+    private float getBossBarProgress() {
+        if (PurpurConfig.commandTPSBarProgressFillMode == FillMode.MSPT) {
+            return Math.max(Math.min((float) mspt / 50.0F, 1.0F), 0.0F);
+        } else {
+            return Math.max(Math.min((float) tps / 20.0F, 1.0F), 0.0F);
+        }
+    }
+
+    private BossBar.Color getBossBarColor() {
+        if (isGood(PurpurConfig.commandTPSBarProgressFillMode)) {
+            return PurpurConfig.commandTPSBarProgressColorGood;
+        } else if (isMedium(PurpurConfig.commandTPSBarProgressFillMode)) {
+            return PurpurConfig.commandTPSBarProgressColorMedium;
+        } else {
+            return PurpurConfig.commandTPSBarProgressColorLow;
+        }
+    }
+
+    private boolean isGood(FillMode mode) {
+        return isGood(mode, 0);
+    }
+
+    private boolean isGood(FillMode mode, int ping) {
+        if (mode == FillMode.MSPT) {
+            return mspt < 40;
+        } else if (mode == FillMode.TPS) {
+            return tps >= 19;
+        } else if (mode == FillMode.PING) {
+            return ping < 100;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isMedium(FillMode mode) {
+        return isMedium(mode, 0);
+    }
+
+    private boolean isMedium(FillMode mode, int ping) {
+        if (mode == FillMode.MSPT) {
+            return mspt < 50;
+        } else if (mode == FillMode.TPS) {
+            return tps >= 15;
+        } else if (mode == FillMode.PING) {
+            return ping < 200;
+        } else {
+            return false;
+        }
+    }
+
+    private Component getTPSColor() {
+        String color;
+        if (isGood(FillMode.TPS)) {
+            color = PurpurConfig.commandTPSBarTextColorGood;
+        } else if (isMedium(FillMode.TPS)) {
+            color = PurpurConfig.commandTPSBarTextColorMedium;
+        } else {
+            color = PurpurConfig.commandTPSBarTextColorLow;
+        }
+        return MiniMessage.get().parse(color, Template.of("text", String.format("%.2f", tps)));
+    }
+
+    private Component getMSPTColor() {
+        String color;
+        if (isGood(FillMode.MSPT)) {
+            color = PurpurConfig.commandTPSBarTextColorGood;
+        } else if (isMedium(FillMode.MSPT)) {
+            color = PurpurConfig.commandTPSBarTextColorMedium;
+        } else {
+            color = PurpurConfig.commandTPSBarTextColorLow;
+        }
+        return MiniMessage.get().parse(color, Template.of("text", String.format("%.2f", mspt)));
+    }
+
+    private Component getPingColor(int ping) {
+        String color;
+        if (isGood(FillMode.PING, ping)) {
+            color = PurpurConfig.commandTPSBarTextColorGood;
+        } else if (isMedium(FillMode.PING, ping)) {
+            color = PurpurConfig.commandTPSBarTextColorMedium;
+        } else {
+            color = PurpurConfig.commandTPSBarTextColorLow;
+        }
+        return MiniMessage.get().parse(color, Template.of("text", String.format("%s", ping)));
+    }
+
+    public enum FillMode {
+        TPS, MSPT, PING
+    }
+}
