From 04e9f6a7201bbb7fcdb997db63fd08d51eecf3f3 Mon Sep 17 00:00:00 2001 From: youhavetrouble Date: Sat, 17 Jun 2023 11:14:01 +0200 Subject: [PATCH] let there be jank --- pom.xml | 126 ++++----- readme.md | 7 + .../me/youhavetrouble/statstop/StatsData.java | 86 +++++- .../me/youhavetrouble/statstop/StatsTop.java | 2 +- .../statstop/StatsTopCommand.java | 245 +++++++++++++++++- src/main/resources/paper-plugin.yml | 4 +- 6 files changed, 403 insertions(+), 67 deletions(-) diff --git a/pom.xml b/pom.xml index 5ba85e0..df77103 100644 --- a/pom.xml +++ b/pom.xml @@ -2,73 +2,73 @@ - 4.0.0 + 4.0.0 - me.youhavetrouble - StatsTop - 1.0 - jar + me.youhavetrouble + StatsTop + 1.0 + jar - StatsTop + StatsTop - - 1.8 - UTF-8 - + + 17 + UTF-8 + - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - ${java.version} - ${java.version} - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.4 - - - package - - shade - - - false - - - - - - - - src/main/resources - true - - - + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 16 + 16 + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + false + + + + + + + + src/main/resources + true + + + - - - papermc-repo - https://repo.papermc.io/repository/maven-public/ - - - sonatype - https://oss.sonatype.org/content/groups/public/ - - + + + papermc-repo + https://repo.papermc.io/repository/maven-public/ + + + sonatype + https://oss.sonatype.org/content/groups/public/ + + - - - io.papermc.paper - paper-api - 1.20-R0.1-SNAPSHOT - provided - - + + + io.papermc.paper + paper-api + 1.20-R0.1-SNAPSHOT + provided + + diff --git a/readme.md b/readme.md index e69de29..eb7f78e 100644 --- a/readme.md +++ b/readme.md @@ -0,0 +1,7 @@ +# StatsTop + +Simple plugin adding a command that shows the top players on the server for given statistic. + +Some stats are probably broken or don't have correct translation strings set. + +This project is completely unsupported, use at your own risk. \ No newline at end of file diff --git a/src/main/java/me/youhavetrouble/statstop/StatsData.java b/src/main/java/me/youhavetrouble/statstop/StatsData.java index 1a31f05..4b19224 100644 --- a/src/main/java/me/youhavetrouble/statstop/StatsData.java +++ b/src/main/java/me/youhavetrouble/statstop/StatsData.java @@ -1,2 +1,86 @@ -package me.youhavetrouble.statstop;public class StatsData { +package me.youhavetrouble.statstop; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; + +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +public class StatsData { + + private final LinkedHashMap data = new LinkedHashMap<>(); + private final long timestamp; + private final Statistic stat; + private final Object arg; + + protected StatsData(LinkedHashMap data, Statistic stat, Object arg) { + this.data.putAll(data); + this.stat = stat; + this.arg = arg; + this.timestamp = System.currentTimeMillis(); + } + + public long getTimestamp() { + return timestamp; + } + + public Component getTop(int amount, CommandSender sender) { + Component component = Component.text("Top", NamedTextColor.GOLD) + .append(Component.space()) + .append(Component.translatable("stat.minecraft." + stat.toString().toLowerCase(Locale.ENGLISH), NamedTextColor.GOLD)); + + if (arg != null) { + if (arg instanceof EntityType entityType) { + component = component + .append(Component.text(" - ")) + .append(Component.translatable("entity.minecraft." + entityType.toString().toLowerCase(Locale.ENGLISH))); + } else if (arg instanceof Material material) { + component = component.append(Component.text(" - ", NamedTextColor.GOLD)); + + if (material.isBlock()) { + component = component.append(Component.translatable("block.minecraft." + material.toString().toLowerCase(Locale.ENGLISH), NamedTextColor.GOLD)); + } else if (material.isItem()) { + component = component.append(Component.translatable("item.minecraft." + material.toString().toLowerCase(Locale.ENGLISH), NamedTextColor.GOLD)); + } + + } + } + + String senderName = null; + + if (sender instanceof Player player) { + senderName = player.getName(); + } + + HashSet names = new HashSet<>(amount); + Component senderData = null; + int i = 0; + for (Map.Entry key : data.entrySet()) { + if (key.getKey().equals(senderName)) { + senderData = getLine(i + 1, key.getKey(), key.getValue(), NamedTextColor.YELLOW); + } + if (i >= amount && senderData != null) continue; + names.add(key.getKey()); + component = component.append(Component.newline()); + component = component.append(key.getKey().equals(senderName) ? senderData : getLine(i + 1, key.getKey(), key.getValue(), NamedTextColor.WHITE)); + i++; + } + + if (sender instanceof Player player && !names.contains(player.getName()) && senderData != null) { + component = component.append(senderData); + } + return component; + } + + private Component getLine(int place, String name, int value, TextColor textColor) { + return Component.text(place + ". " + name + " - " + value, textColor); + } } diff --git a/src/main/java/me/youhavetrouble/statstop/StatsTop.java b/src/main/java/me/youhavetrouble/statstop/StatsTop.java index eec838b..278218f 100644 --- a/src/main/java/me/youhavetrouble/statstop/StatsTop.java +++ b/src/main/java/me/youhavetrouble/statstop/StatsTop.java @@ -6,7 +6,7 @@ public final class StatsTop extends JavaPlugin { @Override public void onEnable() { - // Plugin startup logic + getServer().getCommandMap().register("top", new StatsTopCommand()); } diff --git a/src/main/java/me/youhavetrouble/statstop/StatsTopCommand.java b/src/main/java/me/youhavetrouble/statstop/StatsTopCommand.java index 4f1c713..cbe58f9 100644 --- a/src/main/java/me/youhavetrouble/statstop/StatsTopCommand.java +++ b/src/main/java/me/youhavetrouble/statstop/StatsTopCommand.java @@ -1,2 +1,245 @@ -package me.youhavetrouble.statstop;public class StatsTopCommand { +package me.youhavetrouble.statstop; + +import net.kyori.adventure.text.Component; +import org.bukkit.*; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.util.StringUtil; +import org.bukkit.util.permissions.DefaultPermissions; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +public class StatsTopCommand extends Command { + + private final HashMap cachedData = new HashMap<>(); + + protected StatsTopCommand() { + super("top", "Display top for given stat", "/top [arg]", new ArrayList<>()); + setPermission("statstop.top"); + DefaultPermissions.registerPermission("statstop.top", "Allows you to use /top", PermissionDefault.TRUE); + } + + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) { + List completions = new ArrayList<>(); + + if (args.length == 1) { + for (Statistic stat : Statistic.values()) { + if (StringUtil.startsWithIgnoreCase(stat.toString(), args[0])) { + completions.add(stat.toString().toLowerCase(Locale.ENGLISH)); + } + } + } + if (args.length == 2) { + Statistic statistic; + try { + statistic = Statistic.valueOf(args[0].toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + return completions; + } + + switch (statistic.getType()) { + case ITEM -> { + for (Material material : Material.values()) { + if (!material.isItem()) continue; + if (StringUtil.startsWithIgnoreCase(material.toString(), args[1])) { + completions.add(material.toString().toLowerCase(Locale.ENGLISH)); + } + } + } + case BLOCK -> { + for (Material material : Material.values()) { + if (!material.isBlock()) continue; + if (StringUtil.startsWithIgnoreCase(material.toString(), args[1])) { + completions.add(material.toString().toLowerCase(Locale.ENGLISH)); + } + } + } + case ENTITY -> { + for (EntityType entityType : EntityType.values()) { + if (StringUtil.startsWithIgnoreCase(entityType.toString(), args[1])) { + completions.add(entityType.toString().toLowerCase(Locale.ENGLISH)); + } + } + } + } + + } + + return completions; + } + + @Override + public boolean execute(@NotNull CommandSender commandSender, @NotNull String s, @NotNull String[] strings) { + if (!(commandSender instanceof Player player)) { + commandSender.sendMessage(Component.text("Only players can use this command")); + return true; + } + + if (strings.length < 1) return false; + String statName = strings[0]; + Statistic stat = null; + + try { + stat = Statistic.valueOf(statName.toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException e) { + commandSender.sendMessage(Component.text(getUsage())); + return false; + } + + Object arg = null; + + if (strings.length == 2) { + try { + arg = Material.valueOf(strings[1].toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException ignored) { + } + if (arg == null) { + try { + arg = EntityType.valueOf(strings[1].toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException ignored) { + } + } + if (arg == null) { + commandSender.sendMessage(Component.text(getUsage())); + return false; + } + } + + StatsData statsData = null; + + switch (stat.getType()) { + case UNTYPED -> { + statsData = cachedData.get(getStatId(stat, null)); + if (statsData == null || statsData.getTimestamp() + 60000 < System.currentTimeMillis()) { + statsData = createData(stat); + cachedData.put(getStatId(stat, null), statsData); + } + } + case ITEM, BLOCK -> { + if (!(arg instanceof Material material)) return false; + statsData = cachedData.get(getStatId(stat, arg)); + if (statsData == null || statsData.getTimestamp() + 60000 < System.currentTimeMillis()) { + statsData = createData(stat, material); + cachedData.put(getStatId(stat, arg), statsData); + } + } + case ENTITY -> { + if (!(arg instanceof EntityType entityType)) return false; + statsData = cachedData.get(getStatId(stat, arg)); + if (statsData == null || statsData.getTimestamp() + 60000 < System.currentTimeMillis()) { + statsData = createData(stat, entityType); + cachedData.put(getStatId(stat, arg), statsData); + } + } + } + + if (statsData == null) { + commandSender.sendMessage(Component.text("Error getting data")); + return false; + } + + Component topComponent = statsData.getTop(10, commandSender); + commandSender.sendMessage(topComponent); + + return false; + } + + private String getStatId(Statistic stat, Object arg) { + + String statKey = stat.getKey().getKey(); + + return switch (stat.getType()) { + case UNTYPED -> statKey; + case ITEM, BLOCK -> { + if (arg instanceof Material item) { + yield statKey + "." + item; + } else { + yield null; + } + } + case ENTITY -> { + if (arg instanceof EntityType entityType) { + yield statKey + "." + entityType; + } else { + yield null; + } + } + }; + } + + /** + * Creates a map of player names to their stat value. This will return null for stats that are not UNTYPED. + * + * @param stat statistic to get data for + * @return map of player names to their stat value + */ + private StatsData createData(Statistic stat) { + if (stat.getType() != Statistic.Type.UNTYPED) return null; + HashMap data = new HashMap<>(); + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + int statValue = offlinePlayer.getStatistic(stat); + String playerName = offlinePlayer.getName(); + data.put(playerName, statValue); + } + + LinkedHashMap sortedData = new LinkedHashMap<>(); + data.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .forEachOrdered(x -> sortedData.put(x.getKey(), x.getValue())); + + return new StatsData(sortedData, stat, null); + } + + /** + * Creates a map of player names to their stat value. This will return null for stats that are not ENTITY. + * + * @param stat statistic to get data for + * @return map of player names to their stat value + */ + private StatsData createData(Statistic stat, EntityType entityType) { + if (stat.getType() != Statistic.Type.ENTITY) return null; + HashMap data = new HashMap<>(); + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + int statValue = offlinePlayer.getStatistic(stat, entityType); + String playerName = offlinePlayer.getName(); + data.put(playerName, statValue); + } + + LinkedHashMap sortedData = new LinkedHashMap<>(); + data.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .forEachOrdered(x -> sortedData.put(x.getKey(), x.getValue())); + + return new StatsData(sortedData, stat, entityType); + } + + /** + * Creates a map of player names to their stat value. This will return null for stats that are not BLOCK or ITEM. + * + * @param stat statistic to get data for + * @return map of player names to their stat value + */ + private StatsData createData(Statistic stat, Material item) { + if (stat.getType() != Statistic.Type.ITEM && stat.getType() != Statistic.Type.BLOCK) return null; + HashMap data = new HashMap<>(); + for (OfflinePlayer offlinePlayer : Bukkit.getOfflinePlayers()) { + int statValue = offlinePlayer.getStatistic(stat, item); + String playerName = offlinePlayer.getName(); + data.put(playerName, statValue); + } + + LinkedHashMap sortedData = new LinkedHashMap<>(); + data.entrySet() + .stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .forEachOrdered(x -> sortedData.put(x.getKey(), x.getValue())); + + return new StatsData(sortedData, stat, item); + } } diff --git a/src/main/resources/paper-plugin.yml b/src/main/resources/paper-plugin.yml index c622eac..c467c20 100644 --- a/src/main/resources/paper-plugin.yml +++ b/src/main/resources/paper-plugin.yml @@ -1,4 +1,6 @@ name: StatsTop version: '${project.version}' main: me.youhavetrouble.statstop.StatsTop -api-version: 1.20 +authors: + - YouHaveTrouble +api-version: "1.20"