Merge pull request #25 from itHotL/top-statistic

Top statistic
This commit is contained in:
Artemis-the-gr8 2022-05-18 00:11:32 +02:00 committed by GitHub
commit 0c93fa85c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 564 additions and 356 deletions

View File

@ -19,7 +19,7 @@
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.18-R0.1-SNAPSHOT</version>
<version>1.18.2-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>

View File

@ -3,6 +3,7 @@ package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.commands.ReloadCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.StatCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.TabCompleter;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.listeners.JoinListener;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
@ -14,17 +15,16 @@ public class Main extends JavaPlugin {
@Override
public void onEnable() {
ConfigHandler config = new ConfigHandler(this);
EnumHandler enumHandler = new EnumHandler();
EnumHandler enumHandler = new EnumHandler(this);
OutputFormatter outputFormatter = new OutputFormatter(config);
StatManager statManager = new StatManager(enumHandler, this);
this.getCommand("statistic").setExecutor(new StatCommand(outputFormatter, statManager, this));
this.getCommand("statistic").setTabCompleter(new TabCompleter(
enumHandler, statManager,this));
this.getCommand("statisticreload").setExecutor(new ReloadCommand(config, outputFormatter));
//prepare private hashMap of offline players
OfflinePlayerHandler.updateOfflinePlayers();
this.getCommand("statistic").setExecutor(new StatCommand(outputFormatter, enumHandler, this));
this.getCommand("statistic").setTabCompleter(new TabCompleter(enumHandler, this));
this.getCommand("statisticreload").setExecutor(new ReloadCommand(config, outputFormatter, this));
Bukkit.getPluginManager().registerEvents(new JoinListener(), this);
this.getLogger().info("Enabled PlayerStats!");
@ -35,14 +35,8 @@ public class Main extends JavaPlugin {
this.getLogger().info("Disabled PlayerStats!");
}
public void logStatRelatedExceptions(Exception exception) {
if (exception instanceof IllegalArgumentException) {
getLogger().warning("IllegalArgumentException - this is probably not a valid statistic name!");
public long logTimeTaken(String className, String methodName, long previousTime, int lineNumber) {
getLogger().info(className + " " + methodName + " " + lineNumber + ": " + (System.currentTimeMillis() - previousTime));
return System.currentTimeMillis();
}
else if (exception instanceof NullPointerException) {
getLogger().warning("NullPointerException - no statistic name was provided");
}
}
}

View File

@ -1,165 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StatManager {
private final Main plugin;
private final EnumHandler enumHandler;
private final OfflinePlayerHandler offlinePlayerHandler;
private final List<String> statNames;
private final List<String> entityStatNames;
private final List<String> subStatEntryNames;
public StatManager(EnumHandler e, Main p) {
plugin = p;
enumHandler = e;
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
statNames = Arrays.stream(Statistic.values()).map(
Statistic::toString).map(String::toLowerCase).toList();
entityStatNames = Arrays.stream(Statistic.values()).filter(statistic ->
statistic.getType().equals(Statistic.Type.ENTITY)).map(
Statistic::toString).map(String::toLowerCase).collect(Collectors.toList());
subStatEntryNames = new ArrayList<>();
subStatEntryNames.addAll(enumHandler.getBlockNames());
subStatEntryNames.addAll(enumHandler.getEntityTypeNames());
subStatEntryNames.addAll(enumHandler.getItemNames());
}
public int getStatistic(String statName, String playerName) throws IllegalArgumentException, NullPointerException {
return getStatistic(statName, null, playerName);
}
//returns the integer associated with a certain statistic for a player
public int getStatistic(String statName, String subStatEntryName, String playerName) throws IllegalArgumentException, NullPointerException {
long time = System.currentTimeMillis();
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
plugin.getLogger().info("StatManager 51: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
if (player == null) throw new NullPointerException("No player called " + playerName + " was found!");
Statistic stat = getStatistic(statName);
plugin.getLogger().info("StatManager 56: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
if (stat != null) {
switch (stat.getType()) {
case UNTYPED -> {
plugin.getLogger().info("StatManager 62: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
return player.getStatistic(stat);
}
case BLOCK -> {
plugin.getLogger().info("StatManager 67: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
Material block = enumHandler.getBlock(subStatEntryName);
if (block == null) throw new NullPointerException(subStatEntryName + " is not a valid block name!");
return player.getStatistic(stat, block);
}
case ENTITY -> {
plugin.getLogger().info("StatManager 74: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
EntityType entity = enumHandler.getEntityType(subStatEntryName);
if (entity == null) throw new NullPointerException(subStatEntryName + " is not a valid entity name!");
return player.getStatistic(stat, entity);
}
case ITEM -> {
plugin.getLogger().info("StatManager 81: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
Material item = enumHandler.getItem(subStatEntryName);
if (item == null) throw new NullPointerException(subStatEntryName + " is not a valid item name!");
return player.getStatistic(stat, item);
}
}
}
throw new NullPointerException(statName + " is not a valid statistic name!");
}
//returns the statistic enum constant, or null if non-existent (param: statName, not case sensitive)
private Statistic getStatistic(String statName) {
try {
return Statistic.valueOf(statName.toUpperCase());
}
catch (IllegalArgumentException | NullPointerException exception) {
plugin.logStatRelatedExceptions(exception);
return null;
}
}
//gets the type of the statistic from the string, otherwise returns null (param: statName, not case sensitive)
public Statistic.Type getStatType(String statName) {
try {
return Statistic.valueOf(statName.toUpperCase()).getType();
}
catch (IllegalArgumentException | NullPointerException exception) {
plugin.logStatRelatedExceptions(exception);
return null;
}
}
//checks if string is a valid statistic (param: statName, not case sensitive)
public boolean isStatistic(String statName) {
return statNames.contains(statName.toLowerCase());
}
//checks if string is a valid substatistic dealing with entities (param: statName, not case sensitive)
public boolean isStatEntityType(String statName) {
return entityStatNames.contains(statName.toLowerCase());
}
//checks in the most general sense if this statistic is a substatistic (param: statName, not case sensitive)
public boolean isSubStatEntry(String statName) {
return subStatEntryNames.contains(statName.toLowerCase());
}
//checks whether a subStatEntry is of the type that the statistic requires
public boolean isMatchingSubStatEntry(String statName, String subStatEntry) {
Statistic.Type type = getStatType(statName);
if (type != null && subStatEntry != null) {
switch (type) {
case ENTITY -> {
return enumHandler.isEntityType(subStatEntry);
}
case ITEM -> {
return enumHandler.isItem(subStatEntry);
}
case BLOCK -> {
return enumHandler.isBlock(subStatEntry);
}
case UNTYPED -> {
return false;
}
}
}
return false;
}
//returns the names of all general statistics in lowercase
public List<String> getStatNames() {
return statNames;
}
//returns all statistics that have type entities, in lowercase
public List<String> getEntityTypeNames() {
return entityStatNames;
}
//returns all substatnames in lowercase
public List<String> getSubStatEntryNames() {
return subStatEntryNames;
}
}

View File

@ -0,0 +1,67 @@
package com.gmail.artemis.the.gr8.playerstats;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class StatRequest {
private final CommandSender sender;
private String statName;
private String subStatEntry;
private String playerName;
private boolean playerFlag;
private boolean topFlag;
//playerFlag and topFlag are false by default, will be set to true if "player" or "top" is in the args
public StatRequest(@NotNull CommandSender s) {
sender = s;
playerFlag = false;
topFlag = false;
}
public CommandSender getCommandSender() {
return sender;
}
public String getStatName() {
return statName;
}
public void setStatName(String statName) {
this.statName = statName;
}
public String getSubStatEntry() {
return subStatEntry;
}
public void setSubStatEntry(String subStatEntry) {
this.subStatEntry = subStatEntry;
}
public String getPlayerName() {
return playerName;
}
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
//the "player" arg in the statCommand is a special case, because it could either be a valid subStatEntry, or indicate that the lookup action should target a specific player
//this is why the playerFlag exists - if this is true, and playerName is null, subStatEntry will be "player"
public boolean playerFlag() {
return playerFlag;
}
public void setPlayerFlag(boolean playerFlag) {
this.playerFlag = playerFlag;
}
public boolean topFlag() {
return topFlag;
}
public void setTopFlag(boolean topFlag) {
this.topFlag = topFlag;
}
}

View File

@ -0,0 +1,154 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class StatThread extends Thread {
private final StatRequest request;
private final EnumHandler enumHandler;
private final OutputFormatter outputFormatter;
private final Main plugin;
private String className = "StatThread";
//constructor (called on thread creation)
public StatThread(StatRequest s, EnumHandler e, OutputFormatter o, Main p) {
request = s;
enumHandler = e;
outputFormatter = o;
plugin = p;
}
//what the thread will do once started
@Override
public void run() throws IllegalStateException, NullPointerException {
long time = System.currentTimeMillis();
if (outputFormatter == null || plugin == null) {
throw new IllegalStateException("Not all classes off the plugin are running!");
}
if (request == null) {
throw new NullPointerException("No statistic request was found!");
}
CommandSender sender = request.getCommandSender();
String playerName = request.getPlayerName();
String statName = request.getStatName();
String subStatEntry = request.getSubStatEntry();
boolean topFlag = request.topFlag();
if (playerName != null) {
try {
sender.sendMessage(
outputFormatter.formatPlayerStat(
playerName, statName, subStatEntry, getStatistic(
statName, subStatEntry, playerName)));
plugin.logTimeTaken(className, "run(): individual stat", time, 60);
} catch (Exception e) {
sender.sendMessage(outputFormatter.formatExceptions(e.toString()));
}
} else if (topFlag) {
try {
LinkedHashMap<String, Integer> topStats = getTopStatistics(statName, subStatEntry);
plugin.logTimeTaken(className, "run(): for each loop", time, 69);
String top = outputFormatter.formatTopStats(topStats, statName, subStatEntry);
sender.sendMessage(top);
plugin.logTimeTaken(className, "run(): format output", time, 73);
} catch (Exception e) {
sender.sendMessage(outputFormatter.formatExceptions(e.toString()));
e.printStackTrace();
}
}
}
//returns the integer associated with a certain statistic for a player
private int getStatistic(String statName, String subStatEntryName, String playerName) throws IllegalArgumentException, NullPointerException {
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
Statistic stat = enumHandler.getStatEnum(statName);
if (stat != null) {
return getPlayerStat(player, stat, subStatEntryName);
}
throw new IllegalArgumentException("Statistic " + statName + " could not be retrieved!");
}
throw new IllegalArgumentException("Player object for " + playerName + " could not be retrieved!");
}
private LinkedHashMap<String, Integer> getTopStatistics(String statName, String subStatEntry) {
Statistic stat = enumHandler.getStatEnum(statName);
if (stat != null) {
HashMap<String, Integer> playerStats = new HashMap<>((int) (OfflinePlayerHandler.getOfflinePlayerCount() * 1.05));
OfflinePlayerHandler.getAllOfflinePlayerNames().forEach(playerName -> {
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
if (player != null)
try {
int statistic = getPlayerStat(player, stat, subStatEntry);
if (statistic > 0) {
playerStats.put(playerName, statistic);
}
} catch (IllegalArgumentException ignored) {
}
});
return playerStats.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(10).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}
throw new NullPointerException("Statistic " + statName + " could not be retrieved!");
}
private int getPlayerStat(@NotNull OfflinePlayer player, @NotNull Statistic stat, String subStatEntryName) throws IllegalArgumentException {
switch (stat.getType()) {
case UNTYPED -> {
return player.getStatistic(stat);
}
case BLOCK -> {
Material block = enumHandler.getBlock(subStatEntryName);
if (block != null) {
return player.getStatistic(stat, block);
}
else {
throw new IllegalArgumentException(subStatEntryName + " is not a valid block name!");
}
}
case ENTITY -> {
EntityType entity = enumHandler.getEntityType(subStatEntryName);
if (entity != null) {
return player.getStatistic(stat, entity);
}
else {
throw new IllegalArgumentException(subStatEntryName + " is not a valid entity name!");
}
}
case ITEM -> {
Material item = enumHandler.getItem(subStatEntryName);
if (item != null) {
return player.getStatistic(stat, item);
}
else {
throw new IllegalArgumentException(subStatEntryName + " is not a valid item name!");
}
}
default ->
throw new IllegalArgumentException("This statistic does not seem to be of type:untyped/block/entity/item, I think we should panic");
}
}
}

View File

@ -1,6 +1,8 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
import org.bukkit.ChatColor;
import org.bukkit.command.Command;
@ -12,17 +14,27 @@ public class ReloadCommand implements CommandExecutor {
private final ConfigHandler config;
private final OutputFormatter outputFormatter;
private final Main plugin;
public ReloadCommand(ConfigHandler c, OutputFormatter o) {
public ReloadCommand(ConfigHandler c, OutputFormatter o, Main p) {
outputFormatter = o;
config = c;
plugin = p;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (config.reloadConfig()) {
long time = System.currentTimeMillis();
outputFormatter.updateOutputColors();
time = plugin.logTimeTaken("ReloadCommand", "onCommand", time, 33);
OfflinePlayerHandler.updateOfflinePlayers();
time = plugin.logTimeTaken("ReloadCommand", "onCommand", time, 36);
sender.sendMessage(ChatColor.GREEN + "Config reloaded!");
plugin.logTimeTaken("ReloadCommand", "onCommand", time, 39);
return true;
}
return false;

View File

@ -1,7 +1,9 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.StatManager;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.StatThread;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
import org.bukkit.command.Command;
@ -13,98 +15,81 @@ import org.jetbrains.annotations.NotNull;
public class StatCommand implements CommandExecutor {
private final OfflinePlayerHandler offlinePlayerHandler;
private final OutputFormatter outputFormatter;
private final StatManager statManager;
private final EnumHandler enumHandler;
private final Main plugin;
public StatCommand(OutputFormatter o, StatManager s, Main p) {
public StatCommand(OutputFormatter o, EnumHandler e, Main p) {
outputFormatter = o;
statManager = s;
enumHandler = e;
plugin = p;
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
long time = System.currentTimeMillis();
long startTime = System.currentTimeMillis();
//part 1: collecting all relevant information from the args
if (args.length >= 2) {
StatRequest request = new StatRequest(sender);
String statName = null;
String subStatEntry = null;
String playerName = null;
boolean playerFlag = false;
plugin.getLogger().info("onCommand 40: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
//all args are in lowercase
for (String arg : args) {
if (statManager.isStatistic(arg)) {
statName = (statName == null) ? arg : statName;
plugin.getLogger().info("onCommand 48: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
if (enumHandler.isStatistic(arg) && request.getStatName() == null) {
request.setStatName(arg);
}
else if (statManager.isSubStatEntry(arg)) {
else if (enumHandler.isSubStatEntry(arg)) {
if (arg.equalsIgnoreCase("player")) {
if (!playerFlag) {
subStatEntry = (subStatEntry == null) ? arg : subStatEntry;
playerFlag = true;
plugin.getLogger().info("onCommand 56: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
}
if (request.playerFlag()) {
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
}
else {
subStatEntry = (subStatEntry == null || playerFlag) ? arg : subStatEntry;
plugin.getLogger().info("onCommand 62: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
request.setPlayerFlag(true);
}
}
else {
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
}
}
else if (arg.equalsIgnoreCase("top")) {
request.setTopFlag(true);
}
else if (arg.equalsIgnoreCase("me") && sender instanceof Player) {
playerName = sender.getName();
plugin.getLogger().info("onCommand 69: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
request.setPlayerName(sender.getName());
}
else if (offlinePlayerHandler.isOfflinePlayerName(arg)) {
playerName = (playerName == null) ? arg : playerName;
plugin.getLogger().info("onCommand 74: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
else if (OfflinePlayerHandler.isOfflinePlayerName(arg) && request.getPlayerName() == null) {
request.setPlayerName(arg);
}
}
if (playerName != null && statName != null) {
plugin.getLogger().info("onCommand 79: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
subStatEntry = statManager.isMatchingSubStatEntry(statName, subStatEntry) ? subStatEntry : null;
plugin.getLogger().info("onCommand 82: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
try {
plugin.getLogger().info("onCommand 85: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
int stat = statManager.getStatistic(statName, subStatEntry, playerName);
plugin.getLogger().info("onCommand 89: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
String msg = outputFormatter.formatPlayerStat(playerName, statName, subStatEntry, stat);
plugin.getLogger().info("onCommand 93: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
sender.sendMessage(msg);
plugin.getLogger().info("onCommand 97: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
}
catch (Exception e) {
sender.sendMessage(e.toString());
}
}
}
plugin.getLogger().info("onCommand 106: " + (System.currentTimeMillis() - time));
plugin.getLogger().info("Total time elapsed: " + (System.currentTimeMillis() - startTime));
//part 2: sending the information to the StatThread
if (isValidStatRequest(request)) {
StatThread statThread = new StatThread(request, enumHandler, outputFormatter, plugin);
statThread.start();
plugin.logTimeTaken("StatCommand", "onCommand", time, 71);
return true;
}
}
return false;
}
//check whether all necessary ingredients are present to proceed with a lookup
private boolean isValidStatRequest(StatRequest request) {
if (request.getStatName() != null) {
if (request.topFlag() || request.getPlayerName() != null) {
validatePlayerFlag(request);
return enumHandler.isValidStatEntry(request.getStatName(), request.getSubStatEntry());
}
}
return false;
}
//account for the fact that "player" could be either a subStatEntry or a flag to indicate the target for the lookup, and correct the request if necessary
private void validatePlayerFlag(StatRequest request) {
if (!enumHandler.isValidStatEntry(request.getStatName(), request.getSubStatEntry()) && request.playerFlag()) {
request.setSubStatEntry("player");
}
}
}

View File

@ -1,7 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.StatManager;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.command.Command;
@ -15,16 +14,12 @@ import java.util.stream.Collectors;
public class TabCompleter implements org.bukkit.command.TabCompleter {
private final EnumHandler enumHandler;
private final OfflinePlayerHandler offlinePlayerHandler;
private final StatManager statManager;
private final Main plugin;
private final List<String> commandOptions;
public TabCompleter(EnumHandler e, StatManager s, Main p) {
public TabCompleter(EnumHandler e, Main p) {
enumHandler = e;
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
statManager = s;
plugin = p;
commandOptions = new ArrayList<>();
@ -45,14 +40,14 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
//after typing "stat", suggest a list of viable statistics
if (args.length >= 1) {
if (args.length == 1) {
tabSuggestions = statManager.getStatNames().stream().filter(stat ->
tabSuggestions = enumHandler.getStatNames().stream().filter(stat ->
stat.contains(args[0].toLowerCase())).collect(Collectors.toList());
}
//after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions
else {
if (statManager.isStatistic(args[args.length-2])) {
tabSuggestions = switch (statManager.getStatType(args[args.length-2])) {
if (enumHandler.isStatistic(args[args.length-2])) {
tabSuggestions = switch (enumHandler.getStatType(args[args.length-2])) {
case UNTYPED -> commandOptions;
case BLOCK -> enumHandler.getBlockNames().stream().filter(block ->
block.contains(args[args.length - 1])).collect(Collectors.toList());
@ -61,23 +56,22 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
case ENTITY -> enumHandler.getEntityTypeNames().stream().filter(entity ->
entity.contains(args[args.length - 1])).collect(Collectors.toList());
};
}
//if previous arg = "player", suggest playerNames
else if (args[args.length-2].equalsIgnoreCase("player")) {
if (args.length >= 3 && statManager.getEntityTypeNames().contains(args[args.length-3].toLowerCase())) {
if (args.length >= 3 && enumHandler.getEntityStatNames().contains(args[args.length-3].toLowerCase())) {
tabSuggestions = commandOptions;
}
else {
tabSuggestions = offlinePlayerHandler.getAllOfflinePlayerNames().stream().filter(player ->
tabSuggestions = OfflinePlayerHandler.getAllOfflinePlayerNames().stream().filter(player ->
player.toLowerCase().contains(args[args.length-1].toLowerCase())).collect(Collectors.toList());
}
}
//after a substatistic, suggest commandOptions
else if (statManager.isSubStatEntry(args[args.length-2])) {
else if (enumHandler.isSubStatEntry(args[args.length-2])) {
tabSuggestions = commandOptions;
}
}

View File

@ -1,5 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats;
package com.gmail.artemis.the.gr8.playerstats.filehandlers;
import com.gmail.artemis.the.gr8.playerstats.Main;
import org.bukkit.ChatColor;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
@ -19,24 +20,53 @@ public class ConfigHandler {
saveDefaultConfig();
}
//returns the config setting for use-dots, or the default value "true" if no value can be retrieved
public boolean getUseDots() {
ConfigurationSection ranked = config.getConfigurationSection("ranked-list");
try {
return ranked == null || ranked.getBoolean("use-dots");
}
catch (Exception e) {
e.printStackTrace();
return true;
}
}
//returns a HashMap with all the available color choices, or a ChatColor.RESET if no colors were found
public HashMap<String, ChatColor> getChatColors() {
HashMap<String, ChatColor> chatColors = new HashMap<>();
ConfigurationSection individual = config.getConfigurationSection("individual-statistics");
chatColors.put("playerNames", getChatColor(individual, "player-names"));
chatColors.put("statNames", getChatColor(individual, "stat-names"));
chatColors.put("subStatNames", getChatColor(individual, "sub-stat-names"));
chatColors.put("numbers", getChatColor(individual, "numbers"));
chatColors.put("player-names", getChatColor(individual, "player-names"));
chatColors.put("stat-names", getChatColor(individual, "stat-names"));
chatColors.put("sub-stat-names", getChatColor(individual, "sub-stat-names"));
chatColors.put("stat-numbers", getChatColor(individual, "stat-numbers"));
ConfigurationSection ranked = config.getConfigurationSection("ranked-list");
chatColors.put("playerNamesRanked", getChatColor(ranked, "player-names"));
chatColors.put("statNamesRanked", getChatColor(ranked, "stat-names"));
chatColors.put("subStatNamesRanked", getChatColor(ranked, "sub-stat-names"));
chatColors.put("numbersRanked", getChatColor(ranked, "numbers"));
chatColors.put("player-names-ranked", getChatColor(ranked, "player-names"));
chatColors.put("list-title", getChatColor(ranked, "list-title"));
chatColors.put("sub-stat-names-ranked", getChatColor(ranked, "sub-stat-names"));
chatColors.put("stat-numbers-ranked", getChatColor(ranked, "stat-numbers"));
chatColors.put("list-numbers", getChatColor(ranked, "list-numbers"));
chatColors.put("dots", getChatColor(ranked, "dots"));
return chatColors;
}
//reload the config after changes have been made to it
public boolean reloadConfig() {
try {
if (!configFile.exists()) {
saveDefaultConfig();
}
config = YamlConfiguration.loadConfiguration(configFile);
return true;
}
catch (Exception e) {
e.printStackTrace();
return false;
}
}
//returns the requested entry from the provided configuration section, null if section does not exist, and ChatColor.RESET if there is no entry
private ChatColor getChatColor(ConfigurationSection section, String path) {
ChatColor color;
@ -56,21 +86,6 @@ public class ConfigHandler {
return color;
}
//reload the config after changes have been made to it
public boolean reloadConfig() {
try {
if (!configFile.exists()) {
saveDefaultConfig();
}
config = YamlConfiguration.loadConfiguration(configFile);
return true;
}
catch (Exception e) {
e.printStackTrace();
return false;
}
}
//create a config file if none exists yet (from the config.yml in the plugin's resources)
private void saveDefaultConfig() {
config = plugin.getConfig();

View File

@ -1,21 +1,20 @@
package com.gmail.artemis.the.gr8.playerstats.listeners;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
public class JoinListener implements Listener {
private final OfflinePlayerHandler offlinePlayerHandler;
public JoinListener() {
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
if (!joinEvent.getPlayer().hasPlayedBefore()) {
offlinePlayerHandler.updateOfflinePlayers();
OfflinePlayerHandler.updateOfflinePlayers();
}
}
}

View File

@ -1,32 +1,57 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
import com.gmail.artemis.the.gr8.playerstats.Main;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class EnumHandler {
private final List<String> blockNames;
private final List<String> entityTypeNames;
private final List<String> itemNames;
private final List<String> statNames;
private final List<String> entityStatNames;
private final List<String> subStatEntryNames;
private final Main plugin;
public EnumHandler() {
public EnumHandler(Main p) {
plugin = p;
blockNames = Arrays.stream(Material.values()).filter(
Material::isBlock).map(Material::toString).map(String::toLowerCase).toList();
entityTypeNames = Arrays.stream(EntityType.values()).map(
EntityType::toString).map(String::toLowerCase).toList();
itemNames = Arrays.stream(Material.values()).filter(
Material::isItem).map(Material::toString).map(String::toLowerCase).toList();
statNames = Arrays.stream(Statistic.values()).map(
Statistic::toString).map(String::toLowerCase).toList();
entityStatNames = Arrays.stream(Statistic.values()).filter(statistic ->
statistic.getType().equals(Statistic.Type.ENTITY)).map(
Statistic::toString).map(String::toLowerCase).collect(Collectors.toList());
subStatEntryNames = new ArrayList<>();
subStatEntryNames.addAll(getBlockNames());
subStatEntryNames.addAll(getEntityTypeNames());
subStatEntryNames.addAll(getItemNames());
}
//checks whether the provided string is a valid item
public boolean isItem(String itemName) {
return itemNames.contains(itemName.toLowerCase());
}
//returns corresponding item enum constant (uppercase), otherwise null (param: itemName, not case sensitive)
@Nullable
public Material getItem(String itemName) {
return Material.matchMaterial(itemName);
}
@ -36,18 +61,25 @@ public class EnumHandler {
return itemNames;
}
//checks whether the provided string is a valid entity
public boolean isEntityType(String entityName) {
return entityTypeNames.contains(entityName.toLowerCase());
}
//returns EntityType enum constant (uppercase) if the input name is valid, otherwise null (param: entityName, not case sensitive)
@Nullable
public EntityType getEntityType(String entityName) {
EntityType entityType = null;
EntityType entityType;
try {
entityType = EntityType.valueOf(entityName.toUpperCase());
}
catch (IllegalArgumentException | NullPointerException exception) {
exception.printStackTrace();
catch (IllegalArgumentException e) {
plugin.getLogger().warning("IllegalArgumentException: " + entityName + " is not a valid statistic name!");
return null;
}
catch (NullPointerException e) {
plugin.getLogger().warning("NullPointerException: please provide a statistic name!");
return null;
}
return entityType;
}
@ -57,11 +89,13 @@ public class EnumHandler {
return entityTypeNames;
}
//checks whether the provided string is a valid block
public boolean isBlock(String materialName) {
return blockNames.contains(materialName.toLowerCase());
}
//returns corresponding block enum constant (uppercase), otherwise null (param: materialName, not case sensitive)
@Nullable
public Material getBlock(String materialName) {
return Material.matchMaterial(materialName);
}
@ -71,4 +105,85 @@ public class EnumHandler {
return blockNames;
}
//returns the statistic enum constant, or null if non-existent (param: statName, not case sensitive)
public Statistic getStatEnum(String statName) {
try {
return Statistic.valueOf(statName.toUpperCase());
}
catch (IllegalArgumentException e) {
plugin.getLogger().warning("IllegalArgumentException: " + statName + " is not a valid statistic name!");
return null;
}
catch (NullPointerException e) {
plugin.getLogger().warning("NullPointerException: please provide a statistic name!");
return null;
}
}
//gets the type of the statistic from the string, otherwise returns null (param: statName, not case sensitive)
public Statistic.Type getStatType(String statName) {
try {
return Statistic.valueOf(statName.toUpperCase()).getType();
}
catch (IllegalArgumentException e) {
plugin.getLogger().warning("IllegalArgumentException: " + statName + " is not a valid statistic name!");
return null;
}
catch (NullPointerException e) {
plugin.getLogger().warning("NullPointerException: please provide a statistic name!");
return null;
}
}
//checks if string is a valid statistic (param: statName, not case sensitive)
public boolean isStatistic(String statName) {
return statNames.contains(statName.toLowerCase());
}
//returns the names of all general statistics in lowercase
public List<String> getStatNames() {
return statNames;
}
//returns all statistics that have type entities, in lowercase
public List<String> getEntityStatNames() {
return entityStatNames;
}
//checks if this statistic is a subStatEntry, meaning it is a block, item or entity (param: statName, not case sensitive)
public boolean isSubStatEntry(String statName) {
return subStatEntryNames.contains(statName.toLowerCase());
}
//checks if string is a valid statistic (param: statName, not case sensitive)
public boolean isValidStatEntry(String statName) {
return isValidStatEntry(statName, null);
}
//checks whether a subStatEntry is of the type that the statistic requires
public boolean isValidStatEntry(String statName, String subStatEntry) {
Statistic stat = getStatEnum(statName);
return (stat != null && isMatchingSubStatEntry(stat, subStatEntry));
}
//returns true if subStatEntry matches the type the stat requires, or if stat is untyped and subStatEntry is null
private boolean isMatchingSubStatEntry(@NotNull Statistic stat, String subStatEntry) {
switch (stat.getType()) {
case ENTITY -> {
return subStatEntry != null && isEntityType(subStatEntry);
}
case ITEM -> {
return subStatEntry != null && isItem(subStatEntry);
}
case BLOCK -> {
return subStatEntry != null && isBlock(subStatEntry);
}
case UNTYPED -> {
return subStatEntry==null;
}
default -> {
return false;
}
}
}
}

View File

@ -4,51 +4,60 @@ import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import java.util.*;
import java.util.stream.Collectors;
public class OfflinePlayerHandler {
private static OfflinePlayerHandler instance;
private List<OfflinePlayer> offlinePlayers;
private List<String> offlinePlayerNames;
private HashMap<String, OfflinePlayer> offlinePlayerMap;
private static HashMap<String, OfflinePlayer> offlinePlayerMap;
private static List<String> offlinePlayerNames;
private static int totalOfflinePlayers;
private OfflinePlayerHandler() {
updateOfflinePlayers();
}
public static OfflinePlayerHandler getInstance() {
if (instance == null) {
instance = new OfflinePlayerHandler();
}
return instance;
}
public boolean isOfflinePlayerName(String playerName) {
public static boolean isOfflinePlayerName(String playerName) {
return offlinePlayerNames.contains(playerName);
}
public OfflinePlayer getOfflinePlayer(String playerName) {
long time = System.currentTimeMillis();
OfflinePlayer player = offlinePlayerMap.get(playerName);
System.out.println(("OfflinePlayerHandler 35: " + (System.currentTimeMillis() - time)));
return player;
public static OfflinePlayer getOfflinePlayer(String playerName) {
return offlinePlayerMap.get(playerName);
}
public List<OfflinePlayer> getAllOfflinePlayers() {
return offlinePlayers;
public static int getOfflinePlayerCount() {
return totalOfflinePlayers > 0 ? totalOfflinePlayers : 1;
}
public List<String> getAllOfflinePlayerNames() {
public static List<String> getAllOfflinePlayerNames() {
return offlinePlayerNames;
}
public void updateOfflinePlayers() {
offlinePlayerMap = new HashMap<>();
offlinePlayers = Arrays.stream(Bukkit.getOfflinePlayers()).filter(offlinePlayer ->
offlinePlayer.getName() != null && offlinePlayer.hasPlayedBefore()).collect(Collectors.toList());
offlinePlayerNames = offlinePlayers.stream().map(OfflinePlayer::getName).collect(Collectors.toList());
offlinePlayers.forEach(offlinePlayer -> offlinePlayerMap.put(offlinePlayer.getName(), offlinePlayer));
//stores a private HashMap with keys:playerName and values:OfflinePlayer, and a private list of the names for easy access
public static void updateOfflinePlayers() {
long totalTime = System.currentTimeMillis();
long time = System.currentTimeMillis();
if (offlinePlayerMap == null) offlinePlayerMap = new HashMap<>();
else if (!offlinePlayerMap.isEmpty()) {
offlinePlayerMap.clear();
}
if (offlinePlayerNames == null) offlinePlayerNames = new ArrayList<>();
else if (!offlinePlayerNames.isEmpty()) {
offlinePlayerNames.clear();
}
Arrays.stream(Bukkit.getOfflinePlayers()).filter(offlinePlayer ->
offlinePlayer.getName() != null && offlinePlayer.hasPlayedBefore()).forEach(offlinePlayer -> {
offlinePlayerNames.add(offlinePlayer.getName());
offlinePlayerMap.put(offlinePlayer.getName(), offlinePlayer);
});
System.out.println("OfflinePlayerHandler, making the HashMap and ArrayList: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
totalOfflinePlayers = offlinePlayerMap.size();
System.out.println("OfflinePlayerHandler, counting the HashMap: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
totalOfflinePlayers = offlinePlayerNames.size();
System.out.println("OfflinePlayerHandler, counting the ArrayList: " + (System.currentTimeMillis() - time));
System.out.println("updateOfflinePlayers total time: " + (System.currentTimeMillis() - totalTime));
}
}

View File

@ -1,51 +1,72 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
import com.gmail.artemis.the.gr8.playerstats.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import org.bukkit.ChatColor;
import org.bukkit.map.MinecraftFont;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.*;
public class OutputFormatter {
//keys for the HashMap are:
//playerNames(Ranked)
//statNames(Ranked)
//subStatNames(Ranked)
//numbers(Ranked)
//keys for the HashMap are the same as the config options (so e.g. player-names/player-names-ranked)
private final ConfigHandler config;
private HashMap<String, ChatColor> chatColors;
private final String pluginPrefix;
public OutputFormatter(ConfigHandler c) {
config = c;
pluginPrefix = ChatColor.GRAY + "[" + ChatColor.GOLD + "PlayerStats" + ChatColor.GRAY + "] " + ChatColor.RESET;
updateOutputColors();
}
public String formatTopStats(LinkedHashMap<String, Integer> topStats) {
return "";
}
public String formatPlayerStat(String playerName, String statName, int stat) {
return formatPlayerStat(playerName, statName, null, stat);
public String formatExceptions(String exception) {
return pluginPrefix + exception;
}
public String formatPlayerStat(String playerName, String statName, String subStatEntryName, int stat) {
long time = System.currentTimeMillis();
System.out.println("OutputFormatter 33: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
String subStat = subStatEntryName != null ?
chatColors.get("subStatNames") + " (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
chatColors.get("sub-stat-names") + " (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
System.out.println("OutputFormatter 39: " + (System.currentTimeMillis() - time));
time = System.currentTimeMillis();
return chatColors.get("player-names") + playerName + chatColors.get("stat-numbers") + ": " + stat + " " +
chatColors.get("stat-names") + statName.toLowerCase().replace("_", " ") + subStat;
}
String msg = chatColors.get("playerNames") + playerName + chatColors.get("numbers") + ": " + stat + " " +
chatColors.get("statNames") + statName.toLowerCase().replace("_", " ") + subStat;
public String formatTopStats(LinkedHashMap<String, Integer> topStats, String statName, String subStatEntryName) {
String subStat = subStatEntryName != null ?
chatColors.get("sub-stat-names-ranked") + " (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
String topCount = chatColors.get("list-numbers") + " " + topStats.size();
String title = "\n" + pluginPrefix + chatColors.get("list-title") + "Top" + topCount + chatColors.get("list-title") + " " +
statName.toLowerCase().replace("_", " ") + subStat;
System.out.println("OutputFormatter 45: " + (System.currentTimeMillis() - time));
return msg;
boolean useDots = config.getUseDots();
int count = 0;
Set<String> playerNames = topStats.keySet();
MinecraftFont font = new MinecraftFont();
StringBuilder rankList = new StringBuilder();
for (String playerName : playerNames) {
count = count+1;
rankList.append("\n")
.append(chatColors.get("list-numbers")).append(count).append(". ")
.append(chatColors.get("player-names-ranked")).append(playerName)
.append(chatColors.get("dots"));
if (useDots) {
rankList.append(" ");
int dots = (int) Math.round((125.0 - font.getWidth(count + ". " + playerName))/2);
if (dots >= 1) {
rankList.append(".".repeat(dots));
}
}
else {
rankList.append(":");
}
rankList.append(" ").append(chatColors.get("stat-numbers-ranked")).append(topStats.get(playerName).toString());
}
return title + rankList;
}
public void updateOutputColors() {

View File

@ -1,15 +1,23 @@
# PlayerStats Configuration
# --- Color Options ---
# supports: all default Minecraft colors
# --- General Options ---
# --- Format & Color Options ---
individual-statistics:
player-names: gold
stat-names: yellow
sub-stat-names: yellow
numbers: white
stat-numbers: white
ranked-list:
player-names: gold
stat-names: yellow
player-names: green
list-title: yellow
sub-stat-names: yellow
numbers: white
stat-numbers: white
list-numbers: gold
# If true, the statistics will be aligned so that they are all underneath each other
use-dots: true
dots: dark_gray