Merge pull request #45 from itHotL/performance

Performance
This commit is contained in:
Artemis-the-gr8 2022-06-07 16:04:26 +02:00 committed by GitHub
commit b117c2b88a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1052 additions and 533 deletions

View File

@ -5,7 +5,6 @@ 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.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.MessageFactory;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.Bukkit;
@ -34,15 +33,14 @@ public class Main extends JavaPlugin {
//get instances of the classes that should be initialized
ConfigHandler config = new ConfigHandler(this);
MessageFactory messageFactory = new MessageFactory(config, this);
OfflinePlayerHandler offlinePlayerHandler = new OfflinePlayerHandler(config);
ThreadManager threadManager = new ThreadManager(this, adventure(), config, offlinePlayerHandler, messageFactory);
MessageFactory messageFactory = new MessageFactory(config);
ThreadManager threadManager = new ThreadManager(this, adventure(), config, messageFactory);
//register the commands
PluginCommand statcmd = this.getCommand("statistic");
if (statcmd != null) {
statcmd.setExecutor(new StatCommand(threadManager, adventure(),offlinePlayerHandler, messageFactory));
statcmd.setTabCompleter(new TabCompleter(offlinePlayerHandler));
statcmd.setExecutor(new StatCommand(threadManager, adventure(), messageFactory));
statcmd.setTabCompleter(new TabCompleter());
}
PluginCommand reloadcmd = this.getCommand("statisticreload");
if (reloadcmd != null) reloadcmd.setExecutor(new ReloadCommand(threadManager));

View File

@ -1,66 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.utils.MessageFactory;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.Nullable;
public class ReloadThread extends Thread {
private final ConfigHandler config;
private final OfflinePlayerHandler offlinePlayerHandler;
private final Main plugin;
private final StatThread statThread;
private final CommandSender sender;
private final boolean firstTimeLoading;
public ReloadThread(ConfigHandler c, OfflinePlayerHandler o, Main p, @Nullable StatThread s, @Nullable CommandSender se, boolean firstTime) {
config = c;
offlinePlayerHandler = o;
plugin = p;
statThread = s;
sender = se;
firstTimeLoading = firstTime;
plugin.getLogger().info("ReloadThread created");
}
@Override
public void run() {
long time = System.currentTimeMillis();
if (!firstTimeLoading) {
if (statThread != null && statThread.isAlive()) {
try {
plugin.getLogger().info("Waiting for statThread to finish up...");
statThread.join();
} catch (InterruptedException e) {
plugin.getLogger().warning(e.toString());
throw new RuntimeException(e);
}
}
plugin.getLogger().info("Reloading!");
if (config.reloadConfig()) {
offlinePlayerHandler.updateOfflinePlayerList();
plugin.getLogger().info("Amount of relevant players: " + offlinePlayerHandler.getOfflinePlayerCount());
plugin.logTimeTaken("ReloadThread", "loading offline players", time);
if (sender != null) {
sender.sendMessage(MessageFactory.getPluginPrefix() + ChatColor.GREEN + "Config reloaded!");
}
}
}
else {
plugin.getLogger().info("Loading offline players...");
offlinePlayerHandler.updateOfflinePlayerList();
plugin.getLogger().info("Amount of relevant players: " + offlinePlayerHandler.getOfflinePlayerCount());
plugin.logTimeTaken("ReloadThread", "loading offline players", time);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
}
}
}

View File

@ -1,43 +1,46 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.TestFileHandler;
import com.gmail.artemis.the.gr8.playerstats.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.utils.MessageFactory;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.command.CommandSender;
public class ThreadManager {
private static final int threshold = 10;
private final Main plugin;
private final BukkitAudiences adventure;
private final ConfigHandler config;
private final OfflinePlayerHandler offlinePlayerHandler;
private static ConfigHandler config;
private static TestFileHandler testFile;
private final MessageFactory messageFactory;
private ReloadThread reloadThread;
private StatThread statThread;
private static long lastRecordedCalcTime;
public ThreadManager(Main p, BukkitAudiences b, ConfigHandler c, OfflinePlayerHandler o, MessageFactory m) {
public ThreadManager(Main p, BukkitAudiences b, ConfigHandler c, MessageFactory m) {
plugin = p;
adventure = b;
config = c;
offlinePlayerHandler = o;
messageFactory = m;
testFile = new TestFileHandler(plugin);
startReloadThread(null, true);
}
public void startReloadThread(CommandSender sender, boolean firstTimeLoading) {
reloadThread = new ReloadThread(config, offlinePlayerHandler, plugin, statThread, sender, firstTimeLoading);
reloadThread = new ReloadThread(threshold, adventure, config, testFile, messageFactory, plugin, statThread, sender, firstTimeLoading);
reloadThread.start();
}
public void startStatThread(StatRequest request) {
statThread = new StatThread(request, reloadThread, adventure, config, offlinePlayerHandler, messageFactory, plugin);
statThread = new StatThread(threshold, request, reloadThread, adventure, config, testFile, messageFactory, plugin);
statThread.start();
}

View File

@ -1,6 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.enums.Query;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
@ -19,41 +20,109 @@ public class StatCommand implements CommandExecutor {
private final ThreadManager threadManager;
private final BukkitAudiences adventure;
private final OfflinePlayerHandler offlinePlayerHandler;
private final MessageFactory messageFactory;
public StatCommand(ThreadManager t, BukkitAudiences b, OfflinePlayerHandler of, MessageFactory o) {
public StatCommand(ThreadManager t, BukkitAudiences b, MessageFactory o) {
threadManager = t;
adventure = b;
offlinePlayerHandler = of;
messageFactory = o;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
//part 1: collecting all relevant information from the args
if (args.length >= 1) {
if (args.length >= 1) { //part 1: collecting all relevant information from the args
StatRequest request = generateRequest(sender, args);
//part 2: sending the information to the StatThread, or give feedback if request is invalid
if (isValidStatRequest(request)) {
if (isValidStatRequest(request)) { //part 2: sending the information to the StatThread
threadManager.startStatThread(request);
return true;
}
else {
else { //part 2: or give feedback if request is invalid
adventure.sender(sender).sendMessage(getRelevantFeedback(request));
return false;
}
}
//in case of less than 1 argument, always display the help message
else {
else { //in case of less than 1 argument, display the help message
adventure.sender(sender).sendMessage(messageFactory.helpMsg());
return false;
}
}
//create a StatRequest Object with all the relevant information from the args
private StatRequest generateRequest(CommandSender sender, String[] args) {
StatRequest request = new StatRequest(sender);
for (String arg : args) {
//check for statName
if (EnumHandler.isStatistic(arg) && request.getStatName() == null) {
request.setStatName(arg);
}
//check for subStatEntry and playerFlag
else if (EnumHandler.isSubStatEntry(arg)) {
if (arg.equalsIgnoreCase("player") && !request.playerFlag()) {
request.setPlayerFlag(true);
}
else {
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
}
}
//check for selection
else if (request.getSelection() == null) {
if (arg.equalsIgnoreCase("top")) {
request.setSelection(Query.TOP);
}
else if (arg.equalsIgnoreCase("server")) {
request.setSelection(Query.SERVER);
}
else if (arg.equalsIgnoreCase("me") && sender instanceof Player) {
request.setPlayerName(sender.getName());
request.setSelection(Query.PLAYER);
}
else if (OfflinePlayerHandler.isOfflinePlayerName(arg) && request.getPlayerName() == null) {
request.setPlayerName(arg);
request.setSelection(Query.PLAYER);
}
}
}
return request;
}
//part 2: check whether all necessary ingredients are present to proceed with a lookup
private boolean isValidStatRequest(StatRequest request) {
if (request.getStatName() != null) {
if (request.playerFlag()) unpackPlayerFlag(request);
if (request.getSelection() == null) assumeTopAsDefault(request);
if (request.getSubStatEntry() != null) verifySubStat(request);
return EnumHandler.isValidStatEntry(request.getStatType(), request.getSubStatEntry());
}
return false;
}
//account for the fact that "player" could be either a subStatEntry, a flag to indicate the target for the lookup, or both
private void unpackPlayerFlag(StatRequest request) {
if (request.getStatType() == Statistic.Type.ENTITY && request.getSubStatEntry() == null) {
request.setSubStatEntry("player");
}
if (request.getSelection() == null) {
request.setSelection(Query.PLAYER);
}
}
//in case the statistic is untyped, set the unnecessary subStatEntry to null
private void verifySubStat(StatRequest request) {
if (request.getSubStatEntry() != null && request.getStatType() == Statistic.Type.UNTYPED) {
request.setSubStatEntry(null);
}
}
//if no playerName was provided, and there is no topFlag or serverFlag, substitute a top flag
private void assumeTopAsDefault(StatRequest request) {
request.setSelection(Query.TOP);
}
//call this method when isValidStatRequest has returned false to get a relevant error-message
private TextComponent getRelevantFeedback(@NotNull StatRequest request) {
if (request.getStatName() == null) {
return messageFactory.missingStatName();
@ -64,74 +133,9 @@ public class StatCommand implements CommandExecutor {
else if (!EnumHandler.isValidStatEntry(request.getStatType(), request.getSubStatEntry())){
return messageFactory.wrongSubStatType(request.getStatType(), request.getSubStatEntry());
}
else if (!request.topFlag()) {
if (!request.playerFlag()) {
return messageFactory.missingTarget();
}
else {
return messageFactory.missingPlayerName();
}
else if (request.getSelection() == Query.PLAYER && request.getPlayerName() == null) {
return messageFactory.missingPlayerName();
}
return messageFactory.unknownError();
}
//part 1: create a StatRequest Object with all the relevant information from the args
private StatRequest generateRequest(CommandSender sender, String[] args) {
StatRequest request = new StatRequest(sender);
for (String arg : args) {
if (EnumHandler.isStatistic(arg) && request.getStatName() == null) {
request.setStatName(arg);
}
else if (EnumHandler.isSubStatEntry(arg)) {
if (arg.equalsIgnoreCase("player")) {
if (request.playerFlag()) {
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
}
else {
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) {
request.setPlayerName(sender.getName());
}
else if (offlinePlayerHandler.isOfflinePlayerName(arg) && request.getPlayerName() == null) {
request.setPlayerName(arg);
}
}
return request;
}
//part 2: check whether all necessary ingredients are present to proceed with a lookup
private boolean isValidStatRequest(StatRequest request) {
validatePlayerFlag(request);
removeUnnecessarySubStat(request);
if (request.getStatName() != null) {
if (request.topFlag() || request.getPlayerName() != null) {
return EnumHandler.isValidStatEntry(request.getStatType(), 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 (request.getStatType() == Statistic.Type.ENTITY && request.getSubStatEntry() == null && request.playerFlag()) {
request.setSubStatEntry("player");
}
}
private void removeUnnecessarySubStat(StatRequest request) {
if (request.getSubStatEntry() != null && request.getStatType() == Statistic.Type.UNTYPED) {
request.setSubStatEntry(null);
}
}
}

View File

@ -12,16 +12,14 @@ import java.util.stream.Collectors;
public class TabCompleter implements org.bukkit.command.TabCompleter {
private final OfflinePlayerHandler offlinePlayerHandler;
private final List<String> commandOptions;
public TabCompleter(OfflinePlayerHandler o) {
offlinePlayerHandler = o;
public TabCompleter() {
commandOptions = new ArrayList<>();
commandOptions.add("top");
commandOptions.add("player");
commandOptions.add("server");
commandOptions.add("me");
}
@ -61,7 +59,7 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
tabSuggestions = commandOptions;
}
else {
tabSuggestions = offlinePlayerHandler.getOfflinePlayerNames().stream().filter(player ->
tabSuggestions = OfflinePlayerHandler.getOfflinePlayerNames().stream().filter(player ->
player.toLowerCase().contains(args[args.length-1].toLowerCase())).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
public enum Query {
PLAYER, SERVER, TOP
}

View File

@ -1,9 +1,11 @@
package com.gmail.artemis.the.gr8.playerstats.filehandlers;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.enums.Query;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.jetbrains.annotations.Nullable;
import java.io.File;
@ -17,9 +19,10 @@ public class ConfigHandler {
public ConfigHandler(Main p) {
plugin = p;
saveDefaultConfig();
config = YamlConfiguration.loadConfiguration(configFile);
}
//reload the config after changes have been made to it
/** Reloads the config from file, or creates a new file with default values if there is none. */
public boolean reloadConfig() {
try {
if (!configFile.exists()) {
@ -34,67 +37,51 @@ public class ConfigHandler {
}
}
//returns the config setting for include-whitelist-only, or the default value "false"
/** Returns the config setting for include-whitelist-only, or the default value "false". */
public boolean whitelistOnly() {
try {
return config.getBoolean("include-whitelist-only");
}
catch (Exception e) {
plugin.getLogger().warning(e.toString());
return false;
}
return config.getBoolean("include-whitelist-only", false);
}
//returns the config setting for exclude-banned-players, or the default value "false"
/** Returns the config setting for exclude-banned-players, or the default value "false". */
public boolean excludeBanned() {
try {
return config.getBoolean("exclude-banned-players");
}
catch (Exception e) {
plugin.getLogger().warning(e.toString());
return false;
}
return config.getBoolean("exclude-banned-players", false);
}
//returns the number of maximum days since a player has last been online, or the default value of 0 to not use this constraint
/** Returns the number of maximum days since a player has last been online, or the default value of 0 to not use this constraint. */
public int lastPlayedLimit() {
try {
return config.getInt("number-of-days-since-last-joined");
}
catch (Exception e) {
plugin.getLogger().warning(e.toString());
return 0;
}
return config.getInt("number-of-days-since-last-joined", 0);
}
//returns the config setting for top-list-max-size, or the default value of 10 if no value can be retrieved
/** Returns the config setting for top-list-max-size, or the default value of 10 if no value can be retrieved. */
public int getTopListMaxSize() {
try {
return config.getInt("top-list-max-size");
}
catch (Exception e) {
plugin.getLogger().warning(e.toString());
return 10;
}
return config.getInt("top-list-max-size", 10);
}
//returns the config setting for use-dots, or the default value "true" if no value can be retrieved
/** Returns the config setting for use-dots, or the default value "true" if no value can be retrieved. */
public boolean useDots() {
try {
return config.getBoolean("use-dots");
}
catch (Exception e) {
plugin.getLogger().warning(e.toString());
return true;
}
return config.getBoolean("use-dots", true);
}
public String getPlayerNameFormatting(boolean topStat, boolean isStyle) {
return getStringFromConfig(topStat, isStyle, "player-names");
/** Returns the specified server name, or "this server" if no value can be retrieved. */
public String getServerName() {
return config.getString("your-server-name", "this server");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "green" or "gold" for Color (for top or individual color). */
public String getPlayerNameFormatting(Query selection, boolean isStyle) {
String def;
if (selection == Query.TOP) {
def = "green";
}
else {
def = "gold";
}
return getStringFromConfig(selection, isStyle, def, "player-names");
}
public boolean playerNameIsBold() {
ConfigurationSection style = getRelevantSection(true, true);
ConfigurationSection style = getRelevantSection(Query.PLAYER);
if (style != null) {
String styleString = style.getString("player-names");
@ -103,53 +90,96 @@ public class ConfigHandler {
return false;
}
public String getStatNameFormatting(boolean topStat, boolean isStyle) {
return getStringFromConfig(topStat, isStyle, "stat-names");
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "yellow" for Color. */
public String getStatNameFormatting(Query selection, boolean isStyle) {
return getStringFromConfig(selection, isStyle, "yellow", "stat-names");
}
public String getSubStatNameFormatting(boolean topStat, boolean isStyle) {
return getStringFromConfig(topStat, isStyle, "sub-stat-names");
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "#FFD52B" for Color. */
public String getSubStatNameFormatting(Query selection, boolean isStyle) {
return getStringFromConfig(selection, isStyle, "#FFD52B", "sub-stat-names");
}
public String getStatNumberFormatting(boolean topStat, boolean isStyle) {
return getStringFromConfig(topStat, isStyle, "stat-numbers");
}
public String getListNumberFormatting(boolean isStyle) {
return getStringFromConfig(true, isStyle, "list-numbers");
}
public String getDotsColor() {
return getStringFromConfig(true, false, "dots");
}
//returns the config value for a color or style option in string-format, or null if no value was found
private String getStringFromConfig(boolean topStat, boolean isStyle, String pathName){
ConfigurationSection section = getRelevantSection(topStat, isStyle);
return section != null ? section.getString(pathName) : null;
}
//returns the config section that contains the relevant color or style option
private ConfigurationSection getRelevantSection(boolean topStat, boolean isStyle) {
ConfigurationSection section;
try {
if (!topStat) {
if (!isStyle) section = config.getConfigurationSection("individual-statistics-color");
else section = config.getConfigurationSection("individual-statistics-style");
}
else {
if (!isStyle) section = config.getConfigurationSection("top-list-color");
else section = config.getConfigurationSection("top-list-style");
}
return section;
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "#55AAFF" or "#ADE7FF" for Color (for the top or individual/server color). */
public String getStatNumberFormatting(Query selection, boolean isStyle) {
String def;
if (selection == Query.TOP) {
def = "#55AAFF";
}
catch (IllegalArgumentException | NullPointerException exception) {
plugin.getLogger().warning(exception.toString());
return null;
else {
def = "#ADE7FF";
}
return getStringFromConfig(selection, isStyle, def,"stat-numbers");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "yellow" or "gold" for Color (for top/server). */
public String getTitleFormatting(Query selection, boolean isStyle) {
String def;
if (selection == Query.TOP) {
def = "yellow";
}
else {
def = "gold";
}
return getStringFromConfig(selection, isStyle, def, "title");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "gold" for Color. */
public String getTitleNumberFormatting(boolean isStyle) {
return getStringFromConfig(Query.TOP, isStyle, "gold", "title-number");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "#FFB80E" for Color. */
public String getServerNameFormatting(boolean isStyle) {
return getStringFromConfig(Query.SERVER, isStyle, "#FFB80E", "server-name");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "gold" for Color. */
public String getRankNumberFormatting(boolean isStyle) {
return getStringFromConfig(Query.TOP, isStyle, "gold", "rank-numbers");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are "none" for Style,
and "dark_gray" for Color. */
public String getDotsFormatting(boolean isStyle) {
return getStringFromConfig(Query.TOP, isStyle, "dark_gray", "dots");
}
/** Returns the config value for a color or style option in string-format, the supplied default value, or null if no configSection was found. */
private @Nullable String getStringFromConfig(Query selection, boolean isStyle, String def, String pathName){
String path = isStyle ? pathName + "-style" : pathName;
String defaultValue = isStyle ? "none" : def;
ConfigurationSection section = getRelevantSection(selection);
return section != null ? section.getString(path, defaultValue) : null;
}
/** Returns the config section that contains the relevant color or style option. */
private @Nullable ConfigurationSection getRelevantSection(Query selection) {
switch (selection) {
case TOP -> {
return config.getConfigurationSection("top-list");
}
case PLAYER -> {
return config.getConfigurationSection("individual-statistics");
}
case SERVER -> {
return config.getConfigurationSection("total-server");
}
default -> {
return null;
}
}
}
//create a config file if none exists yet (from the config.yml in the plugin's resources)
/** Create a config file if none exists yet (from the config.yml in the plugin's resources). */
private void saveDefaultConfig() {
config = plugin.getConfig();
plugin.saveDefaultConfig();

View File

@ -0,0 +1,161 @@
package com.gmail.artemis.the.gr8.playerstats.filehandlers;
import com.gmail.artemis.the.gr8.playerstats.Main;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
import java.io.IOException;
public class TestFileHandler {
private File testFile;
private FileConfiguration testConf;
private ConfigurationSection number;
private final Main plugin;
private String onEnable;
private String reload;
private String debugging;
private String topStat;
public TestFileHandler(Main p) {
plugin = p;
onEnable = "onEnable";
reload = "reload";
debugging = "exception-debugging";
topStat = "top-stat";
}
/**
* Creates a new config section for the given threshold. Only needs to be called once, unless threshold changes.
* @param count amount of players to calculate statistics with
* @param threshold how small the subTasks have to become
*/
public void saveThreshold(int count, int threshold) {
loadFile(count);
String path = threshold + " threshold";
try {
number = testConf.getConfigurationSection(path);
if (number == null) {
number = testConf.createSection(path);
number.createSection(onEnable);
number.createSection(reload);
number.createSection(debugging);
number.createSection(topStat);
}
else {
number = testConf.getConfigurationSection(path);
}
saveFile();
}
catch (Exception e) {
e.printStackTrace();
}
}
public void logRunCount(boolean errorEncountered) {
try {
ConfigurationSection section = number.getConfigurationSection(debugging);
if (section != null) {
int runs = section.getInt("runs");
section.set("runs", runs +1);
if (errorEncountered) {
int errors = section.getInt("errors");
section.set("errors", errors + 1);
String path = "error-" + (errors + 1) + "-during-run";
int lastError = section.getInt("error-" + errors + "-during-run");
int runsUntilError = runs - lastError;
String path2 = "until-error-" + (errors + 1);
section.set(path2, runsUntilError);
section.set(path, runs);
}
saveFile();
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Logs how long a certain method took for the earlier set threshold. Always make sure saveThreshold has been
* called once before this method is called.
* @param time how long the given action took
* @param scenario describes which section to get. 1 means onEnable, 2 means reload, and 3 means top-stat
*/
public void saveTimeTaken(long time, int scenario) {
String path = "";
if (scenario == 1) path = onEnable;
else if (scenario == 2) path = reload;
else if (scenario == 3) path = topStat;
try {
ConfigurationSection section = number.getConfigurationSection(path);
if (section != null) {
saveTimeToSection(time, section);
saveFile();
}
}
catch (Exception e) {
e.printStackTrace();
}
}
private void saveTimeToSection(long time, ConfigurationSection section) {
if (section.contains("average")) {
long average = section.getLong("average");
long newAverage = ((average * (section.getKeys(false).size() -1)) + time)/section.getKeys(false).size();
section.set(section.getKeys(false).size() + "", time);
section.set("average", newAverage);
}
else {
section.set("average", time);
section.set("1", time);
}
}
private void loadFile(int players) {
String fileName = "test_" + players + ".yml";
testFile = new File(plugin.getDataFolder(), fileName);
if (!testFile.exists()) {
plugin.getLogger().info("Attempting to create testFile...");
createFile();
}
testConf = new YamlConfiguration();
try {
testConf.load(testFile);
}
catch (IOException | InvalidConfigurationException exception) {
exception.printStackTrace();
}
saveFile();
}
private void createFile() {
testFile.getParentFile().mkdirs();
try {
testFile.createNewFile();
plugin.getLogger().info("Even though this would return false, secretly a file has been created anyway");
}
catch (IOException e) {
e.printStackTrace();
}
}
private void saveFile() {
try {
testConf.save(testFile);
}
catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,75 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.utils.UnixTimeHandler;
import org.bukkit.OfflinePlayer;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
public class ReloadAction extends RecursiveAction {
private final int threshold;
private final OfflinePlayer[] players;
private final int start;
private final int end;
private final boolean whitelistOnly;
private final boolean excludeBanned;
private final int lastPlayedLimit;
private final ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
public ReloadAction(int threshold, OfflinePlayer[] players,
boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit,
ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
this(threshold, players, 0, players.length,
whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs);
}
protected ReloadAction(int threshold, OfflinePlayer[] players, int start, int end,
boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit,
ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
this.threshold = threshold;
this.players = players;
this.start = start;
this.end = end;
this.whitelistOnly = whitelistOnly;
this.excludeBanned = excludeBanned;
this.lastPlayedLimit = lastPlayedLimit;
this.offlinePlayerUUIDs = offlinePlayerUUIDs;
}
@Override
protected void compute() {
final int length = end - start;
if (length < threshold) {
process();
}
else {
final int split = length / 2;
final ReloadAction subTask1 = new ReloadAction(threshold, players, start, (start + split),
whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs);
final ReloadAction subTask2 = new ReloadAction(threshold, players, (start + split), end,
whitelistOnly, excludeBanned, lastPlayedLimit, offlinePlayerUUIDs);
//queue and compute all subtasks in the right order
invokeAll(subTask1, subTask2);
}
}
private void process() {
for (int i = start; i < end; i++) {
OfflinePlayer player = players[i];
if (player.getName() != null &&
(!whitelistOnly || player.isWhitelisted()) &&
(!excludeBanned || !player.isBanned()) &&
(lastPlayedLimit == 0 || UnixTimeHandler.hasPlayedSince(lastPlayedLimit, player.getLastPlayed()))) {
offlinePlayerUUIDs.put(player.getName(), player.getUniqueId());
}
}
}
}

View File

@ -0,0 +1,122 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.TestFileHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.utils.MessageFactory;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.Nullable;
import java.util.ConcurrentModificationException;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
public class ReloadThread extends Thread {
private final int threshold;
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static TestFileHandler testFile;
private final MessageFactory messageFactory;
private final Main plugin;
private final StatThread statThread;
private final CommandSender sender;
private final boolean firstTimeLoading;
public ReloadThread(int threshold, BukkitAudiences b, ConfigHandler c, TestFileHandler t, MessageFactory m, Main p, @Nullable StatThread s, @Nullable CommandSender se, boolean firstTime) {
this.threshold = threshold;
adventure = b;
config = c;
testFile = t;
messageFactory = m;
plugin = p;
statThread = s;
sender = se;
firstTimeLoading = firstTime;
plugin.getLogger().info("ReloadThread created");
}
@Override
public void run() {
long time = System.currentTimeMillis();
if (!firstTimeLoading) {
if (statThread != null && statThread.isAlive()) {
try {
plugin.getLogger().info("Waiting for statThread to finish up...");
statThread.join();
} catch (InterruptedException e) {
plugin.getLogger().warning(e.toString());
throw new RuntimeException(e);
}
}
plugin.getLogger().info("Reloading!");
if (config.reloadConfig()) {
try {
OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap(false));
} catch (ConcurrentModificationException e) {
plugin.getLogger().warning("The request could not be fully executed due to a ConcurrentModificationException");
if (sender != null) {
adventure.sender(sender).sendMessage(messageFactory.partiallyReloaded());
}
}
testFile.saveTimeTaken(System.currentTimeMillis() - time, 2);
plugin.getLogger().info("Amount of relevant players: " + OfflinePlayerHandler.getOfflinePlayerCount());
plugin.logTimeTaken("ReloadThread", "loading offline players", time);
if (sender != null) {
adventure.sender(sender).sendMessage(messageFactory.reloadedConfig());
}
}
}
else {
plugin.getLogger().info("Loading offline players...");
OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap(true));
testFile.saveThreshold(OfflinePlayerHandler.getOfflinePlayerCount(), threshold);
testFile.saveTimeTaken(System.currentTimeMillis() - time, 1);
plugin.getLogger().info("Amount of relevant players: " + OfflinePlayerHandler.getOfflinePlayerCount());
plugin.logTimeTaken("ReloadThread", "loading offline players", time);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
}
}
private ConcurrentHashMap<String, UUID> getPlayerMap(boolean firstTimeLoading) {
OfflinePlayer[] offlinePlayers = Bukkit.getOfflinePlayers();
int size = firstTimeLoading ? offlinePlayers.length : OfflinePlayerHandler.getOfflinePlayerCount();
ConcurrentHashMap<String, UUID> playerMap = new ConcurrentHashMap<>(size);
ReloadAction task = new ReloadAction(threshold, offlinePlayers, config.whitelistOnly(), config.excludeBanned(), config.lastPlayedLimit(), playerMap);
ForkJoinPool commonPool = ForkJoinPool.commonPool();
try {
commonPool.invoke(task);
} catch (ConcurrentModificationException e) {
throw new ConcurrentModificationException(e.toString());
}
return playerMap;
}
private ConcurrentHashMap<String, UUID> generateFakeExtraPlayers(ConcurrentHashMap<String, UUID> realPlayers, int loops) {
ConcurrentHashMap<String, UUID> newPlayerMap = new ConcurrentHashMap<>(realPlayers.size() * loops);
for (int i = 0; i < loops; i++) {
for (String key : realPlayers.keySet()) {
newPlayerMap.put(key + i, realPlayers.get(key));
}
}
return newPlayerMap;
}
}

View File

@ -1,45 +1,94 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.enums.Query;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class StatRequest {
private final CommandSender sender;
private String statName;
private Statistic.Type statType;
private String subStatEntry;
private String playerName;
private Query selection;
private boolean playerFlag;
private boolean topFlag;
private Statistic statEnum;
private Statistic.Type statType;
private EntityType entity;
private Material block;
private Material item;
//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;
}
//sets the statName, and automatically tries to set the correct statType and get the corresponding item/block/entity if there is a subStatEntry
public void setStatName(String statName) {
this.statName = statName;
setStatType(statName);
}
private void setStatType(String statName) {
if (statName != null) {
try {
statType = EnumHandler.getStatType(statName);
}
catch (IllegalArgumentException e) {
e.printStackTrace();
setStatEnumAndType();
if (subStatEntry != null) {
extractSubStat();
}
}
}
private void setStatEnumAndType() throws IllegalArgumentException {
try {
statEnum = EnumHandler.getStatEnum(statName);
statType = statEnum.getType();
} catch (IllegalArgumentException e) {
Bukkit.getLogger().warning(e.toString());
}
}
//sets the subStatEntry, and automatically tries to get the corresponding item/block/entity if there is a valid statType present
//if the subStatEntry is set to null, any present item/block/entity is set to null again
public void setSubStatEntry(String subStatEntry) {
this.subStatEntry = subStatEntry;
if (subStatEntry != null && statType != null) {
extractSubStat();
}
else if (subStatEntry == null) {
entity = null;
item = null;
block = null;
}
}
private void extractSubStat() {
switch (statType) {
case ENTITY -> {
try {
entity = EnumHandler.getEntityType(subStatEntry);
} catch (IllegalArgumentException e) {
Bukkit.getLogger().warning(e.toString());
}
}
case ITEM -> {
try {
item = EnumHandler.getItem(subStatEntry);
} catch (IllegalArgumentException e) {
Bukkit.getLogger().warning(e.toString());
}
}
case BLOCK -> {
try {
block = EnumHandler.getBlock(subStatEntry);
} catch (IllegalArgumentException e) {
Bukkit.getLogger().warning(e.toString());
}
}
}
}
public void setPlayerName(String playerName) {
@ -52,8 +101,8 @@ public class StatRequest {
this.playerFlag = playerFlag;
}
public void setTopFlag(boolean topFlag) {
this.topFlag = topFlag;
public void setSelection(Query selection) {
this.selection = selection;
}
public CommandSender getCommandSender() {
@ -69,10 +118,26 @@ public class StatRequest {
return statType;
}
public Statistic getStatEnum() {
return statEnum;
}
public String getSubStatEntry() {
return subStatEntry;
}
public EntityType getEntity() {
return entity;
}
public Material getBlock() {
return block;
}
public Material getItem() {
return item;
}
public String getPlayerName() {
return playerName;
}
@ -81,8 +146,7 @@ public class StatRequest {
return playerFlag;
}
public boolean topFlag() {
return topFlag;
public @Nullable Query getSelection() {
return selection;
}
}

View File

@ -1,44 +1,46 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.enums.Query;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.TestFileHandler;
import com.gmail.artemis.the.gr8.playerstats.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
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.MessageFactory;
import com.google.common.collect.ImmutableList;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.OfflinePlayer;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
public class StatThread extends Thread {
private final int threshold;
private final StatRequest request;
private final ReloadThread reloadThread;
private final BukkitAudiences adventure;
private final ConfigHandler config;
private final OfflinePlayerHandler offlinePlayerHandler;
private static ConfigHandler config;
private static TestFileHandler testFile;
private final MessageFactory messageFactory;
private final Main plugin;
//constructor (called on thread creation)
public StatThread(StatRequest s, @Nullable ReloadThread r, BukkitAudiences b, ConfigHandler c, OfflinePlayerHandler of, MessageFactory o, Main p) {
public StatThread(int threshold, StatRequest s, @Nullable ReloadThread r, BukkitAudiences b, ConfigHandler c, TestFileHandler t, MessageFactory o, Main p) {
this.threshold = threshold;
request = s;
reloadThread = r;
adventure = b;
config = c;
offlinePlayerHandler = of;
testFile = t;
messageFactory = o;
plugin = p;
plugin.getLogger().info("StatThread created!");
@ -47,8 +49,6 @@ public class StatThread extends Thread {
//what the thread will do once started
@Override
public void run() throws IllegalStateException, NullPointerException {
long time = System.currentTimeMillis();
if (messageFactory == null || plugin == null) {
throw new IllegalStateException("Not all classes off the plugin are running!");
}
@ -70,120 +70,112 @@ public class StatThread extends Thread {
String playerName = request.getPlayerName();
String statName = request.getStatName();
String subStatEntry = request.getSubStatEntry();
boolean topFlag = request.topFlag();
Query selection = request.getSelection();
if (playerName != null) {
try {
adventure.sender(sender).sendMessage(
messageFactory.formatPlayerStat(
playerName, statName, subStatEntry, getStatistic(
statName, subStatEntry, playerName)));
plugin.logTimeTaken("StatThread", "calculating individual stat", time);
} catch (Exception e) {
sender.sendMessage(messageFactory.formatExceptions(e.toString()));
e.printStackTrace();
}
} else if (topFlag) {
if (ThreadManager.getLastRecordedCalcTime() > 30000) {
if (selection == Query.TOP || selection == Query.SERVER) {
if (ThreadManager.getLastRecordedCalcTime() > 20000) {
adventure.sender(sender).sendMessage(messageFactory.waitAMoment(true));
}
else if (ThreadManager.getLastRecordedCalcTime() > 2000) {
else if (ThreadManager.getLastRecordedCalcTime() > 1500) {
adventure.sender(sender).sendMessage(messageFactory.waitAMoment(false));
}
try {
adventure.sender(sender).sendMessage(messageFactory.formatTopStats(
getTopStatistics(statName, subStatEntry), statName, subStatEntry));
plugin.logTimeTaken("StatThread", "calculating top stat", time);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
if (selection == Query.TOP) {
adventure.sender(sender).sendMessage(messageFactory.formatTopStats(
getTopStats(), statName, subStatEntry));
}
else {
adventure.sender(sender).sendMessage(messageFactory.formatServerStat(
statName, subStatEntry, getServerTotal()));
}
} catch (ConcurrentModificationException e) {
testFile.logRunCount(true);
adventure.sender(sender).sendMessage(messageFactory.unknownError());
} catch (Exception e) {
sender.sendMessage(messageFactory.formatExceptions(e.toString()));
e.printStackTrace();
adventure.sender(sender).sendMessage(messageFactory.formatExceptions(e.toString()));
}
}
else if (selection == Query.PLAYER) {
try {
long time = System.currentTimeMillis();
adventure.sender(sender).sendMessage(
messageFactory.formatPlayerStat(
playerName, statName, subStatEntry, getIndividualStat()));
plugin.logTimeTaken("StatThread", "calculating individual stat", time);
} catch (UnsupportedOperationException | NullPointerException e) {
adventure.sender(sender).sendMessage(messageFactory.formatExceptions(e.getMessage()));
}
}
}
//returns the integer associated with a certain statistic for a player
private int getStatistic(String statName, String subStatEntryName, String playerName) throws IllegalArgumentException, NullPointerException {
try {
Statistic stat = EnumHandler.getStatEnum(statName);
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
return getPlayerStat(player, stat, subStatEntryName);
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.toString());
}
private LinkedHashMap<String, Integer> getTopStats() throws ConcurrentModificationException, NullPointerException {
return getAllStats().entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(config.getTopListMaxSize()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}
private LinkedHashMap<String, Integer> getTopStatistics(String statName, String subStatEntry) {
private int getServerTotal() {
List<Integer> numbers = getAllStats().values().stream().toList();
return numbers.parallelStream().mapToInt(Integer::intValue).sum();
}
//invokes a bunch of worker pool threads to divide and conquer (get the statistics for all players in the list)
private @NotNull ConcurrentHashMap<String, Integer> getAllStats() throws ConcurrentModificationException, NullPointerException {
long time = System.currentTimeMillis();
ConcurrentHashMap<String, Integer> playerStats = new ConcurrentHashMap<>((int) (OfflinePlayerHandler.getOfflinePlayerCount() * 1.05));
ImmutableList<String> playerNames = ImmutableList.copyOf(OfflinePlayerHandler.getOfflinePlayerNames());
TopStatAction task = new TopStatAction(threshold, playerNames,
request, playerStats);
ForkJoinPool commonPool = ForkJoinPool.commonPool();
try {
Statistic stat = EnumHandler.getStatEnum(statName);
HashMap<String, Integer> playerStats = new HashMap<>((int) (getOfflinePlayerCount() * 1.05));
offlinePlayerHandler.getOfflinePlayerNames().forEach(playerName -> {
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
try {
int statistic = getPlayerStat(player, stat, subStatEntry);
if (statistic > 0) {
playerStats.put(playerName, statistic);
commonPool.invoke(task);
} catch (ConcurrentModificationException e) {
plugin.getLogger().warning("The request could not be executed due to a ConcurrentModificationException. " +
"This likely happened because Bukkit hasn't fully initialized all players yet. Try again and it should be fine!");
throw new ConcurrentModificationException(e.toString());
}
testFile.saveTimeTaken(System.currentTimeMillis() - time, 3);
testFile.logRunCount(false);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
plugin.logTimeTaken("StatThread", "calculating all stats", time);
return playerStats;
}
//gets the actual statistic data for an individual player
private int getIndividualStat() throws UnsupportedOperationException, NullPointerException {
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(request.getPlayerName());
if (player != null) {
switch (request.getStatType()) {
case UNTYPED -> {
return player.getStatistic(request.getStatEnum());
}
case ENTITY -> {
return player.getStatistic(request.getStatEnum(), request.getEntity());
}
case BLOCK -> {
return player.getStatistic(request.getStatEnum(), request.getBlock());
}
case ITEM -> {
return player.getStatistic(request.getStatEnum(), request.getItem());
}
default -> {
if (request.getStatType() != null) {
throw new UnsupportedOperationException("PlayerStats is not familiar with this statistic type - please check if you are using the latest version of the plugin!");
}
else {
throw new NullPointerException("Trying to calculate a statistic of which the type is null - is this a valid statistic?");
}
} catch (IllegalArgumentException ignored) {
}
});
return playerStats.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(config.getTopListMaxSize()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.toString());
}
}
//gets the actual statistic data for a given player
private int getPlayerStat(@NotNull OfflinePlayer player, @NotNull Statistic stat, String subStatEntryName) throws IllegalArgumentException {
switch (stat.getType()) {
case UNTYPED -> {
return player.getStatistic(stat);
}
case BLOCK -> {
try {
return player.getStatistic(stat, EnumHandler.getBlock(subStatEntryName));
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.toString());
}
}
case ENTITY -> {
try {
return player.getStatistic(stat, EnumHandler.getEntityType(subStatEntryName));
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.toString());
}
}
case ITEM -> {
try {
return player.getStatistic(stat, EnumHandler.getItem(subStatEntryName));
}
catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.toString());
}
}
default ->
throw new IllegalArgumentException("This statistic does not seem to be of type:untyped/block/entity/item, I think we should panic");
}
}
//returns the amount of offline players, attempts to update the list if none are found, and otherwise throws an error
private int getOfflinePlayerCount() {
try {
return offlinePlayerHandler.getOfflinePlayerCount();
}
catch (NullPointerException e) {
throw new RuntimeException("No offline players were found to calculate statistics for!");
}
throw new NullPointerException("The player you are trying to request either does not exist, or is not on the list for statistic lookups!");
}
}

View File

@ -0,0 +1,74 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.google.common.collect.ImmutableList;
import org.bukkit.OfflinePlayer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
public class TopStatAction extends RecursiveAction {
private final int threshold;
private final ImmutableList<String> playerNames;
private final StatRequest request;
private final ConcurrentHashMap<String, Integer> playerStats;
/**
* Gets the statistic numbers for all players whose name is on the list, puts them in a ConcurrentHashMap
* using the default ForkJoinPool, and returns the ConcurrentHashMap when everything is done
* @param playerNames List of playerNames of players that should be included in the stat calculations
* @param statRequest a validated statRequest
* @param playerStats the ConcurrentHashMap to put the results on
*/
public TopStatAction(int threshold, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> playerStats) {
this.threshold = threshold;
this.playerNames = playerNames;
this.request = statRequest;
this.playerStats = playerStats;
}
@Override
protected void compute() {
if (playerNames.size() < threshold) {
getStatsDirectly();
}
else {
final TopStatAction subTask1 = new TopStatAction(threshold, playerNames.subList(0, playerNames.size()/2), request, playerStats);
final TopStatAction subTask2 = new TopStatAction(threshold, playerNames.subList(playerNames.size()/2, playerNames.size()), request, playerStats);
//queue and compute all subtasks in the right order
invokeAll(subTask1, subTask2);
}
}
private void getStatsDirectly() {
try {
Iterator<String> iterator = playerNames.iterator();
if (iterator.hasNext()) {
do {
String playerName = iterator.next();
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
int statistic = 0;
switch (request.getStatType()) {
case UNTYPED -> statistic = player.getStatistic(request.getStatEnum());
case ENTITY -> statistic = player.getStatistic(request.getStatEnum(), request.getEntity());
case BLOCK -> statistic = player.getStatistic(request.getStatEnum(), request.getBlock());
case ITEM -> statistic = player.getStatistic(request.getStatEnum(), request.getItem());
}
if (statistic > 0) {
playerStats.put(playerName, statistic);
}
}
} while (iterator.hasNext());
}
} catch (NoSuchElementException ignored) {
}
}
}

View File

@ -1,6 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.enums.Query;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
@ -9,9 +9,10 @@ import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.Index;
import org.bukkit.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.Statistic;
import org.bukkit.map.MinecraftFont;
import org.jetbrains.annotations.Nullable;
import java.util.*;
@ -20,57 +21,67 @@ import static net.kyori.adventure.text.Component.*;
public class MessageFactory {
private static ConfigHandler config;
private final Main plugin;
private static final TextColor msgColor = TextColor.fromHexString("#55aaff");
private static final String pluginPrefix = ChatColor.GRAY + "[" + ChatColor.GOLD + "PlayerStats" + ChatColor.GRAY + "] " + ChatColor.RESET;
private static final TextColor hoverBaseColor = TextColor.fromHexString("#55C6FF");
private static final TextColor hoverAccentColor1 = TextColor.fromHexString("#FFB80E");
private static final TextColor hoverAccentColor2 = TextColor.fromHexString("#FFD52B");
public MessageFactory(ConfigHandler c, Main p) {
plugin = p;
public MessageFactory(ConfigHandler c) {
config = c;
}
public static String getPluginPrefix() {
return pluginPrefix;
private static TextComponent getPluginPrefix() {
return text("[")
.append(text("PlayerStats").color(NamedTextColor.GOLD))
.append(text("]")
.append(space()))
.color(NamedTextColor.GRAY);
}
public TextComponent reloadedConfig() {
return getPluginPrefix().append(text("Config reloaded!").color(NamedTextColor.GREEN));
}
public TextComponent stillReloading() {
return text(getPluginPrefix()).append(text("The plugin is still (re)loading, your request will be processed when it is done!").color(msgColor));
return getPluginPrefix().append(text("The plugin is still (re)loading, your request will be processed when it is done!").color(msgColor));
}
public TextComponent partiallyReloaded() {
return getPluginPrefix().append(
text("The reload process was interrupted. If you notice unexpected behavior, please reload PlayerStats again to fix it!").color(msgColor));
}
public TextComponent waitAMoment(boolean longWait) {
return longWait ? text(getPluginPrefix()).append(text("Calculating statistics, this may take a minute...").color(msgColor))
: text(getPluginPrefix()).append(text("Calculating statistics, this may take a few moments...").color(msgColor));
return longWait ? getPluginPrefix().append(text("Calculating statistics, this may take a minute...").color(msgColor))
: getPluginPrefix().append(text("Calculating statistics, this may take a few moments...").color(msgColor));
}
public String formatExceptions(String exception) {
return getPluginPrefix() + exception;
public TextComponent formatExceptions(String exception) {
return getPluginPrefix().append(text(exception).color(msgColor));
}
public TextComponent missingStatName() {
return text(getPluginPrefix()).append(text("Please provide a valid statistic name!").color(msgColor));
return getPluginPrefix().append(text("Please provide a valid statistic name!").color(msgColor));
}
public TextComponent missingSubStatName(Statistic.Type statType) {
String subStat = getSubStatTypeName(statType) == null ? "sub-statistic" : getSubStatTypeName(statType);
return text(getPluginPrefix())
return getPluginPrefix()
.append(text("Please add a valid ")
.append(text(subStat))
.append(text(" to look up this statistic!")))
.color(msgColor);
}
public TextComponent missingTarget() {
return text(getPluginPrefix()).append(text("Please add \"me\", \"player\" or \"top\"").color(msgColor));
}
public TextComponent missingPlayerName() {
return text(getPluginPrefix()).append(text("Please specify a valid player-name!").color(msgColor));
return getPluginPrefix().append(text("Please specify a valid player-name!").color(msgColor));
}
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatEntry) {
String subStat = getSubStatTypeName(statType) == null ? "sub-statistic for this statistic" : getSubStatTypeName(statType);
return text(getPluginPrefix())
return getPluginPrefix()
.append(text("\"")
.append(text(subStatEntry))
.append(text("\""))
@ -80,7 +91,9 @@ public class MessageFactory {
}
public TextComponent unknownError() {
return text(getPluginPrefix()).append(text("Something went wrong with your input. Check /statistic for a usage explanation").color(msgColor));
return getPluginPrefix()
.append(text("Something went wrong with your request, please try again or see /statistic for a usage explanation!")
.color(msgColor));
}
public TextComponent helpMsg() {
@ -88,12 +101,10 @@ public class MessageFactory {
TextComponent underscores = text("____________").color(TextColor.fromHexString("#6E3485"));
TextComponent arrow = text("").color(NamedTextColor.GOLD);
TextColor arguments = NamedTextColor.YELLOW;
TextColor hoverBaseColor = TextColor.fromHexString("#55C6FF");
TextColor hoverAccentColor1 = TextColor.fromHexString("#FFB80E");
TextColor hoverAccentColor2 = TextColor.fromHexString("#FFD52B");
return Component.newline()
.append(underscores).append(spaces).append(text(MessageFactory.getPluginPrefix())).append(spaces).append(underscores)
.append(underscores).append(spaces).append(getPluginPrefix()).append(spaces).append(underscores)
.append(newline())
.append(text("Hover over the arguments for more information!").color(NamedTextColor.GRAY).decorate(TextDecoration.ITALIC))
.append(newline())
@ -127,6 +138,10 @@ public class MessageFactory {
.hoverEvent(HoverEvent.showText(
text("Choose any player that has played on your server").color(hoverBaseColor))))
.append(text(" | ").color(arguments))
.append(text("server").color(arguments)
.hoverEvent(HoverEvent.showText(
text("See the combined total for everyone on your server").color(hoverBaseColor))))
.append(text(" | ").color(arguments))
.append(text("top").color(arguments)
.hoverEvent(HoverEvent.showText(
text("See the top ").color(hoverBaseColor)
@ -142,28 +157,23 @@ public class MessageFactory {
public TextComponent formatPlayerStat(String playerName, String statName, String subStatEntryName, int stat) {
TextComponent.Builder singleStat = Component.text();
String subStat = subStatEntryName != null ?
" (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
singleStat.append(playerNameComponent(playerName + ": ", false))
.append(statNumberComponent(stat, false)).append(space())
.append(statNameComponent(statName.toLowerCase().replace("_", " "), false))
.append(subStatNameComponent(subStat, false));
singleStat.append(playerNameComponent(Query.PLAYER, playerName + ": "))
.append(statNumberComponent(Query.PLAYER, stat)).append(space())
.append(statNameComponent(Query.PLAYER, statName))
.append(subStatNameComponent(Query.PLAYER, subStatEntryName));
return singleStat.build();
}
public TextComponent formatTopStats(LinkedHashMap<String, Integer> topStats, String statName, String subStatEntryName) {
long time = System.currentTimeMillis();
TextComponent.Builder topList = Component.text();
String subStat = subStatEntryName != null ?
"(" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
topList.append(newline()).append(text(getPluginPrefix()))
.append(statNameComponent("Top", true)).append(space())
.append(listNumberComponent(topStats.size() + "")).append(space())
.append(statNameComponent(statName.toLowerCase().replace("_", " "), true)).append(space())
.append(subStatNameComponent(subStat, true));
topList.append(newline()).append(getPluginPrefix())
.append(titleComponent(Query.TOP, "Top")).append(space())
.append(titleNumberComponent(topStats.size())).append(space())
.append(statNameComponent(Query.TOP, statName)).append(space())
.append(subStatNameComponent(Query.TOP, subStatEntryName));
boolean useDots = config.useDots();
Set<String> playerNames = topStats.keySet();
@ -174,8 +184,8 @@ public class MessageFactory {
count = count+1;
topList.append(newline())
.append(listNumberComponent(count + ". "))
.append(playerNameComponent(playerName, true));
.append(rankingNumberComponent(count + ". "))
.append(playerNameComponent(Query.TOP, playerName));
if (useDots) {
topList.append(space());
@ -189,14 +199,28 @@ public class MessageFactory {
}
}
else {
topList.append(playerNameComponent(":", true));
topList.append(playerNameComponent(Query.TOP, ":"));
}
topList.append(space()).append(statNumberComponent(topStats.get(playerName), true));
topList.append(space()).append(statNumberComponent(Query.TOP, topStats.get(playerName)));
}
plugin.logTimeTaken("MessageFactory", "applying colors", time);
return topList.build();
}
public TextComponent formatServerStat(String statName, String subStatEntry, int stat) {
TextComponent.Builder serverStat = Component.text();
serverStat.append(titleComponent(Query.SERVER, "Total for"))
.append(space())
.append(serverNameComponent())
.append(space())
.append(statNumberComponent(Query.SERVER, stat))
.append(space())
.append(statNameComponent(Query.SERVER, statName))
.append(space())
.append(subStatNameComponent(Query.SERVER, subStatEntry));
return serverStat.build();
}
//returns the type of the substatistic in String-format, or null if this statistic is not of type block, item or entity
private String getSubStatTypeName(Statistic.Type statType) {
String subStat;
@ -215,40 +239,70 @@ public class MessageFactory {
return subStat;
}
//try to get the hex color or NamedTextColor from config String, substitute a default ChatColor if both fail, and try to apply style where necessary
private TextComponent playerNameComponent(String playerName, boolean topStat) {
NamedTextColor defaultColor = topStat ? NamedTextColor.GREEN : NamedTextColor.GOLD;
TextComponent.Builder player = applyColor(
config.getPlayerNameFormatting(topStat, false), playerName, defaultColor);
return applyStyle(config.getPlayerNameFormatting(topStat, true), player).build();
private TextComponent playerNameComponent(Query selection, String playerName) {
return getComponent(playerName,
getColorFromString(config.getPlayerNameFormatting(selection, false)),
getStyleFromString(config.getPlayerNameFormatting(selection, true)));
}
private TextComponent statNameComponent(String statName, boolean topStat) {
TextComponent.Builder stat = applyColor(
config.getStatNameFormatting(topStat, false), statName, NamedTextColor.YELLOW);
return applyStyle(config.getStatNameFormatting(topStat, true), stat).build();
private TextComponent statNameComponent(Query selection, String statName) {
return getComponent(statName.toLowerCase().replace("_", " "),
getColorFromString(config.getStatNameFormatting(selection, false)),
getStyleFromString(config.getStatNameFormatting(selection, true)));
}
private TextComponent subStatNameComponent(String subStatName, boolean topStat) {
TextComponent.Builder subStat = applyColor(
config.getSubStatNameFormatting(topStat, false), subStatName, NamedTextColor.YELLOW);
return applyStyle(config.getSubStatNameFormatting(topStat, true), subStat).build();
private TextComponent subStatNameComponent(Query selection, String subStatName) {
if (subStatName == null) {
return empty();
}
else {
return getComponent("(" + subStatName.toLowerCase().replace("_", " ") + ")",
getColorFromString(config.getSubStatNameFormatting(selection, false)),
getStyleFromString(config.getSubStatNameFormatting(selection, true)))
.append(space());
}
}
private TextComponent statNumberComponent(int statNumber, boolean topStat) {
TextComponent.Builder number = applyColor(
config.getStatNumberFormatting(topStat, false), statNumber + "", NamedTextColor.LIGHT_PURPLE);
return applyStyle(config.getStatNumberFormatting(topStat, true), number).build();
private TextComponent statNumberComponent(Query selection, int number) {
return getComponent(number + "",
getColorFromString(config.getStatNumberFormatting(selection, false)),
getStyleFromString(config.getStatNumberFormatting(selection, true)));
}
private TextComponent listNumberComponent(String listNumber) {
TextComponent.Builder list = applyColor(config.getListNumberFormatting(false), listNumber + "", NamedTextColor.GOLD);
return applyStyle(config.getListNumberFormatting(true), list).build();
private TextComponent titleComponent(Query selection, String content) {
return getComponent(content,
getColorFromString(config.getTitleFormatting(selection, false)),
getStyleFromString(config.getTitleFormatting(selection, true)));
}
private TextComponent titleNumberComponent(int number) {
return getComponent(number + "",
getColorFromString(config.getTitleNumberFormatting(false)),
getStyleFromString(config.getTitleNumberFormatting(true)));
}
private TextComponent serverNameComponent() {
TextComponent colon = text(":").color(getColorFromString(config.getServerNameFormatting(false)));
return getComponent(config.getServerName(),
getColorFromString(config.getServerNameFormatting(false)),
getStyleFromString(config.getServerNameFormatting(true)))
.append(colon);
}
private TextComponent rankingNumberComponent(String number) {
return getComponent(number,
getColorFromString(config.getRankNumberFormatting(false)),
getStyleFromString(config.getRankNumberFormatting(true)));
}
private TextComponent dotsComponent(String dots) {
return text(dots).color(getColorFromString(config.getDotsColor())).colorIfAbsent(NamedTextColor.DARK_GRAY);
//return applyColor(config.getDotsColor(), dots, NamedTextColor.DARK_GRAY).build();
return getComponent(dots,
getColorFromString(config.getDotsFormatting(false)),
getStyleFromString(config.getDotsFormatting(true)));
}
private TextComponent getComponent(String content, TextColor color, @Nullable TextDecoration style) {
return style == null ? text(content).color(color) : text(content).color(color).decoration(style, TextDecoration.State.TRUE);
}
private TextColor getColorFromString(String configString) {
@ -262,57 +316,27 @@ public class MessageFactory {
}
}
catch (IllegalArgumentException | NullPointerException exception) {
plugin.getLogger().warning(exception.toString());
Bukkit.getLogger().warning(exception.toString());
}
}
return null;
}
private TextComponent.Builder applyColor(String configString, String content, NamedTextColor defaultColor) {
TextComponent.Builder component = Component.text();
if (configString != null) {
try {
if (configString.contains("#")) {
return component.content(content).color(TextColor.fromHexString(configString));
}
else {
return component.content(content).color(getTextColorByName(configString));
}
}
catch (IllegalArgumentException | NullPointerException exception) {
plugin.getLogger().warning(exception.toString());
}
}
return component.content(content).colorIfAbsent(defaultColor);
}
private TextColor getTextColorByName(String textColor) {
Index<String, NamedTextColor> names = NamedTextColor.NAMES;
return names.value(textColor);
}
private TextComponent.Builder applyStyle(String configString, TextComponent.Builder component) {
if (configString != null) {
if (configString.equalsIgnoreCase("none")) {
return component;
}
else if (configString.equalsIgnoreCase("bold")) {
return component.decoration(TextDecoration.BOLD, TextDecoration.State.TRUE);
}
else if (configString.equalsIgnoreCase("italic")) {
return component.decoration(TextDecoration.ITALIC, TextDecoration.State.TRUE);
}
else if (configString.equalsIgnoreCase("magic")) {
return component.decoration(TextDecoration.OBFUSCATED, TextDecoration.State.TRUE);
}
else if (configString.equalsIgnoreCase("strikethrough")) {
return component.decoration(TextDecoration.STRIKETHROUGH, TextDecoration.State.TRUE);
}
else if (configString.equalsIgnoreCase("underlined")) {
return component.decoration(TextDecoration.UNDERLINED, TextDecoration.State.TRUE);
}
private @Nullable TextDecoration getStyleFromString(String configString) {
if (configString.equalsIgnoreCase("none")) {
return null;
}
else if (configString.equalsIgnoreCase("magic")) {
return TextDecoration.OBFUSCATED;
}
else {
Index<String, TextDecoration> styles = TextDecoration.NAMES;
return styles.value(configString);
}
return component;
}
}

View File

@ -1,53 +1,53 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class OfflinePlayerHandler {
private final ConfigHandler config;
private HashMap<String, UUID> offlinePlayerUUIDs;
private static ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
private static ArrayList<String> playerNames;
public OfflinePlayerHandler(ConfigHandler c) {
config = c;
private OfflinePlayerHandler() {
}
public boolean isOfflinePlayerName(String playerName) {
public static boolean isOfflinePlayerName(String playerName) {
return offlinePlayerUUIDs.containsKey(playerName);
}
public int getOfflinePlayerCount() throws NullPointerException {
public static int getOfflinePlayerCount() throws NullPointerException {
if (offlinePlayerUUIDs != null && offlinePlayerUUIDs.size() > 0) return offlinePlayerUUIDs.size();
else throw new NullPointerException("No players found!");
}
public Set<String> getOfflinePlayerNames() {
return offlinePlayerUUIDs.keySet();
public static ArrayList<String> getOfflinePlayerNames() {
return playerNames;
}
public void updateOfflinePlayerList() {
updateOfflinePlayerList(config.whitelistOnly(), config.excludeBanned(), config.lastPlayedLimit());
/**
* Get a new HashMap that stores the players to include in stat calculations.
* This HashMap is stored as a private variable in OfflinePlayerHandler (keys: playerNames, values: UUIDs).
*/
public static void updateOfflinePlayerList(ConcurrentHashMap<String, UUID> playerList) {
offlinePlayerUUIDs = playerList;
playerNames = Collections.list(offlinePlayerUUIDs.keys());
}
//stores a private HashMap of all relevant offline players with keys:playerName and values:UUID
private void updateOfflinePlayerList(boolean whitelistOnly, boolean excludeBanned, int lastPlayedLimit) {
if (offlinePlayerUUIDs == null) offlinePlayerUUIDs = new HashMap<>();
else if (!offlinePlayerUUIDs.isEmpty()) {
offlinePlayerUUIDs.clear();
/**
* Uses the playerName to get the player's UUID from a private HashMap, and uses the UUID to get the corresponding OfflinePlayer Object.
* @param playerName name of the target player
* @return OfflinePlayer (if this player is on the list, otherwise null)
*/
public static @Nullable OfflinePlayer getOfflinePlayer(String playerName) {
if (offlinePlayerUUIDs.get(playerName) != null) {
return Bukkit.getOfflinePlayer(offlinePlayerUUIDs.get(playerName));
}
else {
return null;
}
Arrays.stream(Bukkit.getOfflinePlayers()).filter(offlinePlayer ->
offlinePlayer.getName() != null &&
(!excludeBanned || !offlinePlayer.isBanned()) &&
(!whitelistOnly || offlinePlayer.isWhitelisted()) &&
(lastPlayedLimit == 0 || UnixTimeHandler.hasPlayedSince(lastPlayedLimit, offlinePlayer.getLastPlayed())))
.forEach(offlinePlayer -> offlinePlayerUUIDs.put((offlinePlayer.getName()), offlinePlayer.getUniqueId()));
}
public OfflinePlayer getOfflinePlayer(String playerName) {
return Bukkit.getOfflinePlayer(offlinePlayerUUIDs.get(playerName));
}
}

View File

@ -1,8 +1,8 @@
# ------------------------------
# PlayerStats Configuration
# ------------------------------
# --------------------------------
# PlayerStats Configuration
# --------------------------------
# ------ General Options -------
# ----------- General ------------
# Filtering options to control which players should be included in statistic calculations
include-whitelist-only: false
exclude-banned-players: false
@ -11,43 +11,77 @@ exclude-banned-players: false
# Leave this on 0 to include all players
number-of-days-since-last-joined: 0
# ------ Format Options --------
# ----------- Format -------------
# The name you want displayed for a total-on-this-server statistic
your-server-name: 'this server'
# The maximum number of results displayed in the top list
top-list-max-size: 10
# If true, the top list will be aligned with lines of dots so that the stat numbers are all underneath each other
use-dots: true
# ------ Color Options ---------
# -------- Color & Style ---------
# The colors below can be chat color names or hex codes (format: '#xxxxxx' <-- including quotation marks!)
# The style options include: bold, italic, underlined, strikethrough, and magic
individual-statistics-color:
player-names: gold
# The style options include: bold, italic, underlined, strikethrough (and for some reason I decided to also include magic)
top-list:
title: yellow
title-style: none
title-number: gold
title-number-style: none
stat-names: yellow
stat-names-style: none
sub-stat-names: '#FFD52B'
stat-numbers: '#ADE7FF'
sub-stat-names-style: none
individual-statistics-style:
player-names: none
stat-names: none
sub-stat-names: none
stat-numbers: none
rank-numbers: gold
rank-numbers-style: none
top-list-color:
player-names: green
stat-names: yellow
sub-stat-names: '#FFD52B'
stat-numbers: '#55aaff'
list-numbers: gold
player-names-style: none
stat-numbers: '#55AAFF'
stat-numbers-style: none
dots: dark_gray
dots-style: none
top-list-style:
player-names: none
stat-names: none
sub-stat-names: none
stat-numbers: none
list-numbers: none
individual-statistics:
player-names: gold
player-names-style: none
stat-names: yellow
stat-names-style: none
sub-stat-names: '#FFD52B'
sub-stat-names-style: none
stat-numbers: '#ADE7FF'
stat-numbers-style: none
total-server:
title: gold
title-style: none
server-name: gold
server-name-style: italic
stat-names: yellow
stat-names-style: none
sub-stat-names: '#FFD52B'
sub-stat-names-style: none
stat-numbers: '#ADE7FF'
stat-numbers-style: none