From 70058c552d296a997156654519b3c65f542c3e7c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Elias=20M=C3=BCller?= <elias@elias-mueller.com>
Date: Sat, 24 Aug 2024 01:37:02 +0200
Subject: [PATCH] added settings with technicalTab toggle

---
 .../java/eu/mhsl/craftattack/spawn/Main.java  |  4 +-
 .../spawn/appliances/settings/Settings.java   | 81 +++++++++++++++++++
 .../appliances/settings/SettingsCommand.java  | 17 ++++
 .../settings/SettingsInventoryListener.java   | 30 +++++++
 .../settings/datatypes/BoolSetting.java       | 59 ++++++++++++++
 .../settings/datatypes/Setting.java           | 49 +++++++++++
 .../settings/TechnicalTablistSetting.java     | 26 ++++++
 .../spawn/appliances/tablist/Tablist.java     | 39 +++++----
 .../spawn/util/text/ComponentUtil.java        | 20 +++--
 src/main/resources/plugin.yml                 |  3 +-
 10 files changed, 304 insertions(+), 24 deletions(-)
 create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsCommand.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsInventoryListener.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java
 create mode 100644 src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/TechnicalTablistSetting.java

diff --git a/src/main/java/eu/mhsl/craftattack/spawn/Main.java b/src/main/java/eu/mhsl/craftattack/spawn/Main.java
index 5160274..11abe83 100644
--- a/src/main/java/eu/mhsl/craftattack/spawn/Main.java
+++ b/src/main/java/eu/mhsl/craftattack/spawn/Main.java
@@ -17,6 +17,7 @@ import eu.mhsl.craftattack.spawn.appliances.help.Help;
 import eu.mhsl.craftattack.spawn.appliances.playerlimit.PlayerLimit;
 import eu.mhsl.craftattack.spawn.appliances.report.Report;
 import eu.mhsl.craftattack.spawn.appliances.restart.Restart;
+import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
 import eu.mhsl.craftattack.spawn.appliances.tablist.Tablist;
 import eu.mhsl.craftattack.spawn.appliances.titleClear.TitleClear;
 import eu.mhsl.craftattack.spawn.appliances.whitelist.Whitelist;
