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