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"