@@ -59,7 +60,8 @@ public final class Main extends JavaPlugin {
                 new DisplayName(),
                 new Debug(),
                 new Fleischerchest(),
-                new CustomAdvancements()
+                new CustomAdvancements(),
+                new Settings()
         );
 
         Bukkit.getLogger().info("Loading appliances...");
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java
new file mode 100644
index 0000000..446de0d
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/Settings.java
@@ -0,0 +1,81 @@
+package eu.mhsl.craftattack.spawn.appliances.settings;
+
+import eu.mhsl.craftattack.spawn.appliance.Appliance;
+import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
+import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.Setting;
+import eu.mhsl.craftattack.spawn.appliances.settings.settings.TechnicalTablistSetting;
+import net.kyori.adventure.text.Component;
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+import org.bukkit.event.Listener;
+import org.bukkit.inventory.Inventory;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.WeakHashMap;
+
+public class Settings extends Appliance {
+    public enum Key {
+        TechnicalTab,
+    }
+
+    public record OpenSettingsInventory(Inventory inventory, List<Setting<?>> settings) {}
+    private final WeakHashMap<Player, OpenSettingsInventory> openSettingsInventories = new WeakHashMap<>();
+    private final WeakHashMap<Player, List<Setting<?>>> settingsCache = new WeakHashMap<>();
+
+    private List<Setting<?>> getSettings(Player player) {
+        if(settingsCache.containsKey(player)) return settingsCache.get(player);
+
+        List<Setting<?>> settings =  List.of(
+            new TechnicalTablistSetting()
+        );
+
+        settings.forEach(setting -> setting.initializeFromPlayer(player));
+        this.settingsCache.put(player, settings);
+        return settings;
+    }
+
+    public <T> T getSetting(Player player, Key key, Class<T> clazz) {
+        Setting<?> setting = getSettings(player).stream()
+                .filter(s -> s.getKey().equals(key))
+                .findFirst()
+                .orElseThrow();
+
+        if(!clazz.equals(setting.dataType())) throw new IllegalStateException("Tried to retrieve Setting with Datatype " + clazz.getSimpleName() + " but expected " + setting.dataType().getSimpleName());
+        if(!clazz.isInstance(setting.state())) throw new ClassCastException(clazz.getSimpleName() + " is not an instance of " + setting.dataType().getSimpleName());
+        return clazz.cast(setting.state());
+    }
+
+    public void openSettings(Player player) {
+        Inventory inventory = Bukkit.createInventory(null, 9, Component.text("Einstellungen"));
+        List<Setting<?>> settings = getSettings(player);
+        settings.forEach(setting -> inventory.addItem(setting.buildItem()));
+        player.openInventory(inventory);
+        this.openSettingsInventories.put(player, new OpenSettingsInventory(inventory, settings));
+    }
+
+    public void onSettingsClose(Player player) {
+        if(!openSettingsInventories.containsKey(player)) return;
+        openSettingsInventories.remove(player);
+        player.updateInventory();
+    }
+
+    public boolean hasSettingsOpen(Player player) {
+        return this.openSettingsInventories.containsKey(player);
+    }
+
+    public OpenSettingsInventory getOpenInventory(Player player) {
+        if(!hasSettingsOpen(player)) throw new RuntimeException("Cannot retrieve data from closed Settings inventory!");
+        return this.openSettingsInventories.get(player);
+    }
+
+    @Override
+    protected @NotNull List<Listener> eventHandlers() {
+        return List.of(new SettingsInventoryListener());
+    }
+
+    @Override
+    protected @NotNull List<ApplianceCommand<?>> commands() {
+        return List.of(new SettingsCommand());
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsCommand.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsCommand.java
new file mode 100644
index 0000000..4ba335f
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsCommand.java
@@ -0,0 +1,17 @@
+package eu.mhsl.craftattack.spawn.appliances.settings;
+
+import eu.mhsl.craftattack.spawn.appliance.ApplianceCommand;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.jetbrains.annotations.NotNull;
+
+public class SettingsCommand extends ApplianceCommand.PlayerChecked<Settings> {
+    public SettingsCommand() {
+        super("settings");
+    }
+
+    @Override
+    protected void execute(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) throws Exception {
+        getAppliance().openSettings(getPlayer());
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsInventoryListener.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsInventoryListener.java
new file mode 100644
index 0000000..ed54654
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/SettingsInventoryListener.java
@@ -0,0 +1,30 @@
+package eu.mhsl.craftattack.spawn.appliances.settings;
+
+import eu.mhsl.craftattack.spawn.appliance.ApplianceListener;
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.inventory.InventoryClickEvent;
+import org.bukkit.event.inventory.InventoryCloseEvent;
+
+public class SettingsInventoryListener extends ApplianceListener<Settings> {
+    @EventHandler
+    public void onInventoryClick(InventoryClickEvent event) {
+        Player player = (Player) event.getWhoClicked();
+        if(!getAppliance().hasSettingsOpen(player)) return;
+        event.setCancelled(true);
+
+        Settings.OpenSettingsInventory openInventory = getAppliance().getOpenInventory(player);
+        openInventory.settings().stream()
+            .filter(setting -> setting.buildItem().equals(event.getCurrentItem()))
+            .findFirst()
+            .ifPresent(setting -> {
+                setting.triggerChange(player);
+                openInventory.inventory().setItem(event.getSlot(), setting.buildItem());
+            });
+    }
+
+    @EventHandler
+    public void onInventoryClose(InventoryCloseEvent event) {
+        getAppliance().onSettingsClose((Player) event.getPlayer());
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java
new file mode 100644
index 0000000..b9d04cc
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/BoolSetting.java
@@ -0,0 +1,59 @@
+package eu.mhsl.craftattack.spawn.appliances.settings.datatypes;
+
+import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+
+import java.util.List;
+
+public abstract class BoolSetting extends Setting<Boolean> {
+    private boolean state;
+
+    public BoolSetting(Settings.Key key) {
+        super(key);
+    }
+
+    protected abstract String title();
+    protected abstract String description();
+
+    @Override
+    public void fromStorage(PersistentDataContainer container) {
+        this.state = Boolean.TRUE.equals(container.get(getNamespacedKey(), PersistentDataType.BOOLEAN));
+    }
+
+    @Override
+    protected void toStorage(PersistentDataContainer container, Boolean value) {
+        container.set(getNamespacedKey(), PersistentDataType.BOOLEAN, value);
+    }
+
+    @Override
+    public ItemMeta buildMeta(ItemMeta meta) {
+        meta.displayName(Component.text(title(), NamedTextColor.WHITE));
+        meta.lore(List.of(
+            Component.empty()
+                .append(Component.text("Status: ", NamedTextColor.DARK_GRAY))
+                .append(Component.text(this.state ? "An" : "Aus", this.state ? NamedTextColor.GREEN : NamedTextColor.RED)),
+            Component.empty(),
+            Component.text(description(), NamedTextColor.GRAY)
+        ));
+        return meta;
+    }
+
+    @Override
+    protected void change() {
+        this.state = !this.state;
+    }
+
+    @Override
+    public Class<?> dataType() {
+        return Boolean.class;
+    }
+
+    @Override
+    public Boolean state() {
+        return state;
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java
new file mode 100644
index 0000000..7e0b1b6
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/datatypes/Setting.java
@@ -0,0 +1,49 @@
+package eu.mhsl.craftattack.spawn.appliances.settings.datatypes;
+
+import eu.mhsl.craftattack.spawn.Main;
+import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
+import org.bukkit.Material;
+import org.bukkit.NamespacedKey;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+
+public abstract class Setting<TDataType> {
+    private final Settings.Key key;
+
+    public Setting(Settings.Key key) {
+        this.key = key;
+    }
+
+    public NamespacedKey getNamespacedKey() {
+        return new NamespacedKey(Main.instance(), key.name());
+    }
+
+    public Settings.Key getKey() {
+        return key;
+    }
+
+    public void initializeFromPlayer(Player p) {
+        fromStorage(p.getPersistentDataContainer());
+    }
+
+    public void triggerChange(Player p) {
+        this.change();
+        toStorage(p.getPersistentDataContainer(), this.state());
+    }
+
+    public ItemStack buildItem() {
+        ItemStack stack = new ItemStack(icon(), 1);
+        stack.setItemMeta(buildMeta(stack.getItemMeta()));
+        return stack;
+    }
+
+    protected abstract Material icon();
+    public abstract ItemMeta buildMeta(ItemMeta meta);
+    protected abstract void change();
+    protected abstract void fromStorage(PersistentDataContainer container);
+    protected abstract void toStorage(PersistentDataContainer container, TDataType value);
+    public abstract Class<?> dataType();
+    public abstract TDataType state();
+}
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/TechnicalTablistSetting.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/TechnicalTablistSetting.java
new file mode 100644
index 0000000..e02e712
--- /dev/null
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/settings/settings/TechnicalTablistSetting.java
@@ -0,0 +1,26 @@
+package eu.mhsl.craftattack.spawn.appliances.settings.settings;
+
+import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
+import eu.mhsl.craftattack.spawn.appliances.settings.datatypes.BoolSetting;
+import org.bukkit.Material;
+
+public class TechnicalTablistSetting extends BoolSetting {
+    public TechnicalTablistSetting() {
+        super(Settings.Key.TechnicalTab);
+    }
+
+    @Override
+    protected String title() {
+        return "Technische Informationen";
+    }
+
+    @Override
+    protected String description() {
+        return "Zeige erweiterte Informationen und Statistiken in der Tabliste an";
+    }
+
+    @Override
+    protected Material icon() {
+        return Material.COMMAND_BLOCK_MINECART;
+    }
+}
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/appliances/tablist/Tablist.java b/src/main/java/eu/mhsl/craftattack/spawn/appliances/tablist/Tablist.java
index b7d0b84..452ceb1 100644
--- a/src/main/java/eu/mhsl/craftattack/spawn/appliances/tablist/Tablist.java
+++ b/src/main/java/eu/mhsl/craftattack/spawn/appliances/tablist/Tablist.java
@@ -3,6 +3,7 @@ package eu.mhsl.craftattack.spawn.appliances.tablist;
 import eu.mhsl.craftattack.spawn.Main;
 import eu.mhsl.craftattack.spawn.appliance.Appliance;
 import eu.mhsl.craftattack.spawn.appliances.report.Report;
+import eu.mhsl.craftattack.spawn.appliances.settings.Settings;
 import eu.mhsl.craftattack.spawn.util.statistics.NetworkMonitor;
 import eu.mhsl.craftattack.spawn.util.text.ComponentUtil;
 import eu.mhsl.craftattack.spawn.util.IteratorUtil;
@@ -20,7 +21,6 @@ import java.util.List;
 
 
 public class Tablist extends Appliance {
-    private final int refreshRate = Ticks.TICKS_PER_SECOND * 3;
     private final RainbowComponent serverName = new RainbowComponent(" CraftAttack 7 ", 7, 3);
     private NetworkMonitor networkMonitor;
 
@@ -30,12 +30,13 @@ public class Tablist extends Appliance {
 
     @Override
     public void onEnable() {
+        int tabRefreshRate = 3;
         this.networkMonitor = new NetworkMonitor(localConfig().getString("interface"), Duration.ofSeconds(1));
         Bukkit.getScheduler().runTaskTimerAsynchronously(
-                Main.instance(),
-                () -> IteratorUtil.onlinePlayers(this::updateHeader),
-                refreshRate,
-                refreshRate
+            Main.instance(),
+            () -> IteratorUtil.onlinePlayers(this::updateHeader),
+            tabRefreshRate * Ticks.TICKS_PER_SECOND,
+            tabRefreshRate * Ticks.TICKS_PER_SECOND
         );
     }
 
@@ -50,17 +51,23 @@ public class Tablist extends Appliance {
     }
 
     private void updateHeader(Player player) {
-        player.sendPlayerListHeader(
-                Component.newline()
-                        .append(serverName.getRainbowState()).appendNewline()
-                        .append(Component.text("mhsl.eu", NamedTextColor.GOLD)).appendNewline().appendNewline()
-                        .append(ComponentUtil.getFormattedMSPT()).appendNewline().appendNewline()
-                        .append(ComponentUtil.getFormattedPing(player)).appendNewline()
-                        .append(ComponentUtil.getFormattedNetworkStats(
-                            this.networkMonitor.getTraffic(),
-                            this.networkMonitor.getPackets())
-                        ).appendNewline()
-        );
+        boolean detailedInfo = queryAppliance(Settings.class).getSetting(player, Settings.Key.TechnicalTab, Boolean.class);
+        Component header = Component.newline()
+                .append(serverName.getRainbowState()).appendNewline()
+                .append(Component.text("mhsl.eu", NamedTextColor.GOLD)).appendNewline().appendNewline()
+                .append(ComponentUtil.getFormattedTickTimes(detailedInfo)).appendNewline();
+
+        if(detailedInfo) {
+            header = header
+                .appendNewline()
+                .append(ComponentUtil.getFormattedPing(player)).appendNewline()
+                .append(ComponentUtil.getFormattedNetworkStats(
+                        this.networkMonitor.getTraffic(),
+                        this.networkMonitor.getPackets())
+                ).appendNewline();
+        }
+
+        player.sendPlayerListHeader(header);
     }
 
     private void updateFooter(Player player) {
diff --git a/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java b/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java
index 11991e6..8ec83db 100644
--- a/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java
+++ b/src/main/java/eu/mhsl/craftattack/spawn/util/text/ComponentUtil.java
@@ -2,6 +2,8 @@ package eu.mhsl.craftattack.spawn.util.text;
 
 import eu.mhsl.craftattack.spawn.util.statistics.NetworkMonitor;
 import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.ComponentBuilder;
+import net.kyori.adventure.text.TextComponent;
 import net.kyori.adventure.text.format.NamedTextColor;
 import net.kyori.adventure.text.format.TextColor;
 import org.bukkit.Bukkit;
@@ -39,7 +41,7 @@ public class ComponentUtil {
                 .build();
     }
 
-    public static Component getFormattedMSPT() {
+    public static Component getFormattedTickTimes(boolean detailed) {
         long[] times = Bukkit.getServer().getTickTimes();
         float mspt = ((float) Arrays.stream(times).sum() / times.length) * 1.0E-6f;
         float roundedMspt = Math.round(mspt * 100f) / 100f;
@@ -50,14 +52,20 @@ public class ComponentUtil {
         TextColor percentageColor = ColorUtil.mapGreenToRed(loadPercentage, 80, 100, true);
         TextColor tpsColor = ColorUtil.mapGreenToRed(roundedTPS, 15, 20, false);
 
-        return Component.text()
+        ComponentBuilder<TextComponent, TextComponent.Builder> tickTimes = Component.text()
                 .append(Component.text("Serverlast: ", NamedTextColor.GRAY))
                 .append(Component.text(loadPercentage + "% ", percentageColor))
-                .appendNewline()
+                .appendNewline();
+
+        if(detailed) {
+            tickTimes
                 .append(Component.text(roundedMspt + "mspt", msptColor))
-                .append(Component.text(" | ", NamedTextColor.GRAY))
-                .append(Component.text(roundedTPS + "tps", tpsColor))
-                .build();
+                .append(Component.text(" | ", NamedTextColor.GRAY));
+        }
+
+        return tickTimes
+            .append(Component.text(roundedTPS + "tps", tpsColor))
+            .build();
     }
 
 
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index a30cb7f..757b0a6 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -35,4 +35,5 @@ commands:
   cancelRestart:
   kick:
   panicBan:
-  vogelfrei:
\ No newline at end of file
+  vogelfrei:
+  settings:
\ No newline at end of file