diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index 991fecf..5ef58c2 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -4,7 +4,7 @@ io.github.ithotl PlayerStats PlayerStats - 1.8 + 2.0 Statistics Plugin https://www.spigotmc.org/resources/playerstats.102347/ @@ -49,7 +49,7 @@ - com.artemis.the.gr8.playerstats.Main + com.artemis.the.gr8.playerstats.core.Main @@ -60,15 +60,15 @@ net.kyori - com.artemis.the.gr8.lib.kyori + com.artemis.the.gr8.playerstats.lib.kyori com.tchristofferson - com.artemis.the.gr8.util.tchristofferson + com.artemis.the.gr8.playerstats.lib.tchristofferson org.bstats - com.artemis.the.gr8.util.bstats + com.artemis.the.gr8.playerstats.lib.bstats @@ -107,7 +107,7 @@ attach-sources - deploy + verify jar-no-fork @@ -120,7 +120,7 @@ sign-artifacts - deploy + verify sign @@ -133,7 +133,7 @@ attach-javadocs - deploy + verify jar diff --git a/pom.xml b/pom.xml index 73925e7..1547ebd 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.ithotl PlayerStats - 1.8 + 2.0 PlayerStats Statistics Plugin @@ -141,7 +141,7 @@ - com.artemis.the.gr8.playerstats.Main + com.artemis.the.gr8.playerstats.core.Main @@ -152,15 +152,15 @@ net.kyori - com.artemis.the.gr8.lib.kyori + com.artemis.the.gr8.playerstats.lib.kyori com.tchristofferson - com.artemis.the.gr8.util.tchristofferson + com.artemis.the.gr8.playerstats.lib.tchristofferson org.bstats - com.artemis.the.gr8.util.bstats + com.artemis.the.gr8.playerstats.lib.bstats @@ -200,7 +200,7 @@ attach-sources - deploy + verify jar-no-fork @@ -214,7 +214,7 @@ sign-artifacts - deploy + verify sign @@ -228,7 +228,7 @@ attach-javadocs - deploy + verify jar diff --git a/src/main/java/com/artemis/the/gr8/playerstats/Main.java b/src/main/java/com/artemis/the/gr8/playerstats/Main.java deleted file mode 100644 index d821972..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/Main.java +++ /dev/null @@ -1,189 +0,0 @@ -package com.artemis.the.gr8.playerstats; - -import com.artemis.the.gr8.playerstats.api.PlayerStats; -import com.artemis.the.gr8.playerstats.msg.OutputManager; -import com.artemis.the.gr8.playerstats.api.PlayerStatsAPI; -import com.artemis.the.gr8.playerstats.commands.ReloadCommand; -import com.artemis.the.gr8.playerstats.commands.ShareCommand; -import com.artemis.the.gr8.playerstats.commands.StatCommand; -import com.artemis.the.gr8.playerstats.commands.TabCompleter; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.listeners.JoinListener; -import com.artemis.the.gr8.playerstats.msg.InternalFormatter; -import com.artemis.the.gr8.playerstats.msg.MessageBuilder; -import com.artemis.the.gr8.playerstats.msg.msgutils.LanguageKeyHandler; -import com.artemis.the.gr8.playerstats.statistic.StatCalculator; -import com.artemis.the.gr8.playerstats.utils.EnumHandler; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import me.clip.placeholderapi.PlaceholderAPIPlugin; -import me.clip.placeholderapi.expansion.PlaceholderExpansion; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; -import org.bstats.bukkit.Metrics; -import org.bstats.charts.SimplePie; -import org.bukkit.Bukkit; -import org.bukkit.command.PluginCommand; -import org.bukkit.plugin.java.JavaPlugin; -import org.bukkit.scheduler.BukkitRunnable; -import org.jetbrains.annotations.NotNull; - - -/** - * PlayerStats' Main class - */ -public final class Main extends JavaPlugin { - - private static Main instance; - private static BukkitAudiences adventure; - - private static ConfigHandler config; - private static LanguageKeyHandler languageKeyHandler; - private static OfflinePlayerHandler offlinePlayerHandler; - private static EnumHandler enumHandler; - - private static OutputManager outputManager; - private static ShareManager shareManager; - private static StatCalculator statCalculator; - private static ThreadManager threadManager; - - private static PlayerStats playerStatsAPI; - - - @Override - public void onEnable() { - //initialize all the Managers, singletons, ConfigHandler and the API - initializeMainClasses(); - setupMetrics(); - - //register all commands and the tabCompleter - PluginCommand statcmd = this.getCommand("statistic"); - if (statcmd != null) { - statcmd.setExecutor(new StatCommand(outputManager, threadManager)); - statcmd.setTabCompleter(new TabCompleter(enumHandler, offlinePlayerHandler)); - } - PluginCommand reloadcmd = this.getCommand("statisticreload"); - if (reloadcmd != null) reloadcmd.setExecutor(new ReloadCommand(threadManager)); - PluginCommand sharecmd = this.getCommand("statisticshare"); - if (sharecmd != null) sharecmd.setExecutor(new ShareCommand(shareManager, outputManager)); - - //register the listener - Bukkit.getPluginManager().registerEvents(new JoinListener(threadManager), this); - - //finish up - this.getLogger().info("Enabled PlayerStats!"); - } - - @Override - public void onDisable() { - if (adventure != null) { - adventure.close(); - adventure = null; - } - this.getLogger().info("Disabled PlayerStats!"); - } - - /** - * @return Adventure's BukkitAudiences object - * @throws IllegalStateException if PlayerStats is not enabled - */ - public static @NotNull BukkitAudiences getAdventure() throws IllegalStateException { - if (adventure == null) { - throw new IllegalStateException("Tried to access Adventure without PlayerStats being enabled!"); - } - return adventure; - } - - /** - * @return PlayerStats' ConfigHandler - * @throws IllegalStateException if PlayerStats is not enabled - */ - public static @NotNull ConfigHandler getConfigHandler() throws IllegalStateException { - if (config == null) { - throw new IllegalStateException("PlayerStats does not seem to be loaded!"); - } - return config; - } - - public static @NotNull OfflinePlayerHandler getOfflinePlayerHandler() throws IllegalStateException { - if (offlinePlayerHandler == null) { - throw new IllegalStateException("PlayerStats does not seem to be loaded!"); - } - return offlinePlayerHandler; - } - - public static @NotNull LanguageKeyHandler getLanguageKeyHandler() { - if (languageKeyHandler == null) { - languageKeyHandler = new LanguageKeyHandler(instance); - } - return languageKeyHandler; - } - - /** - * Gets the EnumHandler. If there is no EnumHandler, one will be created. - * @return PlayerStat's EnumHandler - */ - public static @NotNull EnumHandler getEnumHandler() { - if (enumHandler == null) { - enumHandler = new EnumHandler(); - } - return enumHandler; - } - - public static @NotNull StatCalculator getStatCalculator() throws IllegalStateException { - if (statCalculator == null) { - throw new IllegalStateException("PlayerStats does not seem to be loaded!"); - } - return statCalculator; - } - - public static @NotNull InternalFormatter getStatFormatter() throws IllegalStateException { - if (outputManager == null) { - throw new IllegalStateException("PlayerStats does not seem to be loaded!"); - } - return outputManager; - } - - public static @NotNull PlayerStats getPlayerStatsAPI() throws IllegalStateException { - if (playerStatsAPI == null) { - throw new IllegalStateException("PlayerStats does not seem to be loaded!"); - } - return playerStatsAPI; - } - - private void initializeMainClasses() { - instance = this; - adventure = BukkitAudiences.create(this); - - config = new ConfigHandler(this); - enumHandler = new EnumHandler(); - languageKeyHandler = new LanguageKeyHandler(instance); - offlinePlayerHandler = new OfflinePlayerHandler(); - - shareManager = new ShareManager(config); - statCalculator = new StatCalculator(offlinePlayerHandler); - outputManager = new OutputManager(adventure, config, shareManager); - threadManager = new ThreadManager(config, statCalculator, outputManager); - - MessageBuilder apiMessageBuilder = MessageBuilder.defaultBuilder(config); - playerStatsAPI = new PlayerStatsAPI(apiMessageBuilder, offlinePlayerHandler); - } - - private void setupMetrics() { - new BukkitRunnable() { - @Override - public void run() { - final Metrics metrics = new Metrics(instance, 15923); - final boolean placeholderExpansionActive; - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { - PlaceholderExpansion expansion = PlaceholderAPIPlugin - .getInstance() - .getLocalExpansionManager() - .getExpansion("playerstats"); - placeholderExpansionActive = expansion != null; - } else { - placeholderExpansionActive = false; - } - metrics.addCustomChart(new SimplePie("using_placeholder_expansion", () -> placeholderExpansionActive ? "yes" : "no")); - } - }.runTaskLaterAsynchronously(this, 200); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/ThreadManager.java b/src/main/java/com/artemis/the/gr8/playerstats/ThreadManager.java deleted file mode 100644 index 4ea77da..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/ThreadManager.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.artemis.the.gr8.playerstats; - -import com.artemis.the.gr8.playerstats.msg.OutputManager; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.enums.StandardMessage; -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import com.artemis.the.gr8.playerstats.reload.ReloadThread; -import com.artemis.the.gr8.playerstats.statistic.StatCalculator; -import com.artemis.the.gr8.playerstats.statistic.StatThread; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import org.bukkit.command.CommandSender; - -import java.util.HashMap; - -/** - * The ThreadManager is in charge of the Threads that PlayerStats - * can utilize. It keeps track of past and currently active Threads, - * to ensure a Player cannot start multiple Threads at the same time - * (thereby limiting them to one stat-lookup at a time). It also - * passes appropriate references along to the {@link StatThread} - * or {@link ReloadThread}, to ensure those will never run at the - * same time. - */ -public final class ThreadManager { - - private final static int threshold = 10; - private int statThreadID; - private int reloadThreadID; - - private static ConfigHandler config; - private static OutputManager outputManager; - private static StatCalculator statCalculator; - - private ReloadThread lastActiveReloadThread; - private StatThread lastActiveStatThread; - private final HashMap statThreads; - private static long lastRecordedCalcTime; - - public ThreadManager(ConfigHandler config, StatCalculator statCalculator, OutputManager outputManager) { - ThreadManager.config = config; - ThreadManager.outputManager = outputManager; - ThreadManager.statCalculator = statCalculator; - - statThreads = new HashMap<>(); - statThreadID = 0; - reloadThreadID = 0; - lastRecordedCalcTime = 0; - - startReloadThread(null); - } - - public static int getTaskThreshold() { - return threshold; - } - - public void startReloadThread(CommandSender sender) { - if (lastActiveReloadThread == null || !lastActiveReloadThread.isAlive()) { - reloadThreadID += 1; - - lastActiveReloadThread = new ReloadThread(config, outputManager, reloadThreadID, lastActiveStatThread, sender); - lastActiveReloadThread.start(); - } - else { - MyLogger.logLowLevelMsg("Another reloadThread is already running! (" + lastActiveReloadThread.getName() + ")"); - } - } - - public void startStatThread(RequestSettings requestSettings) { - statThreadID += 1; - String cmdSender = requestSettings.getCommandSender().getName(); - - if (config.limitStatRequests() && statThreads.containsKey(cmdSender)) { - Thread runningThread = statThreads.get(cmdSender); - if (runningThread.isAlive()) { - outputManager.sendFeedbackMsg(requestSettings.getCommandSender(), StandardMessage.REQUEST_ALREADY_RUNNING); - } else { - startNewStatThread(requestSettings); - } - } else { - startNewStatThread(requestSettings); - } - } - - /** - * Store the duration in milliseconds of the last top-stat-lookup - * (or of loading the offline-player-list if no look-ups have been done yet). - */ - public static void recordCalcTime(long time) { - lastRecordedCalcTime = time; - } - - /** - * Returns the duration in milliseconds of the last top-stat-lookup - * (or of loading the offline-player-list if no look-ups have been done yet). - */ - public static long getLastRecordedCalcTime() { - return lastRecordedCalcTime; - } - - private void startNewStatThread(RequestSettings requestSettings) { - lastActiveStatThread = new StatThread(outputManager, statCalculator, statThreadID, requestSettings, lastActiveReloadThread); - statThreads.put(requestSettings.getCommandSender().getName(), lastActiveStatThread); - lastActiveStatThread.start(); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStats.java b/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStats.java index 010a66a..43a6323 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStats.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStats.java @@ -1,49 +1,47 @@ package com.artemis.the.gr8.playerstats.api; -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.statistic.request.StatRequest; +import com.artemis.the.gr8.playerstats.core.Main; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; /** * The outgoing API that represents the core functionality of PlayerStats! * - *

To work with it, you'll need to call PlayerStats.{@link #getAPI()} and get an instance of - * {@link PlayerStatsAPI}. You can then use this object to access any of the further methods. - * - *

Since calculating a top or server statistics can take some time, I strongly - * encourage you to call {@link StatRequest#execute()} asynchronously. - * Otherwise, the main Thread will have to wait until all calculations are done, - * and this can severely impact server performance. + *

To work with it, you'll need to call PlayerStats.{@link #getAPI()} + * and get an instance of PlayerStats. You can then use this object to + * access any of the further methods. * * @see StatManager - * @see ApiFormatter + * @see StatTextFormatter + * @see StatNumberFormatter */ public interface PlayerStats { - /** Gets an instance of the {@link PlayerStatsAPI}. + /** Gets an instance of the PlayerStatsAPI. * @return the PlayerStats API - * @throws IllegalStateException if PlayerStats is not loaded on the server when this method is called*/ + * @throws IllegalStateException if PlayerStats is not loaded on + * the server when this method is called + */ @Contract(pure = true) static @NotNull PlayerStats getAPI() throws IllegalStateException { return Main.getPlayerStatsAPI(); } /** - * Gets the current version of PlayerStatsAPI. - * Use this method to ensure the correct version of - * PlayerStats is running on the server before - * accessing further API methods, to prevent - * ClassDefNotFoundExceptions. + * Gets the version number of the PlayerStats API + * that's present for this instance of PlayerStats. + * This number equals the major version number + * of PlayerStats. For v1.7.2, for example, + * the API version will be 1. * - * @return the version of PlayerStatsAPI present on the server + * @return the API version number */ - default String getVersion() { - return "1.8"; - } + String getVersion(); StatManager getStatManager(); - ApiFormatter getFormatter(); + StatTextFormatter getStatTextFormatter(); + + StatNumberFormatter getStatNumberFormatter(); } \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStatsAPI.java b/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStatsAPI.java deleted file mode 100644 index 9fb62a4..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/api/PlayerStatsAPI.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.artemis.the.gr8.playerstats.api; - -import com.artemis.the.gr8.playerstats.statistic.request.*; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; - -import static org.jetbrains.annotations.ApiStatus.Internal; - -/** The implementation of the API Interface */ -public final class PlayerStatsAPI implements PlayerStats, StatManager { - - private final OfflinePlayerHandler offlinePlayerHandler; - private static ApiFormatter apiFormatter; - - @Internal - public PlayerStatsAPI(ApiFormatter formatter, OfflinePlayerHandler offlinePlayers) { - apiFormatter = formatter; - offlinePlayerHandler = offlinePlayers; - } - - @Override - public ApiFormatter getFormatter() { - return apiFormatter; - } - - @Override - public StatManager getStatManager() { - return this; - } - - @Override - public PlayerStatRequest playerStatRequest(String playerName) { - RequestSettings request = RequestHandler.getBasicPlayerStatRequest(playerName); - return new PlayerStatRequest(request); - } - - @Override - public ServerStatRequest serverStatRequest() { - RequestSettings request = RequestHandler.getBasicServerStatRequest(); - return new ServerStatRequest(request); - } - - @Override - public TopStatRequest topStatRequest(int topListSize) { - RequestSettings request = RequestHandler.getBasicTopStatRequest(topListSize); - return new TopStatRequest(request); - } - - @Override - public TopStatRequest totalTopStatRequest() { - int playerCount = offlinePlayerHandler.getOfflinePlayerCount(); - return topStatRequest(playerCount); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/RequestGenerator.java b/src/main/java/com/artemis/the/gr8/playerstats/api/RequestGenerator.java index d8df2d2..d19d531 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/api/RequestGenerator.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/RequestGenerator.java @@ -1,7 +1,5 @@ package com.artemis.the.gr8.playerstats.api; -import com.artemis.the.gr8.playerstats.statistic.StatCalculator; -import com.artemis.the.gr8.playerstats.statistic.request.StatRequest; import org.bukkit.Material; import org.bukkit.Statistic; import org.bukkit.entity.EntityType; @@ -9,7 +7,7 @@ /** * Creates an executable {@link StatRequest}. This Request holds all - * the information PlayerStats needs to work with, and is used by the {@link StatCalculator} + * the information PlayerStats needs to work with, and is used * to get the desired statistic data. */ public interface RequestGenerator { diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java b/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java index d920bfc..33eae8c 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java @@ -1,28 +1,55 @@ package com.artemis.the.gr8.playerstats.api; -import com.artemis.the.gr8.playerstats.statistic.request.StatRequest; - import java.util.LinkedHashMap; -/** - * Turns user input into a {@link StatRequest} that can be used to get statistic data - */ public interface StatManager { + /** Checks if the player belonging to this name + * is on PlayerStats' exclude-list (meaning this + * player is not counted for the server total, and + * does not show in top results). + * + * @param playerName the name of the player to check + * @return true if this player is on the exclude-list + */ + boolean isExcludedPlayer(String playerName); + /** Gets a RequestGenerator that can be used to create a PlayerStatRequest. * This RequestGenerator will make sure all default settings * for a player-statistic-lookup are configured. * * @param playerName the player whose statistic is being requested * @return the RequestGenerator */ - RequestGenerator playerStatRequest(String playerName); + RequestGenerator createPlayerStatRequest(String playerName); + + /** + * Executes this StatRequest. This calculation can take some time, + * so don't call this from the main Thread if you can help it! + * + * @return a StatResult containing the value of this lookup, both as + * numerical value and as formatted message + * @see PlayerStats + * @see StatResult + */ + StatResult executePlayerStatRequest(StatRequest request); /** Gets a RequestGenerator that can be used to create a ServerStatRequest. * This RequestGenerator will make sure all default settings * for a server-statistic-lookup are configured. * * @return the RequestGenerator*/ - RequestGenerator serverStatRequest(); + RequestGenerator createServerStatRequest(); + + /** + * Executes this StatRequest. This calculation can take some time, + * so don't call this from the main Thread if you can help it! + * + * @return a StatResult containing the value of this lookup, both as + * numerical value and as formatted message + * @see PlayerStats + * @see StatResult + */ + StatResult executeServerStatRequest(StatRequest request); /** Gets a RequestGenerator that can be used to create a TopStatRequest * for a top-list of the specified size. This RequestGenerator will @@ -30,7 +57,7 @@ public interface StatManager { * * @param topListSize how big the top-x should be (10 by default) * @return the RequestGenerator*/ - RequestGenerator> topStatRequest(int topListSize); + RequestGenerator> createTopStatRequest(int topListSize); /** Gets a RequestGenerator that can be used to create a TopStatRequest * for all offline players on the server (those that are included by @@ -38,5 +65,16 @@ public interface StatManager { * all default settings for a top-statistic-lookup are configured. * * @return the RequestGenerator*/ - RequestGenerator> totalTopStatRequest(); + RequestGenerator> createTotalTopStatRequest(); + + /** + * Executes this StatRequest. This calculation can take some time, + * so don't call this from the main Thread if you can help it! + * + * @return a StatResult containing the value of this lookup, both as + * numerical value and as formatted message + * @see PlayerStats + * @see StatResult + */ + StatResult> executeTopRequest(StatRequest> request); } \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/StatNumberFormatter.java b/src/main/java/com/artemis/the/gr8/playerstats/api/StatNumberFormatter.java new file mode 100644 index 0000000..ea50549 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/StatNumberFormatter.java @@ -0,0 +1,14 @@ +package com.artemis.the.gr8.playerstats.api; + +import com.artemis.the.gr8.playerstats.api.enums.Unit; + +public interface StatNumberFormatter { + + String formatDefaultNumber(long number); + + String formatDamageNumber(long number, Unit statUnit); + + String formatDistanceNumber(long number, Unit statUnit); + + String formatTimeNumber(long number, Unit biggestTimeUnit, Unit smallestTimeUnit); +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/StatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/api/StatRequest.java new file mode 100644 index 0000000..2f2093b --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/StatRequest.java @@ -0,0 +1,161 @@ +package com.artemis.the.gr8.playerstats.api; + +import com.artemis.the.gr8.playerstats.api.enums.Target; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Holds all the information PlayerStats needs to perform + * a lookup, and can be executed by the {@link StatManager} + * to get the results. + */ +public abstract class StatRequest { + + private final Settings settings; + + protected StatRequest(CommandSender requester) { + settings = new Settings(requester); + } + + public abstract boolean isValid(); + + /** + * Use this method to view the settings that have + * been configured for this StatRequest. + */ + public Settings getSettings() { + return settings; + } + + protected void configureForPlayer(String playerName) { + this.settings.target = Target.PLAYER; + this.settings.playerName = playerName; + } + + protected void configureForServer() { + this.settings.target = Target.SERVER; + } + + protected void configureForTop(int topListSize) { + this.settings.target = Target.TOP; + this.settings.topListSize = topListSize; + } + + protected void configureUntyped(@NotNull Statistic statistic) { + if (statistic.getType() != Statistic.Type.UNTYPED) { + throw new IllegalArgumentException("This statistic is not of Type.Untyped"); + } + this.settings.statistic = statistic; + } + + protected void configureBlockOrItemType(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException { + Statistic.Type type = statistic.getType(); + if (type == Statistic.Type.BLOCK && material.isBlock()) { + this.settings.block = material; + } + else if (type == Statistic.Type.ITEM && material.isItem()){ + this.settings.item = material; + } + else { + throw new IllegalArgumentException("Either this statistic is not of Type.Block or Type.Item, or no valid block or item has been provided"); + } + this.settings.statistic = statistic; + this.settings.subStatEntryName = material.toString(); + } + + protected void configureEntityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException { + if (statistic.getType() != Statistic.Type.ENTITY) { + throw new IllegalArgumentException("This statistic is not of Type.Entity"); + } + this.settings.statistic = statistic; + this.settings.entity = entityType; + this.settings.subStatEntryName = entityType.toString(); + } + + protected boolean hasMatchingSubStat() { + if (settings.statistic == null) { + return false; + } + + switch (settings.statistic.getType()) { + case BLOCK -> { + return settings.block != null; + } + case ENTITY -> { + return settings.entity != null; + } + case ITEM -> { + return settings.item != null; + } + default -> { + return true; + } + } + } + + + public static final class Settings { + private final CommandSender sender; + private Statistic statistic; + private String playerName; + private Target target; + private int topListSize; + + private String subStatEntryName; + private EntityType entity; + private Material block; + private Material item; + + /** + * @param sender the CommandSender who prompted this RequestGenerator + */ + private Settings(@NotNull CommandSender sender) { + this.sender = sender; + } + + public @NotNull CommandSender getCommandSender() { + return sender; + } + + public boolean isConsoleSender() { + return sender instanceof ConsoleCommandSender; + } + + public Statistic getStatistic() { + return statistic; + } + + public @Nullable String getSubStatEntryName() { + return subStatEntryName; + } + + public String getPlayerName() { + return playerName; + } + + public @NotNull Target getTarget() { + return target; + } + + public int getTopListSize() { + return this.topListSize; + } + + public EntityType getEntity() { + return entity; + } + + public Material getBlock() { + return block; + } + + public Material getItem() { + return item; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/StatResult.java b/src/main/java/com/artemis/the/gr8/playerstats/api/StatResult.java similarity index 62% rename from src/main/java/com/artemis/the/gr8/playerstats/statistic/result/StatResult.java rename to src/main/java/com/artemis/the/gr8/playerstats/api/StatResult.java index 8c38fec..8f9aff1 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/StatResult.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/StatResult.java @@ -1,6 +1,5 @@ -package com.artemis.the.gr8.playerstats.statistic.result; +package com.artemis.the.gr8.playerstats.api; -import com.artemis.the.gr8.playerstats.api.ApiFormatter; import net.kyori.adventure.platform.bukkit.BukkitAudiences; import net.kyori.adventure.text.TextComponent; @@ -25,54 +24,54 @@ *
[2.] [player-name] [.....] [formatted-number] *
[3.] etc... * - + *

* By default, the resulting message is a {@link TextComponent}, which can be * sent directly to a Minecraft client or console with the Adventure library. * To send a Component, you need to get a {@link BukkitAudiences} object, - * and use that to send the desired Component. Normally you would have to add - * Adventure as a dependency to your project, but since the library is included - * in PlayerStats, you can access it through the PlayerStatsAPI. Information - * on how to get and use the BukkitAudiences object can be found on + * and use that to send the desired Component. Information on how to get + * and use the BukkitAudiences object can be found on * Adventure's website. * - *

You can also use the provided {@link #getFormattedString()} method to get the + *

You can also use the provided {@link #formattedString()} method to get the * same information in String-format. Don't use Adventure's #content() * or #toString() methods on the Components - those won't get the actual * message. And finally, if you want the results to be formatted differently, - * you can get an instance of the {@link ApiFormatter}. + * you can get an instance of the {@link StatTextFormatter}. */ -public interface StatResult { +public record StatResult(T value, TextComponent formattedComponent, String formattedString) { /** - * Gets the raw number for the completed stat-lookup this {@link StatResult} - * stores. + * Gets the raw number for the completed stat-lookup this {@link StatResult} stores. * - * @return {@code Integer} for playerStat, {@code Long} for serverStat, - * and {@code LinkedHashMap} for topStat + * @return {@code Integer} for playerStat, {@code Long} for serverStat, and {@code LinkedHashMap} + * for topStat */ - T getNumericalValue(); + T getNumericalValue() { + return value; + } /** - * Gets the formatted message for the completed stat-lookup this - * StatResult stores. - - * @return a {@code TextComponent} message containing the formatted number. - * This message follows the same style/color/language settings that are - * specified in the PlayerStats config. See class description for more + * Gets the formatted message for the completed stat-lookup this StatResult stores. + * + * @return a {@code TextComponent} message containing the formatted number. This message follows the same + * style/color/language settings that are specified in the PlayerStats config. See class description for more * information. * @see StatResult */ - TextComponent getFormattedTextComponent(); + TextComponent getFormattedTextComponent() { + return formattedComponent; + } /** - * Gets the formatted message for the completed stat-lookup this - * StatResult stores. - - * @return a String message containing the formatted number. This message - * follows the same style and color settings that are specified in the - * PlayerStats config, but it is not translatable (it is always plain English). - * See class description for more information. + * Gets the formatted message for the completed stat-lookup this StatResult stores. + * + * @return a String message containing the formatted number. This message follows the same style and color settings + * that are specified in the PlayerStats config, but it is not translatable (it is always plain English). See class + * description for more information. * @see StatResult */ - String getFormattedString(); + @Override + public String formattedString() { + return formattedString; + } } \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/ApiFormatter.java b/src/main/java/com/artemis/the/gr8/playerstats/api/StatTextFormatter.java similarity index 94% rename from src/main/java/com/artemis/the/gr8/playerstats/api/ApiFormatter.java rename to src/main/java/com/artemis/the/gr8/playerstats/api/StatTextFormatter.java index e6f8b1f..b062e07 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/api/ApiFormatter.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/StatTextFormatter.java @@ -1,9 +1,6 @@ package com.artemis.the.gr8.playerstats.api; -import com.artemis.the.gr8.playerstats.enums.Unit; -import com.artemis.the.gr8.playerstats.msg.components.ComponentUtils; -import com.artemis.the.gr8.playerstats.msg.msgutils.NumberFormatter; -import com.artemis.the.gr8.playerstats.statistic.result.StatResult; +import com.artemis.the.gr8.playerstats.api.enums.Unit; import net.kyori.adventure.text.TextComponent; import org.bukkit.Statistic; import org.jetbrains.annotations.Nullable; @@ -16,7 +13,7 @@ * @see StatResult */ -public interface ApiFormatter { +public interface StatTextFormatter { /** * Turns a TextComponent into its String representation. This method is equipped @@ -28,19 +25,7 @@ public interface ApiFormatter { * but with color, style and formatting. TranslatableComponents will be turned into * plain English. */ - default String TextComponentToString(TextComponent component) { - return ComponentUtils.getTranslatableComponentSerializer() - .serialize(component); - } - - /** - * Gets a {@link NumberFormatter} to format raw numbers into something more readable. - * - * @return the NumberFormatter - */ - default NumberFormatter getNumberFormatter() { - return new NumberFormatter(); - } + String textComponentToString(TextComponent component); /** * Gets the default prefix PlayerStats uses. diff --git a/src/main/java/com/artemis/the/gr8/playerstats/enums/Target.java b/src/main/java/com/artemis/the/gr8/playerstats/api/enums/Target.java similarity index 75% rename from src/main/java/com/artemis/the/gr8/playerstats/enums/Target.java rename to src/main/java/com/artemis/the/gr8/playerstats/api/enums/Target.java index 3824ee1..8444a3c 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/enums/Target.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/enums/Target.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.enums; +package com.artemis.the.gr8.playerstats.api.enums; /** * This enum represents the targets PlayerStats accepts diff --git a/src/main/java/com/artemis/the/gr8/playerstats/enums/Unit.java b/src/main/java/com/artemis/the/gr8/playerstats/api/enums/Unit.java similarity index 94% rename from src/main/java/com/artemis/the/gr8/playerstats/enums/Unit.java rename to src/main/java/com/artemis/the/gr8/playerstats/api/enums/Unit.java index dc6e997..0a63e10 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/enums/Unit.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/enums/Unit.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.enums; +package com.artemis.the.gr8.playerstats.api.enums; import org.bukkit.Statistic; import org.jetbrains.annotations.NotNull; @@ -185,10 +185,12 @@ match exactly (it can be "day" or "days", for example), and is case-insensitive. } /** - * Gets the most suitable Unit for this number. + * Gets the largest Unit this number can be expressed in as a whole number. + * For example, for Type TIME a value of 80.000 would return Unit.HOUR + * (80.000 ticks equals 4.000 seconds, 67 minutes, or 1 hour) * - * @param type the Unit.Type of the statistic this number belongs to - * @param number the statistic number as returned by Player.getStatistic() + * @param type the Unit.Type of this statistic + * @param number the statistic value in ticks as returned by Player.getStatistic() * @return the Unit */ public static Unit getMostSuitableUnit(Unit.Type type, long number) { diff --git a/src/main/java/com/artemis/the/gr8/playerstats/commands/StatCommand.java b/src/main/java/com/artemis/the/gr8/playerstats/commands/StatCommand.java deleted file mode 100644 index 84d4456..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/commands/StatCommand.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.artemis.the.gr8.playerstats.commands; - -import com.artemis.the.gr8.playerstats.ThreadManager; -import com.artemis.the.gr8.playerstats.enums.StandardMessage; -import com.artemis.the.gr8.playerstats.enums.Target; -import com.artemis.the.gr8.playerstats.msg.OutputManager; -import com.artemis.the.gr8.playerstats.statistic.request.RequestHandler; -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import org.bukkit.Statistic; -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -public final class StatCommand implements CommandExecutor { - - private static ThreadManager threadManager; - private static OutputManager outputManager; - - public StatCommand(OutputManager m, ThreadManager t) { - threadManager = t; - outputManager = m; - } - - @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - if (args.length == 0 || args[0].equalsIgnoreCase("help")) { //in case of less than 1 argument or "help", display the help message - outputManager.sendHelp(sender); - } - else if (args[0].equalsIgnoreCase("examples") || - args[0].equalsIgnoreCase("example")) { //in case of "statistic examples", show examples - outputManager.sendExamples(sender); - } - else { - RequestSettings baseRequest = RequestHandler.getBasicInternalStatRequest(sender); - RequestHandler requestHandler = new RequestHandler(baseRequest); - - RequestSettings completedRequest = requestHandler.getRequestFromArgs(args); - if (completedRequest.isValid()) { - threadManager.startStatThread(completedRequest); - } else { - sendFeedback(completedRequest); - return false; - } - } - return true; - } - - /** - * If a given {@link RequestSettings} object does not result in a valid - * statistic look-up, this will send a feedback message to the CommandSender - * that made the request. The following is checked: - *

    - *
  • Is a statistic set? - *
  • Is a subStatEntry needed, and if so, is a corresponding Material/EntityType present? - *
  • If the target is Player, is a valid playerName provided? - *
- * - * @param requestSettings the RequestSettings to give feedback on - */ - private void sendFeedback(RequestSettings requestSettings) { - CommandSender sender = requestSettings.getCommandSender(); - - if (requestSettings.getStatistic() == null) { - outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_STAT_NAME); - } - else if (requestSettings.getTarget() == Target.PLAYER && requestSettings.getPlayerName() == null) { - outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_PLAYER_NAME); - } - else { - Statistic.Type type = requestSettings.getStatistic().getType(); - if (type != Statistic.Type.UNTYPED && requestSettings.getSubStatEntryName() == null) { - outputManager.sendFeedbackMsgMissingSubStat(sender, type); - } else { - outputManager.sendFeedbackMsgWrongSubStat(sender, type, requestSettings.getSubStatEntryName()); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/commands/TabCompleter.java b/src/main/java/com/artemis/the/gr8/playerstats/commands/TabCompleter.java deleted file mode 100644 index deab6a8..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/commands/TabCompleter.java +++ /dev/null @@ -1,116 +0,0 @@ -package com.artemis.the.gr8.playerstats.commands; - -import com.artemis.the.gr8.playerstats.utils.EnumHandler; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import com.artemis.the.gr8.playerstats.commands.cmdutils.TabCompleteHelper; -import org.bukkit.Statistic; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; - -public final class TabCompleter implements org.bukkit.command.TabCompleter { - - private final EnumHandler enumHandler; - private final OfflinePlayerHandler offlinePlayerHandler; - private final TabCompleteHelper tabCompleteHelper; - - private final List commandOptions; - - public TabCompleter(EnumHandler enumHandler, OfflinePlayerHandler offlinePlayerHandler) { - this.enumHandler = enumHandler; - this.offlinePlayerHandler = offlinePlayerHandler; - tabCompleteHelper = new TabCompleteHelper(enumHandler); - - commandOptions = new ArrayList<>(); - commandOptions.add("top"); - commandOptions.add("player"); - commandOptions.add("server"); - commandOptions.add("me"); - - } - - //args[0] = statistic (length = 1) - //args[1] = commandOption (top/player/me) OR substatistic (block/item/entitytype) (length = 2) - //args[2] = executorName OR commandOption (top/player/me) (length = 3) - //args[3] = executorName (length = 4) - - @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { - List tabSuggestions = new ArrayList<>(); - - if (args.length >= 1) { - String currentArg = args[args.length -1]; - - if (args.length == 1) { //after typing "stat", suggest a list of viable statistics - tabSuggestions = getFirstArgSuggestions(args[0]); - } - - else { //after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions - String previousArg = args[args.length -2]; - - if (enumHandler.isStatistic(previousArg)) { - Statistic stat = EnumHandler.getStatEnum(previousArg); - if (stat != null) { - tabSuggestions = getTabSuggestions(getRelevantList(stat), currentArg); - } - } - - //if previous arg = "player" - else if (previousArg.equalsIgnoreCase("player")) { - - if (args.length >= 3 && enumHandler.isEntityStatistic(args[args.length-3])) { - tabSuggestions = commandOptions; //if arg before "player" was entity-stat, suggest commandOptions - } - else { //otherwise "player" is target-flag: suggest playerNames - tabSuggestions = getTabSuggestions(offlinePlayerHandler.getOfflinePlayerNames(), currentArg); - } - } - - //after a substatistic, suggest commandOptions - else if (enumHandler.isSubStatEntry(previousArg)) { - tabSuggestions = commandOptions; - } - } - } - return tabSuggestions; - } - - private List getFirstArgSuggestions(String currentArg) { - List suggestions = enumHandler.getStatNames(); - suggestions.add("examples"); - suggestions.add("help"); - return getTabSuggestions(suggestions, currentArg); - } - - private List getTabSuggestions(List completeList, String currentArg) { - return completeList.stream() - .filter(item -> item.toLowerCase(Locale.ENGLISH).contains(currentArg.toLowerCase(Locale.ENGLISH))) - .collect(Collectors.toList()); - } - - private List getRelevantList(Statistic stat) { - switch (stat.getType()) { - case BLOCK -> { - return tabCompleteHelper.getAllBlockNames(); - } - case ITEM -> { - if (stat == Statistic.BREAK_ITEM) { - return tabCompleteHelper.getItemBrokenSuggestions(); - } else { - return tabCompleteHelper.getAllItemNames(); - } - } - case ENTITY -> { - return tabCompleteHelper.getEntitySuggestions(); - } - default -> { - return commandOptions; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/commands/cmdutils/TabCompleteHelper.java b/src/main/java/com/artemis/the/gr8/playerstats/commands/cmdutils/TabCompleteHelper.java deleted file mode 100644 index ab4f00a..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/commands/cmdutils/TabCompleteHelper.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.artemis.the.gr8.playerstats.commands.cmdutils; - -import com.artemis.the.gr8.playerstats.utils.EnumHandler; -import org.bukkit.Material; -import org.bukkit.entity.EntityType; - -import java.util.Arrays; -import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; - -public final class TabCompleteHelper { - - private final EnumHandler enumHandler; - private static List itemBrokenSuggestions; - private static List entitySuggestions; - - public TabCompleteHelper(EnumHandler enumHandler) { - this.enumHandler = enumHandler; - prepareLists(); - } - - public List getAllItemNames() { - return enumHandler.getItemNames(); - } - - public List getItemBrokenSuggestions() { - return itemBrokenSuggestions; - } - - public List getAllBlockNames() { - return enumHandler.getBlockNames(); - } - - public List getEntitySuggestions() { - return entitySuggestions; - } - - - private static void prepareLists() { - //breaking an item means running its durability negative - itemBrokenSuggestions = Arrays.stream(Material.values()) - .parallel() - .filter(Material::isItem) - .filter(item -> item.getMaxDurability() != 0) - .map(Material::toString) - .map(string -> string.toLowerCase(Locale.ENGLISH)) - .collect(Collectors.toList()); - - //the only statistics dealing with entities are killed_entity and entity_killed_by - entitySuggestions = Arrays.stream(EntityType.values()) - .parallel() - .filter(EntityType::isAlive) - .map(EntityType::toString) - .map(string -> string.toLowerCase(Locale.ENGLISH)) - .collect(Collectors.toList()); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/config/ConfigUpdateHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/config/ConfigUpdateHandler.java deleted file mode 100644 index 905dc8e..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/config/ConfigUpdateHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.artemis.the.gr8.playerstats.config; - -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import org.bukkit.configuration.file.YamlConfiguration; - -import java.io.File; -import java.io.IOException; - -import com.tchristofferson.configupdater.ConfigUpdater; - -public final class ConfigUpdateHandler { - - /** - * Add new key-value pairs to the config without losing comments, - * using tchristofferson's Config-Updater - */ - public ConfigUpdateHandler(Main plugin, File configFile, int configVersion) { - YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile); - updateTopListDefault(configuration); - updateDefaultColors(configuration); - configuration.set("config-version", configVersion); - try { - configuration.save(configFile); - ConfigUpdater.update(plugin, configFile.getName(), configFile); - MyLogger.logLowLevelMsg("Your config has been updated to version " + configVersion + - ", but all of your custom settings should still be there!"); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * Adjusts the value for "top-list" to migrate the config file from - * versions 1 or 2 to version 3 and above. - */ - private void updateTopListDefault(YamlConfiguration configuration) { - String oldTitle = configuration.getString("top-list-title"); - if (oldTitle != null && oldTitle.equalsIgnoreCase("Top [x]")) { - configuration.set("top-list-title", "Top"); - } - } - - /** - * Adjusts some of the default colors to migrate from versions 2 - * or 3 to version 4 and above. - */ - private void updateDefaultColors(YamlConfiguration configuration) { - updateColor(configuration, "top-list.title", "yellow", "#FFD52B"); - updateColor(configuration, "top-list.title", "#FFEA40", "#FFD52B"); - updateColor(configuration, "top-list.stat-names", "yellow", "#FFD52B"); - updateColor(configuration, "top-list.stat-names", "#FFEA40", "#FFD52B"); - updateColor(configuration, "top-list.sub-stat-names", "#FFD52B", "yellow"); - - updateColor(configuration, "individual-statistics.stat-names", "yellow", "#FFD52B"); - updateColor(configuration, "individual-statistics.sub-stat-names", "#FFD52B", "yellow"); - updateColor(configuration, "total-server.title", "gold", "#55AAFF"); - updateColor(configuration, "total-server.server-name", "gold", "#55AAFF"); - updateColor(configuration, "total-server.stat-names", "yellow", "#FFD52B"); - updateColor(configuration, "total-server.sub-stat-names", "#FFD52B", "yellow"); - } - - private void updateColor(YamlConfiguration configuration, String path, String oldValue, String newValue) { - String configString = configuration.getString(path); - if (configString != null && configString.equalsIgnoreCase(oldValue)) { - configuration.set(path, newValue); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/Main.java b/src/main/java/com/artemis/the/gr8/playerstats/core/Main.java new file mode 100644 index 0000000..5eaa9d6 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/Main.java @@ -0,0 +1,189 @@ +package com.artemis.the.gr8.playerstats.core; + +import com.artemis.the.gr8.playerstats.api.PlayerStats; +import com.artemis.the.gr8.playerstats.api.StatNumberFormatter; +import com.artemis.the.gr8.playerstats.api.StatTextFormatter; +import com.artemis.the.gr8.playerstats.api.StatManager; +import com.artemis.the.gr8.playerstats.core.commands.*; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.NumberFormatter; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; +import com.artemis.the.gr8.playerstats.core.statrequest.RequestManager; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.listeners.JoinListener; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.LanguageKeyHandler; +import com.artemis.the.gr8.playerstats.core.sharing.ShareManager; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import me.clip.placeholderapi.PlaceholderAPIPlugin; +import me.clip.placeholderapi.expansion.PlaceholderExpansion; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import org.bstats.bukkit.Metrics; +import org.bstats.charts.SimplePie; +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +/** + * PlayerStats' Main class + */ +public final class Main extends JavaPlugin implements PlayerStats { + + private static JavaPlugin pluginInstance; + private static PlayerStats playerStatsAPI; + private static BukkitAudiences adventure; + + private static ConfigHandler config; + private static ThreadManager threadManager; + private static LanguageKeyHandler languageKeyHandler; + private static OfflinePlayerHandler offlinePlayerHandler; + + private static RequestManager requestManager; + private static OutputManager outputManager; + private static ShareManager shareManager; + + @Override + public void onEnable() { + initializeMainClasses(); + registerCommands(); + setupMetrics(); + + //register the listener + Bukkit.getPluginManager().registerEvents(new JoinListener(threadManager), this); + + //finish up + this.getLogger().info("Enabled PlayerStats!"); + } + + @Override + public void onDisable() { + if (adventure != null) { + adventure.close(); + adventure = null; + } + this.getLogger().info("Disabled PlayerStats!"); + } + + public void reloadPlugin() { + config.reload(); + MyLogger.setDebugLevel(config.getDebugLevel()); + languageKeyHandler.reload(); + offlinePlayerHandler.reload(); + outputManager.updateSettings(); + shareManager.updateSettings(); + } + + /** + * + * @return the JavaPlugin instance associated with PlayerStats + * @throws IllegalStateException if PlayerStats is not enabled + */ + public static @NotNull JavaPlugin getPluginInstance() throws IllegalStateException { + if (pluginInstance == null) { + throw new IllegalStateException("PlayerStats is not loaded!"); + } + return pluginInstance; + } + + public static @NotNull PlayerStats getPlayerStatsAPI() throws IllegalStateException { + if (playerStatsAPI == null) { + throw new IllegalStateException("PlayerStats does not seem to be loaded!"); + } + return playerStatsAPI; + } + + /** + * Initialize all classes that need initializing, + * and store references to classes that are + * needed for the Command classes or the API. + */ + private void initializeMainClasses() { + pluginInstance = this; + playerStatsAPI = this; + adventure = BukkitAudiences.create(this); + + config = ConfigHandler.getInstance(); + languageKeyHandler = LanguageKeyHandler.getInstance(); + offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + shareManager = ShareManager.getInstance(); + + outputManager = new OutputManager(adventure); + requestManager = new RequestManager(outputManager); + threadManager = new ThreadManager(this, outputManager); + } + + /** + * Register all commands and assign the tabCompleter + * to the relevant commands. + */ + private void registerCommands() { + TabCompleter tabCompleter = new TabCompleter(); + + PluginCommand statcmd = this.getCommand("statistic"); + if (statcmd != null) { + statcmd.setExecutor(new StatCommand(outputManager, threadManager)); + statcmd.setTabCompleter(tabCompleter); + } + PluginCommand excludecmd = this.getCommand("statisticexclude"); + if (excludecmd != null) { + excludecmd.setExecutor(new ExcludeCommand(outputManager)); + excludecmd.setTabCompleter(tabCompleter); + } + + PluginCommand reloadcmd = this.getCommand("statisticreload"); + if (reloadcmd != null) { + reloadcmd.setExecutor(new ReloadCommand(threadManager)); + } + PluginCommand sharecmd = this.getCommand("statisticshare"); + if (sharecmd != null) { + sharecmd.setExecutor(new ShareCommand(outputManager)); + } + } + + /** + * Setup bstats + */ + private void setupMetrics() { + new BukkitRunnable() { + @Override + public void run() { + final Metrics metrics = new Metrics(pluginInstance, 15923); + final boolean placeholderExpansionActive; + if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) { + PlaceholderExpansion expansion = PlaceholderAPIPlugin + .getInstance() + .getLocalExpansionManager() + .getExpansion("playerstats"); + placeholderExpansionActive = expansion != null; + } else { + placeholderExpansionActive = false; + } + metrics.addCustomChart(new SimplePie("using_placeholder_expansion", () -> placeholderExpansionActive ? "yes" : "no")); + } + }.runTaskLaterAsynchronously(this, 200); + } + + @Override + public @NotNull String getVersion() { + return String.valueOf(this.getDescription().getVersion().charAt(0)); + } + + @Override + public StatManager getStatManager() { + return requestManager; + } + + @Override + public StatTextFormatter getStatTextFormatter() { + return outputManager.getMainMessageBuilder(); + } + + @Contract(" -> new") + @Override + public @NotNull StatNumberFormatter getStatNumberFormatter() { + return new NumberFormatter(); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ExcludeCommand.java b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ExcludeCommand.java new file mode 100644 index 0000000..8b756a6 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ExcludeCommand.java @@ -0,0 +1,57 @@ +package com.artemis.the.gr8.playerstats.core.commands; + +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; + +public final class ExcludeCommand implements CommandExecutor { + + private static OutputManager outputManager; + private final OfflinePlayerHandler offlinePlayerHandler; + + public ExcludeCommand(OutputManager outputManager) { + ExcludeCommand.outputManager = outputManager; + this.offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + outputManager.sendExcludeInfo(sender); + } + else if (args.length == 1) { + switch (args[0]) { + case "info" -> outputManager.sendExcludeInfo(sender); + case "list" -> { + ArrayList excludedPlayers = offlinePlayerHandler.getExcludedPlayerNames(); + outputManager.sendExcludedList(sender, excludedPlayers); + } + } + } + else { + switch (args[0]) { + case "add" -> { + if (offlinePlayerHandler.addPlayerToExcludeList(args[1])) { + outputManager.sendFeedbackMsgPlayerExcluded(sender, args[1]); + } else { + outputManager.sendFeedbackMsg(sender, StandardMessage.EXCLUDE_FAILED); + } + } + case "remove" -> { + if (offlinePlayerHandler.removePlayerFromExcludeList(args[1])) { + outputManager.sendFeedbackMsgPlayerIncluded(sender, args[1]); + } else { + outputManager.sendFeedbackMsg(sender, StandardMessage.INCLUDE_FAILED); + } + } + } + } + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/commands/ReloadCommand.java b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ReloadCommand.java similarity index 58% rename from src/main/java/com/artemis/the/gr8/playerstats/commands/ReloadCommand.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/commands/ReloadCommand.java index 6b13458..e3f0036 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/commands/ReloadCommand.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ReloadCommand.java @@ -1,6 +1,6 @@ -package com.artemis.the.gr8.playerstats.commands; +package com.artemis.the.gr8.playerstats.core.commands; -import com.artemis.the.gr8.playerstats.ThreadManager; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -11,12 +11,12 @@ public final class ReloadCommand implements CommandExecutor { private static ThreadManager threadManager; - public ReloadCommand(ThreadManager t) { - threadManager = t; + public ReloadCommand(ThreadManager threadManager) { + ReloadCommand.threadManager = threadManager; } @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) { + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { threadManager.startReloadThread(sender); return true; } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/commands/ShareCommand.java b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ShareCommand.java similarity index 66% rename from src/main/java/com/artemis/the/gr8/playerstats/commands/ShareCommand.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/commands/ShareCommand.java index f3961c4..ffcd633 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/commands/ShareCommand.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/ShareCommand.java @@ -1,10 +1,10 @@ -package com.artemis.the.gr8.playerstats.commands; +package com.artemis.the.gr8.playerstats.core.commands; -import com.artemis.the.gr8.playerstats.ShareManager; -import com.artemis.the.gr8.playerstats.enums.StandardMessage; -import com.artemis.the.gr8.playerstats.msg.OutputManager; -import com.artemis.the.gr8.playerstats.statistic.result.InternalStatResult; -import com.artemis.the.gr8.playerstats.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.sharing.ShareManager; +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.sharing.StoredResult; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -12,17 +12,17 @@ public final class ShareCommand implements CommandExecutor { - private static ShareManager shareManager; private static OutputManager outputManager; + private static ShareManager shareManager; - public ShareCommand(ShareManager s, OutputManager m) { - shareManager = s; - outputManager = m; + public ShareCommand(OutputManager outputManager) { + ShareCommand.outputManager = outputManager; + shareManager = ShareManager.getInstance(); } @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { - if (args.length == 1 && ShareManager.isEnabled()) { + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, @NotNull String[] args) { + if (args.length == 1 && shareManager.isEnabled()) { int shareCode; try { shareCode = Integer.parseInt(args[0]); @@ -37,7 +37,7 @@ else if (shareManager.isOnCoolDown(sender.getName())) { outputManager.sendFeedbackMsg(sender, StandardMessage.STILL_ON_SHARE_COOLDOWN); } else { - InternalStatResult result = shareManager.getStatResult(sender.getName(), shareCode); + StoredResult result = shareManager.getStatResult(sender.getName(), shareCode); if (result == null) { //at this point the only possible cause of formattedComponent being null is the request being older than 25 player-requests ago outputManager.sendFeedbackMsg(sender, StandardMessage.STAT_RESULTS_TOO_OLD); } else { diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/commands/StatCommand.java b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/StatCommand.java new file mode 100644 index 0000000..ee88433 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/StatCommand.java @@ -0,0 +1,270 @@ +package com.artemis.the.gr8.playerstats.core.commands; + +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; +import com.artemis.the.gr8.playerstats.api.RequestGenerator; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import com.artemis.the.gr8.playerstats.api.enums.Target; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.statrequest.PlayerStatRequest; +import com.artemis.the.gr8.playerstats.core.statrequest.ServerStatRequest; +import com.artemis.the.gr8.playerstats.core.statrequest.TopStatRequest; +import com.artemis.the.gr8.playerstats.core.utils.EnumHandler; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.EntityType; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; +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.regex.Matcher; +import java.util.regex.Pattern; + +public final class StatCommand implements CommandExecutor { + + private static final Pattern pattern = Pattern.compile("top|server|me|player"); + + private static ThreadManager threadManager; + private static OutputManager outputManager; + private final ConfigHandler config; + private final EnumHandler enumHandler; + private final OfflinePlayerHandler offlinePlayerHandler; + + public StatCommand(OutputManager outputManager, ThreadManager threadManager) { + StatCommand.threadManager = threadManager; + StatCommand.outputManager = outputManager; + + config = ConfigHandler.getInstance(); + enumHandler = EnumHandler.getInstance(); + offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0 || + args[0].equalsIgnoreCase("help") || + args[0].equalsIgnoreCase("info")) { + outputManager.sendHelp(sender); + } + else if (args[0].equalsIgnoreCase("examples") || + args[0].equalsIgnoreCase("example")) { + outputManager.sendExamples(sender); + } + else { + ArgProcessor processor = new ArgProcessor(sender, args); + if (processor.request != null && processor.request.isValid()) { + threadManager.startStatThread(processor.request); + } else { + sendFeedback(sender, processor); + } + } + return true; + } + + /** + * Analyzes the provided args and sends an appropriate + * feedback message to the CommandSender that called the + * stat command. The following is checked: + *
    + *
  • Is a statistic set? + *
  • Is a subStatEntry needed, and if so, + * is a corresponding Material/EntityType present? + *
  • If the target is Player, is a valid + * playerName provided? + *
+ * + * @param sender the CommandSender to send feedback to + * @param processor the ArgProcessor object that holds + * the analyzed args + */ + private void sendFeedback(CommandSender sender, @NotNull ArgProcessor processor) { + if (processor.statistic == null) { + outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_STAT_NAME); + } + else if (processor.target == Target.PLAYER) { + if (processor.playerName == null) { + outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_PLAYER_NAME); + } else if (offlinePlayerHandler.isExcludedPlayer(processor.playerName) && + !config.allowPlayerLookupsForExcludedPlayers()) { + outputManager.sendFeedbackMsg(sender, StandardMessage.PLAYER_IS_EXCLUDED); + } + } + else { + Statistic.Type type = processor.statistic.getType(); + String statType = enumHandler.getSubStatTypeName(type); + + if (type != Statistic.Type.UNTYPED && processor.subStatName == null) { + outputManager.sendFeedbackMsgMissingSubStat(sender, statType); + } else { + outputManager.sendFeedbackMsgWrongSubStat(sender, statType, processor.subStatName); + } + } + } + + private final class ArgProcessor { + + private final CommandSender sender; + private String[] argsToProcess; + + private Statistic statistic; + private String subStatName; + private Target target; + private String playerName; + private StatRequest request; + + private ArgProcessor(CommandSender sender, String[] args) { + this.sender = sender; + this.argsToProcess = args; + + extractStatistic(); + extractSubStatistic(); + extractTarget(); + combineProcessedArgsIntoRequest(); + } + + private void combineProcessedArgsIntoRequest() { + if (statistic == null || + target == Target.PLAYER && playerName == null) { + return; + } + + RequestGenerator requestGenerator = + switch (target) { + case PLAYER -> new PlayerStatRequest(sender, playerName); + case SERVER -> new ServerStatRequest(sender); + case TOP -> new TopStatRequest(sender, config.getTopListMaxSize()); + }; + + switch (statistic.getType()) { + case UNTYPED -> request = requestGenerator.untyped(statistic); + case BLOCK -> { + Material block = enumHandler.getBlockEnum(subStatName); + if (block != null) { + request = requestGenerator.blockOrItemType(statistic, block); + } + } + case ITEM -> { + Material item = enumHandler.getItemEnum(subStatName); + if (item != null) { + request = requestGenerator.blockOrItemType(statistic, item); + } + } + case ENTITY -> { + EntityType entity = enumHandler.getEntityEnum(subStatName); + if (entity != null) { + request = requestGenerator.entityType(statistic, entity); + } + } + } + } + + private void extractTarget() { + String targetArg = null; + for (String arg : argsToProcess) { + Matcher matcher = pattern.matcher(arg); + if (matcher.find()) { + targetArg = matcher.group(); + switch (targetArg) { + case "me" -> { + if (sender instanceof Player) { + target = Target.PLAYER; + playerName = sender.getName(); + } else { + target = Target.SERVER; + } + } + case "player" -> { + target = Target.PLAYER; + playerName = tryToFindPlayerName(argsToProcess); + } + case "server" -> target = Target.SERVER; + case "top" -> target = Target.TOP; + } + argsToProcess = removeArg(targetArg); + break; + } + } + + if (targetArg == null) { + String playerName = tryToFindPlayerName(argsToProcess); + if (playerName != null) { + target = Target.PLAYER; + this.playerName = playerName; + } else { + target = Target.TOP; + } + } + } + + private void extractStatistic() { + String statName = null; + for (String arg : argsToProcess) { + if (enumHandler.isStatistic(arg)) { + statName = arg; + break; + } + } + if (statName != null) { + statistic = enumHandler.getStatEnum(statName); + argsToProcess = removeArg(statName); + } + } + + private void extractSubStatistic() { + if (statistic == null || + statistic.getType() == Statistic.Type.UNTYPED || + argsToProcess.length == 0) { + return; + } + + String subStatName = null; + List subStats = Arrays.stream(argsToProcess) + .filter(enumHandler::isSubStatEntry) + .toList(); + if (subStats.isEmpty()) { + return; + } + else if (subStats.size() == 1) { + subStatName = subStats.get(0); + } + else { + for (String arg : subStats) { + if (!arg.equalsIgnoreCase("player")) { + subStatName = arg; + break; + } + } + if (subStatName == null) { + subStatName = "player"; + } + } + this.subStatName = subStatName; + argsToProcess = removeArg(subStatName); + } + + @Contract(pure = true) + private @Nullable String tryToFindPlayerName(@NotNull String[] args) { + for (String arg : args) { + if (offlinePlayerHandler.isIncludedPlayer(arg) || offlinePlayerHandler.isExcludedPlayer(arg)) { + return arg; + } + } + return null; + } + + private String[] removeArg(String argToRemove) { + ArrayList currentArgs = new ArrayList<>(Arrays.asList(argsToProcess)); + currentArgs.remove(argToRemove); + return currentArgs.toArray(String[]::new); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/commands/TabCompleter.java b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/TabCompleter.java new file mode 100644 index 0000000..688852f --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/commands/TabCompleter.java @@ -0,0 +1,162 @@ +package com.artemis.the.gr8.playerstats.core.commands; + +import com.artemis.the.gr8.playerstats.core.utils.EnumHandler; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import org.bukkit.Material; +import org.bukkit.Statistic; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +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.Locale; +import java.util.stream.Collectors; + +public final class TabCompleter implements org.bukkit.command.TabCompleter { + + private final OfflinePlayerHandler offlinePlayerHandler; + private final EnumHandler enumHandler; + + private List statCommandTargets; + private List excludeCommandOptions; + private List itemsThatCanBreak; + private List entitiesThatCanDie; + + public TabCompleter() { + offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + enumHandler = EnumHandler.getInstance(); + prepareLists(); + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (command.getName().equalsIgnoreCase("statistic")) { + return getStatCommandSuggestions(args); + } + else if (command.getName().equalsIgnoreCase("statisticexclude")) { + return getExcludeCommandSuggestions(args); + } + return null; + } + + private @Nullable List getExcludeCommandSuggestions(@NotNull String[] args) { + if (args.length == 0) { + return null; + } + + List tabSuggestions = new ArrayList<>(); + if (args.length == 1) { + tabSuggestions = excludeCommandOptions; + } + else if (args.length == 2) { + tabSuggestions = switch (args[0]) { + case "add" -> offlinePlayerHandler.getIncludedOfflinePlayerNames(); + case "remove" -> offlinePlayerHandler.getExcludedPlayerNames(); + default -> tabSuggestions; + }; + } + return getDynamicTabSuggestions(tabSuggestions, args[args.length-1]); + } + + private @Nullable List getStatCommandSuggestions(@NotNull String[] args) { + if (args.length == 0) { + return null; + } + + List tabSuggestions = new ArrayList<>(); + if (args.length == 1) { + tabSuggestions = firstStatCommandArgSuggestions(); + } + else { + String previousArg = args[args.length-2]; + + //after checking if args[0] is a viable statistic, suggest sub-stat or targets + if (enumHandler.isStatistic(previousArg)) { + Statistic stat = enumHandler.getStatEnum(previousArg); + if (stat != null) { + tabSuggestions = suggestionsAfterFirstStatCommandArg(stat); + } + } + else if (previousArg.equalsIgnoreCase("player")) { + if (args.length >= 3 && enumHandler.isEntityStatistic(args[args.length-3])) { + tabSuggestions = statCommandTargets; //if arg before "player" was entity-sub-stat, suggest targets + } + else { //otherwise "player" is the target: suggest playerNames + tabSuggestions = offlinePlayerHandler.getIncludedOfflinePlayerNames(); + } + } + + //after a substatistic, suggest targets + else if (enumHandler.isSubStatEntry(previousArg)) { + tabSuggestions = statCommandTargets; + } + } + return getDynamicTabSuggestions(tabSuggestions, args[args.length-1]); + } + + /** + * These tabSuggestions take into account that the commandSender + * will have been typing, so they are filtered for the letters + * that have already been typed. + */ + private List getDynamicTabSuggestions(@NotNull List completeList, String currentArg) { + return completeList.stream() + .filter(item -> item.toLowerCase(Locale.ENGLISH).contains(currentArg.toLowerCase(Locale.ENGLISH))) + .collect(Collectors.toList()); + } + + private @NotNull List firstStatCommandArgSuggestions() { + List suggestions = enumHandler.getAllStatNames(); + suggestions.add("examples"); + suggestions.add("info"); + suggestions.add("help"); + return suggestions; + } + + private List suggestionsAfterFirstStatCommandArg(@NotNull Statistic stat) { + switch (stat.getType()) { + case BLOCK -> { + return enumHandler.getAllBlockNames(); + } + case ITEM -> { + if (stat == Statistic.BREAK_ITEM) { + return itemsThatCanBreak; + } else { + return enumHandler.getAllItemNames(); + } + } + case ENTITY -> { + return entitiesThatCanDie; + } + default -> { + return statCommandTargets; + } + } + } + + private void prepareLists() { + statCommandTargets = List.of("top", "player", "server", "me"); + excludeCommandOptions = List.of("add", "list", "remove", "info"); + + //breaking an item means running its durability negative + itemsThatCanBreak = Arrays.stream(Material.values()) + .parallel() + .filter(Material::isItem) + .filter(item -> item.getMaxDurability() != 0) + .map(Material::toString) + .map(string -> string.toLowerCase(Locale.ENGLISH)) + .collect(Collectors.toList()); + + //the only statistics dealing with entities are killed_entity and entity_killed_by + entitiesThatCanDie = Arrays.stream(EntityType.values()) + .parallel() + .filter(EntityType::isAlive) + .map(EntityType::toString) + .map(string -> string.toLowerCase(Locale.ENGLISH)) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/config/ConfigHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/core/config/ConfigHandler.java similarity index 90% rename from src/main/java/com/artemis/the/gr8/playerstats/config/ConfigHandler.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/config/ConfigHandler.java index c40010e..d64de0b 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/config/ConfigHandler.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/config/ConfigHandler.java @@ -1,39 +1,54 @@ -package com.artemis.the.gr8.playerstats.config; +package com.artemis.the.gr8.playerstats.core.config; -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.enums.Target; -import com.artemis.the.gr8.playerstats.enums.Unit; -import com.artemis.the.gr8.playerstats.utils.MyLogger; +import com.artemis.the.gr8.playerstats.api.enums.Target; +import com.artemis.the.gr8.playerstats.api.enums.Unit; +import com.artemis.the.gr8.playerstats.core.utils.FileHandler; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; 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; +import java.util.Map; /** Handles all PlayerStats' config-settings. */ -public final class ConfigHandler { +public final class ConfigHandler extends FileHandler { - private static Main plugin; - private static int configVersion; - - private File configFile; + private static volatile ConfigHandler instance; + private final int configVersion; private FileConfiguration config; - public ConfigHandler(Main plugin) { - ConfigHandler.plugin = plugin; - configVersion = 6; - - saveDefaultConfig(); - config = YamlConfiguration.loadConfiguration(configFile); - checkConfigVersion(); + private ConfigHandler() { + super("config.yml"); + config = super.getFileConfiguration(); + configVersion = 7; + checkAndUpdateConfigVersion(); MyLogger.setDebugLevel(getDebugLevel()); } + public static ConfigHandler getInstance() { + ConfigHandler localVar = instance; + if (localVar != null) { + return localVar; + } + + synchronized (ConfigHandler.class) { + if (instance == null) { + instance = new ConfigHandler(); + } + return instance; + } + } + + @Override + public void reload() { + super.reload(); + config = super.getFileConfiguration(); + } + /** * Checks the number that "config-version" returns to see if the - * config needs updating, and if so, send it to the {@link ConfigUpdateHandler}. + * config needs updating, and if so, updates it. *
*
PlayerStats 1.1: "config-version" doesn't exist. *
PlayerStats 1.2: "config-version" is 2. @@ -42,41 +57,17 @@ public ConfigHandler(Main plugin) { *
PlayerStats 1.5: "config-version" is 5. *
PlayerStats 1.6 and up: "config-version" is 6. */ - private void checkConfigVersion() { + private void checkAndUpdateConfigVersion() { if (!config.contains("config-version") || config.getInt("config-version") != configVersion) { - new ConfigUpdateHandler(plugin, configFile, configVersion); - reloadConfig(); - } - } + DefaultValueGetter defaultValueGetter = new DefaultValueGetter(config); + Map defaultValues = defaultValueGetter.getValuesToAdjust(); + defaultValues.put("config-version", configVersion); - /** - * 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(); - configFile = new File(plugin.getDataFolder(), "config.yml"); - } + super.addValues(defaultValues); + reload(); - /** - * Reloads the config from file, or creates a new file with default values - * if there is none. Also reads the value for debug-level and passes it - * on to {@link MyLogger}. - * - * @return true if the config has been reloaded from disk, false if it failed - */ - public boolean reloadConfig() { - if (!configFile.exists()) { - saveDefaultConfig(); - } - try { - config = YamlConfiguration.loadConfiguration(configFile); - return true; - } - catch (IllegalArgumentException e) { - MyLogger.logException(e, "ConfigHandler", "reloadConfig"); - return false; + MyLogger.logLowLevelMsg("Your config has been updated to version " + configVersion + + ", but all of your custom settings should still be there!"); } } @@ -141,6 +132,14 @@ public int getLastPlayedLimit() { return config.getInt("number-of-days-since-last-joined", 0); } + /** + * Whether to allow the /stat player command for excluded players. + * @return the config setting (default: true) + */ + public boolean allowPlayerLookupsForExcludedPlayers() { + return config.getBoolean("allow-player-lookups-for-excluded-players", true); + } + /** * Whether to use TranslatableComponents wherever possible. * diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/config/DefaultValueGetter.java b/src/main/java/com/artemis/the/gr8/playerstats/core/config/DefaultValueGetter.java new file mode 100644 index 0000000..19b56d8 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/config/DefaultValueGetter.java @@ -0,0 +1,56 @@ +package com.artemis.the.gr8.playerstats.core.config; + +import org.bukkit.configuration.file.FileConfiguration; + +import java.util.HashMap; +import java.util.Map; + +public final class DefaultValueGetter { + + private final FileConfiguration config; + private final Map defaultValuesToAdjust; + + public DefaultValueGetter(FileConfiguration configuration) { + config = configuration; + defaultValuesToAdjust = new HashMap<>(); + } + + public Map getValuesToAdjust() { + checkTopListDefault(); + checkDefaultColors(); + return defaultValuesToAdjust; + } + + private void checkTopListDefault() { + String oldTitle = config.getString("top-list-title"); + if (oldTitle != null && oldTitle.equalsIgnoreCase("Top [x]")) { + defaultValuesToAdjust.put("top-list-title", "Top"); + } + } + + /** + * Adjusts some of the default colors to migrate from versions 2 + * or 3 to version 4 and above. + */ + private void checkDefaultColors() { + addValueIfNeeded("top-list.title", "yellow", "#FFD52B"); + addValueIfNeeded("top-list.title", "#FFEA40", "#FFD52B"); + addValueIfNeeded("top-list.stat-names", "yellow", "#FFD52B"); + addValueIfNeeded("top-list.stat-names", "#FFEA40", "#FFD52B"); + addValueIfNeeded("top-list.sub-stat-names", "#FFD52B", "yellow"); + + addValueIfNeeded("individual-statistics.stat-names", "yellow", "#FFD52B"); + addValueIfNeeded("individual-statistics.sub-stat-names", "#FFD52B", "yellow"); + addValueIfNeeded("total-server.title", "gold", "#55AAFF"); + addValueIfNeeded("total-server.server-name", "gold", "#55AAFF"); + addValueIfNeeded("total-server.stat-names", "yellow", "#FFD52B"); + addValueIfNeeded("total-server.sub-stat-names", "#FFD52B", "yellow"); + } + + private void addValueIfNeeded(String path, String oldValue, String newValue) { + String configString = config.getString(path); + if (configString != null && configString.equalsIgnoreCase(oldValue)) { + defaultValuesToAdjust.put(path, newValue); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/enums/DebugLevel.java b/src/main/java/com/artemis/the/gr8/playerstats/core/enums/DebugLevel.java similarity index 85% rename from src/main/java/com/artemis/the/gr8/playerstats/enums/DebugLevel.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/enums/DebugLevel.java index 34e76ce..55db286 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/enums/DebugLevel.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/enums/DebugLevel.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.enums; +package com.artemis.the.gr8.playerstats.core.enums; /** * Represents the debugging level that PlayerStats can use. diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/enums/PluginColor.java b/src/main/java/com/artemis/the/gr8/playerstats/core/enums/PluginColor.java new file mode 100644 index 0000000..5087d07 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/enums/PluginColor.java @@ -0,0 +1,80 @@ +package com.artemis.the.gr8.playerstats.core.enums; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; + +/** + * This enum represents the colorscheme PlayerStats uses in its output messages. + * The first set of colors is used throughout the plugin, while the set of NAME-colors + * represents the colors that player-names can be in the "shared by player-name" + * section of shared statistics + */ +public enum PluginColor { + /** + * ChatColor Gray (#AAAAAA) + */ + GRAY (NamedTextColor.GRAY), + + /** + * A Dark Purple that is mainly used for title-underscores (#6E3485). + */ + DARK_PURPLE (TextColor.fromHexString("#6E3485")), + + /** + * A Light Purple that is meant to simulate the color of a clicked link. + * Used for the "Hover Here" part of shared statistics (#845EC2) + * */ + LIGHT_PURPLE (TextColor.fromHexString("#845EC2")), + + /** + * A Light Blue that is used for the share-button and feedback message accents (#55C6FF). + */ + LIGHT_BLUE (TextColor.fromHexString("#55C6FF")), + + /** + * A very light blue that is used for feedback messages and hover-text (#ADE7FF) + */ + LIGHTEST_BLUE(TextColor.fromHexString("#ADE7FF")), + + /** + * ChatColor Gold (#FFAA00) + */ + GOLD (NamedTextColor.GOLD), + + /** + * A Medium Gold that is used for the example message and for hover-text accents (#FFD52B). + */ + MEDIUM_GOLD (TextColor.fromHexString("#FFD52B")), + + /** + * A Light Gold that is used for the example message and for hover-text accents (#FFEA40). + */ + LIGHT_GOLD (TextColor.fromHexString("#FFEA40")), + + /** + * The color of vanilla Minecraft hearts (#FF1313). + */ + RED (TextColor.fromHexString("#FF1313")); + + + private final TextColor color; + + PluginColor(TextColor color) { + this.color = color; + } + + /** + * Returns the TextColor value belonging to the corresponding enum constant. + */ + public TextColor getColor() { + return color; + } + + /** + * Gets the nearest NamedTextColor for the corresponding enum constant. + */ + public @NotNull TextColor getConsoleColor() { + return NamedTextColor.nearestTo(color); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/enums/StandardMessage.java b/src/main/java/com/artemis/the/gr8/playerstats/core/enums/StandardMessage.java similarity index 69% rename from src/main/java/com/artemis/the/gr8/playerstats/enums/StandardMessage.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/enums/StandardMessage.java index a7cf69b..011956f 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/enums/StandardMessage.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/enums/StandardMessage.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.enums; +package com.artemis.the.gr8.playerstats.core.enums; /** * All standard messages PlayerStats can send as feedback. @@ -8,11 +8,16 @@ public enum StandardMessage { RELOADED_CONFIG, STILL_RELOADING, + EXCLUDE_FAILED, + INCLUDE_FAILED, MISSING_STAT_NAME, MISSING_PLAYER_NAME, + PLAYER_IS_EXCLUDED, + WAIT_A_MOMENT, + WAIT_A_MINUTE, REQUEST_ALREADY_RUNNING, STILL_ON_SHARE_COOLDOWN, RESULTS_ALREADY_SHARED, STAT_RESULTS_TOO_OLD, - UNKNOWN_ERROR, -} + UNKNOWN_ERROR +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/listeners/JoinListener.java b/src/main/java/com/artemis/the/gr8/playerstats/core/listeners/JoinListener.java similarity index 83% rename from src/main/java/com/artemis/the/gr8/playerstats/listeners/JoinListener.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/listeners/JoinListener.java index d45a654..8719b33 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/listeners/JoinListener.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/listeners/JoinListener.java @@ -1,6 +1,6 @@ -package com.artemis.the.gr8.playerstats.listeners; +package com.artemis.the.gr8.playerstats.core.listeners; -import com.artemis.the.gr8.playerstats.ThreadManager; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.player.PlayerJoinEvent; diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/MessageBuilder.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/MessageBuilder.java similarity index 68% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/MessageBuilder.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/MessageBuilder.java index 3f73883..1a6bbff 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/MessageBuilder.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/MessageBuilder.java @@ -1,25 +1,21 @@ -package com.artemis.the.gr8.playerstats.msg; - -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.api.ApiFormatter; -import com.artemis.the.gr8.playerstats.msg.components.ComponentFactory; -import com.artemis.the.gr8.playerstats.msg.components.ExampleMessage; -import com.artemis.the.gr8.playerstats.msg.components.HelpMessage; -import com.artemis.the.gr8.playerstats.msg.components.BukkitConsoleComponentFactory; -import com.artemis.the.gr8.playerstats.msg.components.PrideComponentFactory; -import com.artemis.the.gr8.playerstats.msg.msgutils.*; -import com.artemis.the.gr8.playerstats.utils.EnumHandler; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import com.artemis.the.gr8.playerstats.enums.Target; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.enums.Unit; - -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; +package com.artemis.the.gr8.playerstats.core.msg; + +import com.artemis.the.gr8.playerstats.api.StatTextFormatter; +import com.artemis.the.gr8.playerstats.core.msg.components.*; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.*; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.core.utils.EnumHandler; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.api.enums.Target; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.api.enums.Unit; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import org.bukkit.Statistic; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,47 +34,43 @@ * @see PrideComponentFactory * @see BukkitConsoleComponentFactory */ -public final class MessageBuilder implements ApiFormatter { +public final class MessageBuilder implements StatTextFormatter { - private static ConfigHandler config; - private boolean useHoverText; - private boolean isConsoleBuilder; + private final ConfigHandler config; + private final boolean useHoverText; private final ComponentFactory componentFactory; private final LanguageKeyHandler languageKeyHandler; private final NumberFormatter formatter; + private final ComponentSerializer serializer; - private MessageBuilder(ConfigHandler config) { - this (config, new ComponentFactory(config)); - } - - private MessageBuilder(ConfigHandler configHandler, ComponentFactory factory) { - config = configHandler; - useHoverText = config.useHoverText(); + private MessageBuilder(ComponentFactory factory) { + config = ConfigHandler.getInstance(); + languageKeyHandler = LanguageKeyHandler.getInstance(); componentFactory = factory; + if (componentFactory.isConsoleFactory()) { + useHoverText = false; + } else { + useHoverText = config.useHoverText(); + } formatter = new NumberFormatter(); - languageKeyHandler = Main.getLanguageKeyHandler(); + serializer = new ComponentSerializer(); } - public static MessageBuilder defaultBuilder(ConfigHandler config) { - return new MessageBuilder(config); + @Contract(" -> new") + public static @NotNull MessageBuilder defaultBuilder() { + return new MessageBuilder(new ComponentFactory()); } - public static MessageBuilder fromComponentFactory(ConfigHandler config, ComponentFactory factory) { - return new MessageBuilder(config, factory); + @Contract("_ -> new") + public static @NotNull MessageBuilder fromComponentFactory(ComponentFactory factory) { + return new MessageBuilder(factory); } - /** - * Set whether this {@link MessageBuilder} should use hoverText. - * By default, this follows the setting specified in the {@link ConfigHandler}. - */ - public void toggleHoverUse(boolean desiredSetting) { - useHoverText = desiredSetting; - } - - public void setConsoleBuilder(boolean isConsoleBuilder) { - this.isConsoleBuilder = isConsoleBuilder; + @Override + public @NotNull String textComponentToString(TextComponent component) { + return serializer.getTranslatableComponentSerializer().serialize(component); } @Override @@ -87,9 +79,9 @@ public TextComponent getPluginPrefix() { } @Override - public TextComponent getRainbowPluginPrefix() { - PrideComponentFactory pride = new PrideComponentFactory(config); - return pride.rainbowPrefix(); + public @NotNull TextComponent getRainbowPluginPrefix() { + PrideComponentFactory pride = new PrideComponentFactory(); + return pride.pluginPrefix(); } @Override @@ -98,70 +90,81 @@ public TextComponent getPluginPrefixAsTitle() { } @Override - public TextComponent getRainbowPluginPrefixAsTitle() { - PrideComponentFactory pride = new PrideComponentFactory(config); + public @NotNull TextComponent getRainbowPluginPrefixAsTitle() { + PrideComponentFactory pride = new PrideComponentFactory(); return pride.pluginPrefixAsTitle(); } - public TextComponent reloadedConfig() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content("Config reloaded!")); + public @NotNull TextComponent reloadedConfig() { + return composePluginMessage("Config reloaded!"); } - public TextComponent stillReloading() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content( - "The plugin is (re)loading, your request will be processed when it is done!")); + public @NotNull TextComponent stillReloading() { + return composePluginMessage("The plugin is (re)loading, your request will be processed when it is done!"); } - public TextComponent waitAMoment(boolean longWait) { - String msg = longWait ? "Calculating statistics, this may take a minute..." : - "Calculating statistics, this may take a few moments..."; + public @NotNull TextComponent excludeSuccess(String playerName) { return componentFactory.pluginPrefix() .append(space()) - .append(componentFactory.message().content(msg)); + .append(componentFactory.message().content("Excluded ") + .append(componentFactory.messageAccent().content(playerName)) + .append(text("!"))); } - public TextComponent missingStatName() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content( - "Please provide a valid statistic name!")); + public @NotNull TextComponent excludeFailed() { + return composePluginMessage("This player is already hidden from /stat results!"); } - public TextComponent missingSubStatName(Statistic.Type statType) { + public @NotNull TextComponent includeSuccess(String playerName) { return componentFactory.pluginPrefix() .append(space()) - .append(componentFactory.message().content( - "Please add a valid " + EnumHandler.getSubStatTypeName(statType) + " to look up this statistic!")); + .append(componentFactory.message().content("Removed ") + .append(componentFactory.messageAccent().content(playerName)) + .append(text(" from the exclude-list!"))); } - public TextComponent missingPlayerName() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content( - "Please specify a valid player-name!")); + public @NotNull TextComponent includeFailed() { + return composePluginMessage("This is not a player that has been excluded with the /statexclude command!"); + } + + public @NotNull TextComponent waitAMinute() { + return composePluginMessage("Calculating statistics, this may take a minute..."); + } + + public @NotNull TextComponent waitAMoment() { + return composePluginMessage("Calculating statistics, this may take a few moments..."); + } + + public @NotNull TextComponent missingStatName() { + return composePluginMessage("Please provide a valid statistic name!"); + } + + public @NotNull TextComponent missingSubStatName(String statType) { + return composePluginMessage("Please add a valid " + statType + " to look up this statistic!"); } - public TextComponent wrongSubStatType(Statistic.Type statType, String subStatName) { + public @NotNull TextComponent missingPlayerName() { + return composePluginMessage("Please specify a valid player-name!"); + } + + public @NotNull TextComponent playerIsExcluded() { + return composePluginMessage("This player is excluded from /stat results!"); + } + + public @NotNull TextComponent wrongSubStatType(String statType, String subStatName) { return componentFactory.pluginPrefix() .append(space()) .append(componentFactory.messageAccent().content("\"" + subStatName + "\"")) .append(space()) .append(componentFactory.message().content( - "is not a valid " + EnumHandler.getSubStatTypeName(statType) + "!")); + "is not a valid " + statType + "!")); } - public TextComponent requestAlreadyRunning() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content( - "Please wait for your previous lookup to finish!")); + public @NotNull TextComponent requestAlreadyRunning() { + return composePluginMessage("Please wait for your previous lookup to finish!"); } - public TextComponent stillOnShareCoolDown() { + public @NotNull TextComponent stillOnShareCoolDown() { int waitTime = config.getStatShareWaitingTime(); String minutes = waitTime == 1 ? " minute" : " minutes"; @@ -175,68 +178,88 @@ public TextComponent stillOnShareCoolDown() { .append(text("between sharing!"))); } - public TextComponent resultsAlreadyShared() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content("You already shared these results!")); + public @NotNull TextComponent resultsAlreadyShared() { + return composePluginMessage("You already shared these results!"); } - public TextComponent statResultsTooOld() { - return componentFactory.pluginPrefix() - .append(space()) - .append(componentFactory.message().content( - "It has been too long since you looked up this statistic, please repeat the original command!")); + public @NotNull TextComponent statResultsTooOld() { + return composePluginMessage("It has been too long since you looked up " + + "this statistic, please repeat the original command!"); } - public TextComponent unknownError() { - return componentFactory.pluginPrefix() + public @NotNull TextComponent unknownError() { + return composePluginMessage("Something went wrong with your request, " + + "please try again or see /statistic for a usage explanation!"); + } + + private @NotNull TextComponent composePluginMessage(String content) { + return getPluginPrefix() .append(space()) - .append(componentFactory.message().content( - "Something went wrong with your request, " + - "please try again or see /statistic for a usage explanation!")); + .append(componentFactory.message().content(content)); } - public TextComponent usageExamples() { + @Contract(" -> new") + public @NotNull TextComponent usageExamples() { return ExampleMessage.construct(componentFactory); } public TextComponent helpMsg() { int listSize = config.getTopListMaxSize(); - if (!isConsoleBuilder && useHoverText) { + if (useHoverText) { return HelpMessage.constructHoverMsg(componentFactory, listSize); } else { return HelpMessage.constructPlainMsg(componentFactory, listSize); } } + public @NotNull TextComponent excludeInfoMsg() { + return ExcludeInfoMessage.construct(componentFactory); + } + + public @NotNull TextComponent excludedList(@NotNull ArrayList excludedPlayerNames) { + TextComponent.Builder excludedList = text() + .append(newline()) + .append(getPluginPrefixAsTitle() + .append(newline()) + .append(componentFactory.subTitle("All players that are currently excluded: "))); + + excludedPlayerNames.forEach(playerName -> excludedList + .append(newline()) + .append(componentFactory.arrow() + .append(space()) + .append(componentFactory.infoMessageAccent().content(playerName)))); + + return excludedList.build(); + } + @Override - public TextComponent getStatTitle(Statistic statistic, @Nullable String subStatName) { + public @NotNull TextComponent getStatTitle(Statistic statistic, @Nullable String subStatName) { return getTopStatTitleComponent(0, statistic, subStatName, null); } @Override - public TextComponent getStatTitle(Statistic statistic, Unit unit) { + public @NotNull TextComponent getStatTitle(Statistic statistic, Unit unit) { return getTopStatTitleComponent(0, statistic, null, unit); } @Override - public TextComponent getTopStatTitle(int topListSize, Statistic statistic, @Nullable String subStatName) { + public @NotNull TextComponent getTopStatTitle(int topListSize, Statistic statistic, @Nullable String subStatName) { return getTopStatTitleComponent(topListSize, statistic, subStatName, null); } @Override - public TextComponent getTopStatTitle(int topStatSize, Statistic statistic, Unit unit) { + public @NotNull TextComponent getTopStatTitle(int topStatSize, Statistic statistic, Unit unit) { return getTopStatTitleComponent(topStatSize, statistic, null, unit); } @Override - public TextComponent formatTopStatLine(int positionInTopList, String playerName, long statNumber, Statistic statistic) { + public @NotNull TextComponent formatTopStatLine(int positionInTopList, String playerName, long statNumber, Statistic statistic) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.TOP, statistic); return getTopStatLineComponent(positionInTopList, playerName, statNumberComponent); } @Override - public TextComponent formatTopStatLine(int positionInTopList, String playerName, long statNumber, Unit unit) { + public @NotNull TextComponent formatTopStatLine(int positionInTopList, String playerName, long statNumber, Unit unit) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.TOP, unit); return getTopStatLineComponent(positionInTopList, playerName, statNumberComponent); } @@ -245,55 +268,55 @@ public TextComponent formatTopStatLine(int positionInTopList, String playerName, * Time-number does not hover */ @Override - public TextComponent formatTopStatLineForTypeTime(int positionInTopList, String playerName, long statNumber, Unit bigUnit, Unit smallUnit) { + public @NotNull TextComponent formatTopStatLineForTypeTime(int positionInTopList, String playerName, long statNumber, Unit bigUnit, Unit smallUnit) { TextComponent statNumberComponent = getBasicTimeNumberComponent(statNumber, Target.TOP, bigUnit, smallUnit); return getTopStatLineComponent(positionInTopList, playerName, statNumberComponent); } @Override - public TextComponent formatServerStat(long statNumber, Statistic statistic) { + public @NotNull TextComponent formatServerStat(long statNumber, Statistic statistic) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.SERVER, statistic); return getServerStatComponent(statNumberComponent, statistic, null, null); } @Override - public TextComponent formatServerStat(long statNumber, Statistic statistic, String subStatName) { + public @NotNull TextComponent formatServerStat(long statNumber, Statistic statistic, String subStatName) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.SERVER, statistic); return getServerStatComponent(statNumberComponent, statistic, subStatName, null); } @Override - public TextComponent formatServerStat(long statNumber, Statistic statistic, Unit unit) { + public @NotNull TextComponent formatServerStat(long statNumber, Statistic statistic, Unit unit) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.SERVER, unit); return getServerStatComponent(statNumberComponent, statistic, null, unit); } @Override - public TextComponent formatServerStatForTypeTime(long statNumber, Statistic statistic, Unit bigUnit, Unit smallUnit) { + public @NotNull TextComponent formatServerStatForTypeTime(long statNumber, Statistic statistic, Unit bigUnit, Unit smallUnit) { TextComponent statNumberComponent = getBasicTimeNumberComponent(statNumber, Target.SERVER, bigUnit, smallUnit); return getServerStatComponent(statNumberComponent, statistic, null, null); } @Override - public TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic) { + public @NotNull TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.PLAYER, statistic); return getPlayerStatComponent(playerName, statNumberComponent, statistic, null, null); } @Override - public TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic, Unit unit) { + public @NotNull TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic, Unit unit) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.PLAYER, unit); return getPlayerStatComponent(playerName, statNumberComponent, statistic, null, unit); } @Override - public TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic, String subStatName) { + public @NotNull TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic, String subStatName) { TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.PLAYER, statistic); return getPlayerStatComponent(playerName, statNumberComponent, statistic, subStatName, null); } @Override - public TextComponent formatPlayerStatForTypeTime(String playerName, int statNumber, Statistic statistic, Unit bigUnit, Unit smallUnit) { + public @NotNull TextComponent formatPlayerStatForTypeTime(String playerName, int statNumber, Statistic statistic, Unit bigUnit, Unit smallUnit) { TextComponent statNumberComponent = getBasicTimeNumberComponent(statNumber, Target.PLAYER, bigUnit, smallUnit); return getPlayerStatComponent(playerName, statNumberComponent, statistic, null, null); } @@ -309,7 +332,7 @@ public TextComponent formatPlayerStatForTypeTime(String playerName, int statNumb *
- If both parameters are null, the formattedComponent will be returned * as is. */ - public BiFunction formattedPlayerStatFunction(int stat, @NotNull RequestSettings request) { + public @NotNull FormattingFunction formattedPlayerStatFunction(int stat, @NotNull StatRequest.Settings request) { TextComponent playerStat = formatPlayerStat(request.getPlayerName(), stat, request.getStatistic(), request.getSubStatEntryName()); return getFormattingFunction(playerStat, Target.PLAYER); } @@ -325,7 +348,7 @@ public BiFunction formattedPlayerStatFunc *
- If both parameters are null, the formattedComponent will be returned * as is. */ - public BiFunction formattedServerStatFunction(long stat, @NotNull RequestSettings request) { + public @NotNull FormattingFunction formattedServerStatFunction(long stat, @NotNull StatRequest.Settings request) { TextComponent serverStat = formatServerStat(stat, request.getStatistic(), request.getSubStatEntryName()); return getFormattingFunction(serverStat, Target.SERVER); } @@ -341,13 +364,13 @@ public BiFunction formattedServerStatFunc *
- If both parameters are null, the formattedComponent will be returned * as is. */ - public BiFunction formattedTopStatFunction(@NotNull LinkedHashMap topStats, @NotNull RequestSettings request) { + public @NotNull FormattingFunction formattedTopStatFunction(@NotNull LinkedHashMap topStats, @NotNull StatRequest.Settings request) { final TextComponent title = getTopStatTitle(topStats.size(), request.getStatistic(), request.getSubStatEntryName()); final TextComponent list = getTopStatListComponent(topStats, request.getStatistic()); final boolean useEnters = config.useEnters(Target.TOP, false); final boolean useEntersForShared = config.useEnters(Target.TOP, true); - return (shareCode, sender) -> { + BiFunction biFunction = (shareCode, sender) -> { TextComponent.Builder topBuilder = text(); //if we're adding a share-button @@ -391,9 +414,10 @@ else if (sender != null) { } return topBuilder.build(); }; + return new FormattingFunction(biFunction); } - private TextComponent getPlayerStatComponent(String playerName, TextComponent statNumberComponent, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) { + private @NotNull TextComponent getPlayerStatComponent(String playerName, TextComponent statNumberComponent, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) { TextComponent statUnit = (unit == null) ? getStatUnitComponent(statistic, Target.PLAYER) : getStatUnitComponent(unit, Target.PLAYER); @@ -409,7 +433,7 @@ private TextComponent getPlayerStatComponent(String playerName, TextComponent st .build(); } - private TextComponent getServerStatComponent(TextComponent statNumber, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) { + private @NotNull TextComponent getServerStatComponent(TextComponent statNumber, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) { String serverTitle = config.getServerTitle(); String serverName = config.getServerName(); TextComponent statUnit = (unit == null) ? @@ -428,7 +452,7 @@ private TextComponent getServerStatComponent(TextComponent statNumber, Statistic .build(); } - private TextComponent getTopStatTitleComponent(int topListSize, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) { + private @NotNull TextComponent getTopStatTitleComponent(int topListSize, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) { TextComponent statUnit = (unit == null) ? getStatUnitComponent(statistic, Target.TOP) : getStatUnitComponent(unit, Target.TOP); @@ -450,7 +474,7 @@ private TextComponent getTopStatTitleComponent(int topListSize, Statistic statis } } - private TextComponent getTopStatListComponent(LinkedHashMap topStats, Statistic statistic) { + private @NotNull TextComponent getTopStatListComponent(@NotNull LinkedHashMap topStats, Statistic statistic) { TextComponent.Builder topList = Component.text(); Set playerNames = topStats.keySet(); boolean useDots = config.useDots(); @@ -472,7 +496,7 @@ private TextComponent getTopStatListComponent(LinkedHashMap top return topList.build(); } - private TextComponent getTopStatLineComponent(int positionInTopList, String playerName, TextComponent statNumberComponent) { + private @NotNull TextComponent getTopStatLineComponent(int positionInTopList, String playerName, TextComponent statNumberComponent) { boolean useDots = config.useDots(); String fullPlayerName = useDots ? playerName : playerName + ":"; @@ -498,26 +522,29 @@ private TextComponent getTopStatLineComponent(int positionInTopList, String play } private TextComponent getStatAndSubStatNameComponent(Statistic statistic, @Nullable String subStatName, Target target) { + EnumHandler enumHandler = EnumHandler.getInstance(); + + String statKey = languageKeyHandler.getStatKey(statistic); + String subStatKey = switch (statistic.getType()) { + case UNTYPED -> null; + case ENTITY -> languageKeyHandler.getEntityKey(enumHandler.getEntityEnum(subStatName)); + case BLOCK -> languageKeyHandler.getBlockKey(enumHandler.getBlockEnum(subStatName)); + case ITEM -> languageKeyHandler.getItemKey(enumHandler.getItemEnum(subStatName)); + }; + if (subStatKey == null) { + subStatKey = StringUtils.prettify(subStatName); + } + if (config.useTranslatableComponents()) { - String statKey = languageKeyHandler.getStatKey(statistic); - String subStatKey = switch (statistic.getType()) { - case UNTYPED -> null; - case ENTITY -> languageKeyHandler.getEntityKey(EnumHandler.getEntityEnum(subStatName)); - case BLOCK -> languageKeyHandler.getBlockKey(EnumHandler.getBlockEnum(subStatName)); - case ITEM -> languageKeyHandler.getItemKey(EnumHandler.getItemEnum(subStatName)); - }; - if (subStatKey == null) { - subStatKey = StringUtils.prettify(subStatName); - } return componentFactory.statAndSubStatNameTranslatable(statKey, subStatKey, target); } - String prettyStatName = StringUtils.prettify(statistic.toString()); - String prettySubStatName = StringUtils.prettify(subStatName); + String prettyStatName = languageKeyHandler.convertLanguageKeyToDisplayName(statKey); + String prettySubStatName = languageKeyHandler.convertLanguageKeyToDisplayName(subStatKey); return componentFactory.statAndSubStatName(prettyStatName, prettySubStatName, target); } - private TextComponent getStatNumberComponent(long statNumber, Target target, Unit unit) { + private TextComponent getStatNumberComponent(long statNumber, Target target, @NotNull Unit unit) { return switch (unit.getType()) { case TIME -> getBasicTimeNumberComponent(statNumber, target, unit, null); case DAMAGE -> getDamageNumberComponent(statNumber, target, unit); @@ -581,7 +608,7 @@ private TextComponent getTimeNumberComponent(long statNumber, Target target) { ArrayList unitRange = getTimeUnitRange(statNumber); if (unitRange.size() <= 1 || (useHoverText && unitRange.size() <= 3)) { MyLogger.logWarning("There is something wrong with the time-units you specified, please check your config!"); - return componentFactory.timeNumber(formatter.formatNumber(statNumber), target); + return componentFactory.timeNumber(formatter.formatDefaultNumber(statNumber), target); } else { String mainNumber = formatter.formatTimeNumber(statNumber, unitRange.get(0), unitRange.get(1)); @@ -603,7 +630,7 @@ private TextComponent getBasicTimeNumberComponent(long statNumber, Target target } private TextComponent getDefaultNumberComponent(long statNumber, Target target) { - return componentFactory.statNumber(formatter.formatNumber(statNumber), target); + return componentFactory.statNumber(formatter.formatDefaultNumber(statNumber), target); } /** @@ -618,7 +645,7 @@ private TextComponent getStatUnitComponent(Statistic statistic, Target target) { return getStatUnitComponent(unit, target); } - private TextComponent getStatUnitComponent(Unit unit, Target target) { + private TextComponent getStatUnitComponent(@NotNull Unit unit, Target target) { return switch (unit.getType()) { case DAMAGE -> getDamageUnitComponent(unit, target); case DISTANCE -> getDistanceUnitComponent(unit, target); @@ -629,7 +656,7 @@ private TextComponent getStatUnitComponent(Unit unit, Target target) { /** * Provides its own space in front of it! */ - private TextComponent getDistanceUnitComponent(Unit unit, Target target) { + private @NotNull TextComponent getDistanceUnitComponent(Unit unit, Target target) { if (config.useTranslatableComponents()) { String unitKey = languageKeyHandler.getUnitKey(unit); if (unitKey != null) { @@ -644,18 +671,12 @@ private TextComponent getDistanceUnitComponent(Unit unit, Target target) { /** * Provides its own space in front of it! */ - private TextComponent getDamageUnitComponent(Unit unit, Target target) { + private @NotNull TextComponent getDamageUnitComponent(Unit unit, Target target) { if (unit == Unit.HEART) { - TextComponent heartUnit; - if (isConsoleBuilder) { - heartUnit = componentFactory.consoleHeart(); - } else if (useHoverText) { - heartUnit = componentFactory.clientHeartWithHoverText(); - } else { - heartUnit = componentFactory.clientHeart(false); - } - return Component.space() - .append(heartUnit); + TextComponent heartUnit = useHoverText ? + componentFactory.heartBetweenBracketsWithHoverText() : + componentFactory.heartBetweenBrackets(); + return Component.space().append(heartUnit); } return Component.space() .append(componentFactory.statUnit(unit.getLabel(), target)); @@ -671,11 +692,11 @@ private Component getSharerNameComponent(CommandSender sender) { return componentFactory.sharerName(sender.getName()); } - private BiFunction getFormattingFunction(@NotNull TextComponent statResult, Target target) { + private @NotNull FormattingFunction getFormattingFunction(@NotNull TextComponent statResult, Target target) { boolean useEnters = config.useEnters(target, false); boolean useEntersForShared = config.useEnters(target, true); - return (shareCode, sender) -> { + BiFunction biFunction = (shareCode, sender) -> { TextComponent.Builder statBuilder = text(); //if we're adding a share-button @@ -706,10 +727,11 @@ else if (sender != null) { } return statBuilder.build(); }; + return new FormattingFunction(biFunction); } private int getNumberOfDotsToAlign(String displayText) { - if (isConsoleBuilder) { + if (componentFactory.isConsoleFactory()) { return FontUtils.getNumberOfDotsToAlignForConsole(displayText); } else if (config.playerNameIsBold()) { return FontUtils.getNumberOfDotsToAlignForBoldText(displayText); @@ -725,7 +747,7 @@ private int getNumberOfDotsToAlign(String displayText) { *

2. maxHoverUnit

*

3. minHoverUnit

*/ - private ArrayList getTimeUnitRange(long statNumber) { + private @NotNull ArrayList getTimeUnitRange(long statNumber) { ArrayList unitRange = new ArrayList<>(); if (!config.autoDetectTimeUnit(false)) { unitRange.add(Unit.fromString(config.getTimeUnit(false))); diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/OutputManager.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/OutputManager.java new file mode 100644 index 0000000..cbac752 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/OutputManager.java @@ -0,0 +1,220 @@ +package com.artemis.the.gr8.playerstats.core.msg; + +import com.artemis.the.gr8.playerstats.api.StatTextFormatter; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import com.artemis.the.gr8.playerstats.core.msg.components.*; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.FormattingFunction; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.TextComponent; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.LinkedHashMap; +import java.util.function.Function; + +import static com.artemis.the.gr8.playerstats.core.enums.StandardMessage.*; + +/** + * This class manages all PlayerStats output. It is the only + * place where messages are sent. It gets its messages from a + * {@link MessageBuilder} configured for either a Console or + * for Players (mainly to deal with the lack of hover-text, + * and for Bukkit consoles to make up for the lack of hex-colors). + */ +public final class OutputManager { + + private static BukkitAudiences adventure; + private static EnumMap> standardMessages; + + private final ConfigHandler config; + private MessageBuilder messageBuilder; + private MessageBuilder consoleMessageBuilder; + + public OutputManager(BukkitAudiences adventure) { + OutputManager.adventure = adventure; + config = ConfigHandler.getInstance(); + + getMessageBuilders(); + prepareFunctions(); + } + + public void updateSettings() { + getMessageBuilders(); + } + + public StatTextFormatter getMainMessageBuilder() { + return messageBuilder; + } + + public @NotNull String textComponentToString(TextComponent component) { + return messageBuilder.textComponentToString(component); + } + + /** + * @return a TextComponent with the following parts: + *
[player-name]: [number] [stat-name] {sub-stat-name} + */ + public @NotNull FormattingFunction formatPlayerStat(@NotNull StatRequest.Settings requestSettings, int playerStat) { + return getMessageBuilder(requestSettings.getCommandSender()) + .formattedPlayerStatFunction(playerStat, requestSettings); + } + + /** + * @return a TextComponent with the following parts: + *
[Total on] [server-name]: [number] [stat-name] [sub-stat-name] + */ + public @NotNull FormattingFunction formatServerStat(@NotNull StatRequest.Settings requestSettings, long serverStat) { + return getMessageBuilder(requestSettings.getCommandSender()) + .formattedServerStatFunction(serverStat, requestSettings); + } + + /** + * @return a TextComponent with the following parts: + *
[PlayerStats] [Top 10] [stat-name] [sub-stat-name] + *
[1.] [player-name] [number] + *
[2.] [player-name] [number] + *
[3.] etc... + */ + public @NotNull FormattingFunction formatTopStats(@NotNull StatRequest.Settings requestSettings, @NotNull LinkedHashMap topStats) { + return getMessageBuilder(requestSettings.getCommandSender()) + .formattedTopStatFunction(topStats, requestSettings); + } + + public void sendFeedbackMsg(@NotNull CommandSender sender, StandardMessage message) { + if (message != null) { + adventure.sender(sender).sendMessage(standardMessages.get(message) + .apply(getMessageBuilder(sender))); + } + } + + public void sendFeedbackMsgPlayerExcluded(@NotNull CommandSender sender, String playerName) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .excludeSuccess(playerName)); + } + + public void sendFeedbackMsgPlayerIncluded(@NotNull CommandSender sender, String playerName) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .includeSuccess(playerName)); + } + + public void sendFeedbackMsgMissingSubStat(@NotNull CommandSender sender, String statType) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .missingSubStatName(statType)); + } + + public void sendFeedbackMsgWrongSubStat(@NotNull CommandSender sender, String statType, @Nullable String subStatName) { + if (subStatName == null) { + sendFeedbackMsgMissingSubStat(sender, statType); + } else { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .wrongSubStatType(statType, subStatName)); + } + } + + public void sendExamples(@NotNull CommandSender sender) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .usageExamples()); + } + + public void sendHelp(@NotNull CommandSender sender) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .helpMsg()); + } + + public void sendExcludeInfo(@NotNull CommandSender sender) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .excludeInfoMsg()); + } + + public void sendExcludedList(@NotNull CommandSender sender, ArrayList excludedPlayerNames) { + adventure.sender(sender).sendMessage(getMessageBuilder(sender) + .excludedList(excludedPlayerNames)); + } + + public void sendToAllPlayers(@NotNull TextComponent component) { + adventure.players().sendMessage(component); + } + + public void sendToCommandSender(@NotNull CommandSender sender, @NotNull TextComponent component) { + adventure.sender(sender).sendMessage(component); + } + + private MessageBuilder getMessageBuilder(CommandSender sender) { + return sender instanceof ConsoleCommandSender ? consoleMessageBuilder : messageBuilder; + } + + private void getMessageBuilders() { + messageBuilder = getClientMessageBuilder(); + consoleMessageBuilder = getConsoleMessageBuilder(); + } + + private MessageBuilder getClientMessageBuilder() { + ComponentFactory festiveFactory = getFestiveFactory(); + if (festiveFactory == null) { + return MessageBuilder.defaultBuilder(); + } + return MessageBuilder.fromComponentFactory(festiveFactory); + } + + private @NotNull MessageBuilder getConsoleMessageBuilder() { + MessageBuilder consoleBuilder; + if (isBukkit()) { + consoleBuilder = MessageBuilder.fromComponentFactory(new BukkitConsoleComponentFactory()); + } else { + consoleBuilder = MessageBuilder.fromComponentFactory(new ConsoleComponentFactory()); + } + return consoleBuilder; + } + + private @Nullable ComponentFactory getFestiveFactory() { + if (config.useRainbowMode()) { + return new PrideComponentFactory(); + } + else if (config.useFestiveFormatting()) { + return switch (LocalDate.now().getMonth()) { + case JUNE -> new PrideComponentFactory(); + case OCTOBER -> new HalloweenComponentFactory(); + case SEPTEMBER -> { + if (LocalDate.now().getDayOfMonth() == 12) { + yield new BirthdayComponentFactory(); + } + yield null; + } + case DECEMBER -> new WinterComponentFactory(); + default -> null; + }; + } + return null; + } + + private boolean isBukkit() { + return Bukkit.getName().equalsIgnoreCase("CraftBukkit"); + } + + private void prepareFunctions() { + standardMessages = new EnumMap<>(StandardMessage.class); + + standardMessages.put(RELOADED_CONFIG, MessageBuilder::reloadedConfig); + standardMessages.put(STILL_RELOADING, MessageBuilder::stillReloading); + standardMessages.put(EXCLUDE_FAILED, MessageBuilder::excludeFailed); + standardMessages.put(INCLUDE_FAILED, MessageBuilder::includeFailed); + standardMessages.put(MISSING_STAT_NAME, MessageBuilder::missingStatName); + standardMessages.put(MISSING_PLAYER_NAME, MessageBuilder::missingPlayerName); + standardMessages.put(PLAYER_IS_EXCLUDED, MessageBuilder::playerIsExcluded); + standardMessages.put(WAIT_A_MOMENT, MessageBuilder::waitAMoment); + standardMessages.put(WAIT_A_MINUTE, MessageBuilder::waitAMinute); + standardMessages.put(REQUEST_ALREADY_RUNNING, MessageBuilder::requestAlreadyRunning); + standardMessages.put(STILL_ON_SHARE_COOLDOWN, MessageBuilder::stillOnShareCoolDown); + standardMessages.put(RESULTS_ALREADY_SHARED, MessageBuilder::resultsAlreadyShared); + standardMessages.put(STAT_RESULTS_TOO_OLD, MessageBuilder::statResultsTooOld); + standardMessages.put(UNKNOWN_ERROR, MessageBuilder::unknownError); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/BirthdayComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/BirthdayComponentFactory.java new file mode 100644 index 0000000..44315a1 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/BirthdayComponentFactory.java @@ -0,0 +1,24 @@ +package com.artemis.the.gr8.playerstats.core.msg.components; + +import net.kyori.adventure.text.TextComponent; + +public final class BirthdayComponentFactory extends ComponentFactory { + + public BirthdayComponentFactory() { + super(); + } + + @Override + public TextComponent pluginPrefixAsTitle() { + return miniMessageToComponent( + "" + + "<#FF9300>\ud83d\udd25 __________ [PlayerStats] __________ " + + "<#FF9300>\ud83d\udd25"); + } + + @Override + public TextComponent pluginPrefix() { + return miniMessageToComponent( + "[PlayerStats]"); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/BukkitConsoleComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/BukkitConsoleComponentFactory.java similarity index 58% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/components/BukkitConsoleComponentFactory.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/BukkitConsoleComponentFactory.java index 40c6697..41d65f6 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/BukkitConsoleComponentFactory.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/BukkitConsoleComponentFactory.java @@ -1,7 +1,6 @@ -package com.artemis.the.gr8.playerstats.msg.components; +package com.artemis.the.gr8.playerstats.core.msg.components; -import com.artemis.the.gr8.playerstats.enums.PluginColor; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.enums.PluginColor; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; @@ -16,10 +15,10 @@ * a Bukkit Console. Bukkit consoles don't support hex colors, * unlike Paper consoles. */ -public class BukkitConsoleComponentFactory extends ComponentFactory { +public final class BukkitConsoleComponentFactory extends ComponentFactory { - public BukkitConsoleComponentFactory(ConfigHandler config) { - super(config); + public BukkitConsoleComponentFactory() { + super(); } @Override @@ -29,21 +28,44 @@ protected void prepareColors() { UNDERSCORE = PluginColor.DARK_PURPLE.getConsoleColor(); HEARTS = PluginColor.RED.getConsoleColor(); - MSG_MAIN = PluginColor.MEDIUM_BLUE.getConsoleColor(); - MSG_ACCENT = PluginColor.BLUE.getConsoleColor(); + FEEDBACK_MSG = PluginColor.LIGHTEST_BLUE.getConsoleColor(); + FEEDBACK_MSG_ACCENT = PluginColor.LIGHT_BLUE.getConsoleColor(); - MSG_MAIN_2 = PluginColor.GOLD.getConsoleColor(); - MSG_ACCENT_2A = PluginColor.MEDIUM_GOLD.getConsoleColor(); - MSG_ACCENT_2B = PluginColor.LIGHT_YELLOW.getConsoleColor(); + INFO_MSG = PluginColor.GOLD.getConsoleColor(); + INFO_MSG_ACCENT_DARKEST = PluginColor.MEDIUM_GOLD.getConsoleColor(); + INFO_MSG_ACCENT_MEDIUM = PluginColor.LIGHT_GOLD.getConsoleColor(); + INFO_MSG_ACCENT_LIGHTEST = PluginColor.LIGHTEST_BLUE.getConsoleColor(); - MSG_HOVER = PluginColor.LIGHT_BLUE.getConsoleColor(); + MSG_HOVER = PluginColor.LIGHTEST_BLUE.getConsoleColor(); MSG_CLICKED = PluginColor.LIGHT_PURPLE.getConsoleColor(); - MSG_HOVER_ACCENT = PluginColor.LIGHT_GOLD.getConsoleColor(); } @Override - public TextColor getSharerNameColor() { - return PluginColor.NAME_5.getConsoleColor(); + public boolean isConsoleFactory() { + return true; + } + + @Override + public TextComponent heart() { + return text() + .content(String.valueOf('\u2665')) + .color(HEARTS) + .build(); + } + + @Override + public TextComponent arrow() { + return text(" ->").color(INFO_MSG); + } + + @Override + public TextComponent bulletPoint() { + return text(" *").color(INFO_MSG); + } + + @Override + public TextComponent bulletPointIndented() { + return text(" *").color(INFO_MSG); } @Override diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/ComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ComponentFactory.java similarity index 78% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/components/ComponentFactory.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ComponentFactory.java index f4c2506..94f6f84 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/ComponentFactory.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ComponentFactory.java @@ -1,11 +1,11 @@ -package com.artemis.the.gr8.playerstats.msg.components; - -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.enums.PluginColor; -import com.artemis.the.gr8.playerstats.enums.Target; -import com.artemis.the.gr8.playerstats.enums.Unit; -import com.artemis.the.gr8.playerstats.msg.MessageBuilder; -import com.artemis.the.gr8.playerstats.msg.msgutils.LanguageKeyHandler; +package com.artemis.the.gr8.playerstats.core.msg.components; + +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.enums.PluginColor; +import com.artemis.the.gr8.playerstats.api.enums.Target; +import com.artemis.the.gr8.playerstats.api.enums.Unit; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.LanguageKeyHandler; +import com.artemis.the.gr8.playerstats.core.msg.MessageBuilder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TranslatableComponent; @@ -14,9 +14,11 @@ 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.text.minimessage.MiniMessage; import net.kyori.adventure.util.HSVLike; import net.kyori.adventure.util.Index; import org.bukkit.Bukkit; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -39,20 +41,20 @@ public class ComponentFactory { protected TextColor UNDERSCORE; //dark_purple protected TextColor HEARTS; //red - protected TextColor MSG_MAIN; //medium_blue - protected TextColor MSG_ACCENT; //blue + protected TextColor FEEDBACK_MSG; //lightest_blue + protected TextColor FEEDBACK_MSG_ACCENT; //light_blue - protected TextColor MSG_MAIN_2; //gold - protected TextColor MSG_ACCENT_2A; //medium_gold - protected TextColor MSG_ACCENT_2B; //light_yellow + protected TextColor INFO_MSG; //gold + protected TextColor INFO_MSG_ACCENT_DARKEST; //medium_gold + protected TextColor INFO_MSG_ACCENT_MEDIUM; //light_gold + protected TextColor INFO_MSG_ACCENT_LIGHTEST; //lightest_blue - protected TextColor MSG_HOVER; //light_blue + protected TextColor MSG_HOVER; //lightest_blue protected TextColor MSG_CLICKED; //light_purple - protected TextColor MSG_HOVER_ACCENT; //light_gold - public ComponentFactory(ConfigHandler c) { - config = c; + public ComponentFactory() { + config = ConfigHandler.getInstance(); prepareColors(); } @@ -62,23 +64,31 @@ protected void prepareColors() { UNDERSCORE = PluginColor.DARK_PURPLE.getColor(); HEARTS = PluginColor.RED.getColor(); - MSG_MAIN = PluginColor.MEDIUM_BLUE.getColor(); - MSG_ACCENT = PluginColor.BLUE.getColor(); + FEEDBACK_MSG = PluginColor.LIGHTEST_BLUE.getColor(); + FEEDBACK_MSG_ACCENT = PluginColor.LIGHT_BLUE.getColor(); - MSG_MAIN_2 = PluginColor.GOLD.getColor(); - MSG_ACCENT_2A = PluginColor.MEDIUM_GOLD.getColor(); - MSG_ACCENT_2B = PluginColor.LIGHT_YELLOW.getColor(); + INFO_MSG = PluginColor.GOLD.getColor(); + INFO_MSG_ACCENT_DARKEST = PluginColor.MEDIUM_GOLD.getColor(); + INFO_MSG_ACCENT_MEDIUM = PluginColor.LIGHT_GOLD.getColor(); + INFO_MSG_ACCENT_LIGHTEST = PluginColor.LIGHTEST_BLUE.getColor(); - MSG_HOVER = PluginColor.LIGHT_BLUE.getColor(); - MSG_HOVER_ACCENT = PluginColor.LIGHT_GOLD.getColor(); + MSG_HOVER = PluginColor.LIGHTEST_BLUE.getColor(); MSG_CLICKED = PluginColor.LIGHT_PURPLE.getColor(); } - public TextColor getExampleNameColor() { - return MSG_ACCENT_2B; + @Contract("_ -> new") + protected @NotNull TextComponent miniMessageToComponent(String input) { + return text() + .append(MiniMessage.miniMessage().deserialize(input)) + .build(); + } + + public boolean isConsoleFactory() { + return false; } - public TextColor getSharerNameColor() { - return getColorFromString(config.getSharerNameDecoration(false)); + + public TextComponent getExampleName() { + return text("Artemis_the_gr8").color(FEEDBACK_MSG); } /** @@ -95,11 +105,10 @@ public TextComponent pluginPrefix() { * Returns [PlayerStats] surrounded by underscores on both sides. */ public TextComponent pluginPrefixAsTitle() { - //12 underscores for both console and in-game - return text("____________").color(UNDERSCORE) + return text("____________").color(UNDERSCORE) //12 underscores .append(text(" ")) //4 spaces .append(pluginPrefix()) - .append(text(" ")) //4 spaces + .append(text(" ")) .append(text("____________")); } @@ -116,11 +125,15 @@ public TextComponent subTitle(String content) { * with color Medium_Blue. */ public TextComponent message() { - return text().color(MSG_MAIN).build(); + return text().color(FEEDBACK_MSG).build(); } public TextComponent messageAccent() { - return text().color(MSG_ACCENT).build(); + return text().color(FEEDBACK_MSG_ACCENT).build(); + } + + public TextComponent infoMessageAccent() { + return text().color(INFO_MSG_ACCENT_MEDIUM).build(); } public TextComponent title(String content, Target target) { @@ -163,17 +176,17 @@ public TextComponent playerName(String playerName, Target target) { public TextComponent sharerName(String sharerName) { return getComponent(sharerName, - getSharerNameColor(), + getColorFromString(config.getSharerNameDecoration(false)), getStyleFromString(config.getSharerNameDecoration(true))); } public TextComponent shareButton(int shareCode) { return surroundWithBrackets( text("Share") - .color(MSG_HOVER) + .color(FEEDBACK_MSG_ACCENT) .clickEvent(ClickEvent.runCommand("/statshare " + shareCode)) .hoverEvent(HoverEvent.showText(text("Click here to share this statistic in chat!") - .color(MSG_HOVER_ACCENT)))); + .color(INFO_MSG_ACCENT_MEDIUM)))); } public TextComponent sharedByMessage(Component playerName) { @@ -225,10 +238,10 @@ public TextComponent statAndSubStatNameTranslatable(String statKey, @Nullable St getStyleFromString(config.getStatNameDecoration(target, true))); TextComponent subStat = subStatNameTranslatable(subStatKey, target); - if (LanguageKeyHandler.isKeyForKillEntity(statKey)) { + if (LanguageKeyHandler.isNormalKeyForKillEntity(statKey)) { return totalStatNameBuilder.append(killEntityBuilder(subStat)).build(); } - else if (LanguageKeyHandler.isKeyForEntityKilledBy(statKey)) { + else if (LanguageKeyHandler.isNormalKeyForEntityKilledBy(statKey)) { return totalStatNameBuilder.append(entityKilledByBuilder(subStat)).build(); } else { @@ -265,7 +278,7 @@ public TextComponent damageNumberWithHoverText(String mainNumber, String hoverNu } public TextComponent damageNumberWithHeartUnitInHoverText(String mainNumber, String hoverNumber, Target target) { - return statNumberWithHoverText(mainNumber, hoverNumber, null, null, clientHeart(true), target); + return statNumberWithHoverText(mainNumber, hoverNumber, null, null, heart(), target); } public TextComponent distanceNumber(String prettyNumber, Target target) { @@ -298,34 +311,37 @@ public TextComponent statUnitTranslatable(String unitKey, Target target) { return surroundWithBrackets(statUnit); } - public TextComponent clientHeart(boolean isDisplayedInHoverText) { - TextComponent basicHeartComponent = basicHeartComponent('\u2764'); - if (isDisplayedInHoverText) { - return basicHeartComponent; - } - return surroundWithBrackets(basicHeartComponent); + public TextComponent heart() { + return text() + .content(String.valueOf('\u2764')) + .color(HEARTS) + .build(); + } + + public TextComponent heartBetweenBrackets() { + return surroundWithBrackets(heart()); } - public TextComponent clientHeartWithHoverText() { - TextComponent basicHeartComponent = basicHeartComponent('\u2764') + public TextComponent heartBetweenBracketsWithHoverText() { + TextComponent heart = heart() .toBuilder() .hoverEvent(HoverEvent.showText( text(Unit.HEART.getLabel()) - .color(MSG_HOVER_ACCENT))) + .color(INFO_MSG_ACCENT_MEDIUM))) .build(); - return surroundWithBrackets(basicHeartComponent); + return surroundWithBrackets(heart); } - public TextComponent consoleHeart() { - return surroundWithBrackets(basicHeartComponent('\u2665')); + public TextComponent arrow() { + return text(" →").color(INFO_MSG); //4 spaces, alt + 26 } - //console can do u2665, u2764 looks better in-game - private TextComponent basicHeartComponent(char heartChar) { - return Component.text() - .content(String.valueOf(heartChar)) - .color(HEARTS) - .build(); + public TextComponent bulletPoint() { + return text(" •").color(INFO_MSG); //4 spaces, alt + 7 + } + + public TextComponent bulletPointIndented() { + return text(" •").color(INFO_MSG); //8 spaces, alt + 7 } /** @@ -368,9 +384,9 @@ private TextComponent subStatNameTranslatable(@Nullable String subStatKey, Targe * * @return a TranslatableComponent Builder with the subStat Component as args. */ - private TranslatableComponent.Builder killEntityBuilder(@NotNull TextComponent subStat) { + private @NotNull TranslatableComponent.Builder killEntityBuilder(@NotNull TextComponent subStat) { return translatable() - .key(LanguageKeyHandler.getAlternativeKeyForKillEntity()) //"Killed %s" + .key(LanguageKeyHandler.getCustomKeyForKillEntity()) //"Killed %s" .args(subStat); } @@ -382,19 +398,19 @@ private TranslatableComponent.Builder killEntityBuilder(@NotNull TextComponent s * @return a TranslatableComponent Builder with stat.minecraft.deaths as key, * with a ChildComponent with book.byAuthor as key and the subStat Component as args. */ - private TranslatableComponent.Builder entityKilledByBuilder(@NotNull TextComponent subStat) { + private @NotNull TranslatableComponent.Builder entityKilledByBuilder(@NotNull TextComponent subStat) { return translatable() - .key(LanguageKeyHandler.getAlternativeKeyForEntityKilledBy()) //"Number of Deaths" + .key(LanguageKeyHandler.getCustomKeyForEntityKilledBy()) //"Number of Deaths" .append(space()) .append(translatable() - .key(LanguageKeyHandler.getAlternativeKeyForEntityKilledByArg()) //"by %s" + .key(LanguageKeyHandler.getCustomKeyForEntityKilledByArg()) //"by %s" .args(subStat)); } - private TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber, - @Nullable String hoverUnitName, - @Nullable String hoverUnitKey, - @Nullable TextComponent heartComponent, Target target) { + private @NotNull TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber, + @Nullable String hoverUnitName, + @Nullable String hoverUnitKey, + @Nullable TextComponent heartComponent, Target target) { TextColor baseColor = getColorFromString(config.getStatNumberDecoration(target, false)); TextDecoration style = getStyleFromString(config.getStatNumberDecoration(target, true)); @@ -415,7 +431,7 @@ else if (hoverUnitName != null) { return getComponent(mainNumber, baseColor, style).hoverEvent(HoverEvent.showText(hoverText)); } - private TextComponent surroundWithBrackets(TextComponent component) { + private @NotNull TextComponent surroundWithBrackets(TextComponent component) { return getComponent(null, BRACKETS, null) .append(text("[")) .append(component) @@ -465,7 +481,7 @@ private TextColor getTextColorByName(String textColor) { return names.value(textColor); } - private TextColor getLighterColor(TextColor color) { + private @NotNull TextColor getLighterColor(@NotNull TextColor color) { float multiplier = (float) ((100 - config.getHoverTextAmountLighter()) / 100.0); HSVLike oldColor = HSVLike.fromRGB(color.red(), color.green(), color.blue()); HSVLike newColor = HSVLike.hsvLike(oldColor.h(), oldColor.s() * multiplier, oldColor.v()); diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ConsoleComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ConsoleComponentFactory.java new file mode 100644 index 0000000..2a9c78a --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ConsoleComponentFactory.java @@ -0,0 +1,24 @@ +package com.artemis.the.gr8.playerstats.core.msg.components; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; + +public final class ConsoleComponentFactory extends ComponentFactory { + + public ConsoleComponentFactory() { + super(); + } + + @Override + public boolean isConsoleFactory() { + return true; + } + + @Override + public TextComponent heart() { + return Component.text() + .content(String.valueOf('\u2665')) + .color(HEARTS) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/ExampleMessage.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ExampleMessage.java similarity index 55% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/components/ExampleMessage.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ExampleMessage.java index 33bc58d..7fd0338 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/ExampleMessage.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ExampleMessage.java @@ -1,9 +1,10 @@ -package com.artemis.the.gr8.playerstats.msg.components; +package com.artemis.the.gr8.playerstats.core.msg.components; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.Style; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; @@ -22,34 +23,32 @@ private ExampleMessage(ComponentFactory factory) { exampleMessage = buildMessage(factory); } - public static ExampleMessage construct(ComponentFactory factory) { + @Contract("_ -> new") + public static @NotNull ExampleMessage construct(ComponentFactory factory) { return new ExampleMessage(factory); } - private TextComponent buildMessage(ComponentFactory factory) { - String arrow = factory instanceof BukkitConsoleComponentFactory ? " -> " : " → "; //4 spaces, alt + 26, 1 space - + private @NotNull TextComponent buildMessage(@NotNull ComponentFactory factory) { return Component.newline() .append(factory.pluginPrefixAsTitle()) .append(Component.newline()) - .append(text("Examples: ").color(factory.MSG_MAIN_2)) + .append(factory.subTitle("Examples: ")) .append(Component.newline()) - .append(text(arrow).color(factory.MSG_MAIN_2) - .append(text("/statistic ") - .append(text("animals_bred ").color(factory.MSG_ACCENT_2A) - .append(text("top").color(factory.MSG_ACCENT_2B))))) + .append(factory.arrow()).append(Component.space()) + .append(text("/stat ").color(factory.INFO_MSG) + .append(text("animals_bred ").color(factory.INFO_MSG_ACCENT_MEDIUM) + .append(text("top").color(factory.INFO_MSG_ACCENT_LIGHTEST)))) .append(Component.newline()) - .append(text(arrow).color(factory.MSG_MAIN_2) - .append(text("/statistic ") - .append(text("mine_block diorite ").color(factory.MSG_ACCENT_2A) - .append(text("me").color(factory.MSG_ACCENT_2B))))) + .append(factory.arrow()).append(Component.space()) + .append(text("/stat ").color(factory.INFO_MSG) + .append(text("mine_block diorite ").color(factory.INFO_MSG_ACCENT_MEDIUM) + .append(text("me").color(factory.INFO_MSG_ACCENT_LIGHTEST)))) .append(Component.newline()) - .append(text(arrow).color(factory.MSG_MAIN_2) - .append(text("/statistic ") - .append(text("deaths ").color(factory.MSG_ACCENT_2A) - .append(text("player ").color(factory.MSG_ACCENT_2B) - .append(text("Artemis_the_gr8") - .color(factory.getExampleNameColor())))))); + .append(factory.arrow()).append(Component.space()) + .append(text("/stat ").color(factory.INFO_MSG) + .append(text("deaths ").color(factory.INFO_MSG_ACCENT_MEDIUM) + .append(text("player ").color(factory.INFO_MSG_ACCENT_LIGHTEST) + .append(factory.getExampleName())))); } @Override diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ExcludeInfoMessage.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ExcludeInfoMessage.java new file mode 100644 index 0000000..eb2f35a --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/ExcludeInfoMessage.java @@ -0,0 +1,109 @@ +package com.artemis.the.gr8.playerstats.core.msg.components; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.ComponentLike; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.Style; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.List; + +import static net.kyori.adventure.text.Component.text; + +public final class ExcludeInfoMessage implements TextComponent { + + private final TextComponent excludeInfo; + + private ExcludeInfoMessage(ComponentFactory factory) { + excludeInfo = buildMessage(factory); + } + + @Contract("_ -> new") + public static @NotNull ExcludeInfoMessage construct(ComponentFactory factory) { + return new ExcludeInfoMessage(factory); + } + + private @NotNull TextComponent buildMessage(@NotNull ComponentFactory factory) { + return Component.newline() + .append(factory.pluginPrefixAsTitle()) + .append(Component.newline()) + .append(factory.subTitle("Hover over the arguments for more information!")) + .append(Component.newline()) + .append(text("Usage: ").color(factory.INFO_MSG) + .append(text("/statexclude").color(factory.INFO_MSG_ACCENT_MEDIUM))) + .append(Component.newline()) + .append(factory.bulletPoint()).append(Component.space()) + .append(text("add ").color(factory.INFO_MSG_ACCENT_DARKEST) + .append(text("{player-name}").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .hoverEvent(HoverEvent.showText( + text("Excludes this player from /stat results").color(factory.INFO_MSG_ACCENT_LIGHTEST)))) + .append(Component.newline()) + .append(factory.bulletPoint()).append(Component.space()) + .append(text("remove ").color(factory.INFO_MSG_ACCENT_DARKEST) + .append(text("{player-name}").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .hoverEvent(HoverEvent.showText( + text("Includes this player in /stat results again").color(factory.INFO_MSG_ACCENT_LIGHTEST)))) + .append(Component.newline()) + .append(factory.bulletPoint()).append(Component.space()) + .append(text("list").color(factory.INFO_MSG_ACCENT_DARKEST) + .hoverEvent(HoverEvent.showText( + text("See a list of all currently excluded players").color(factory.INFO_MSG_ACCENT_LIGHTEST)))) + .append(Component.newline()) + .append(Component.newline()) + .append(text("Excluded players are:") + .color(factory.INFO_MSG)) + .append(Component.newline()) + .append(factory.arrow()).append(Component.space()) + .append(text("not visible in the top 10").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .append(Component.newline()) + .append(factory.arrow()).append(Component.space()) + .append(text("not counted for the server total").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .append(Component.newline()) + .append(factory.arrow()).append(Component.space()) + .append(text("hidden").color(factory.INFO_MSG_ACCENT_LIGHTEST) + .hoverEvent(HoverEvent.showText(text("All statistics are still stored and tracked by the") + .append(Component.newline()) + .append(text("server, this command does not delete anything!")) + .color(factory.INFO_MSG_ACCENT_LIGHTEST)))) + .append(text(" - not removed") + .color(factory.INFO_MSG_ACCENT_MEDIUM)); + } + + @Override + public @NotNull String content() { + return excludeInfo.content(); + } + + @Override + public @NotNull TextComponent content(@NotNull String content) { + return excludeInfo.content(content); + } + + @Override + public @NotNull Builder toBuilder() { + return excludeInfo.toBuilder(); + } + + @Override + public @Unmodifiable @NotNull List children() { + return excludeInfo.children(); + } + + @Override + public @NotNull TextComponent children(@NotNull List children) { + return excludeInfo.children(children); + } + + @Override + public @NotNull Style style() { + return excludeInfo.style(); + } + + @Override + public @NotNull TextComponent style(@NotNull Style style) { + return excludeInfo.style(style); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/HalloweenComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/HalloweenComponentFactory.java new file mode 100644 index 0000000..60effbc --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/HalloweenComponentFactory.java @@ -0,0 +1,47 @@ +package com.artemis.the.gr8.playerstats.core.msg.components; + +import net.kyori.adventure.text.TextComponent; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; + +public final class HalloweenComponentFactory extends ComponentFactory { + + + public HalloweenComponentFactory() { + super(); + } + + @Override + public TextComponent pluginPrefixAsTitle() { + return miniMessageToComponent( + "" + + "\u2620 __________ [PlayerStats] __________ " + + "\u2620"); + } + + @Override + public TextComponent pluginPrefix() { + return miniMessageToComponent( + "[PlayerStats]"); + } + + @Override + public TextComponent sharerName(String sharerName) { + return miniMessageToComponent(decorateWithRandomGradient(sharerName)); + } + + private @NotNull String decorateWithRandomGradient(@NotNull String input) { + Random random = new Random(); + String colorString = switch (random.nextInt(6)) { + case 0 -> ""; + case 1 -> ""; + case 2 -> ""; + case 3 -> ""; + case 4 -> ""; + case 5 -> ""; + default -> ""; + }; + return colorString + input + ""; + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/HelpMessage.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/HelpMessage.java similarity index 59% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/components/HelpMessage.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/HelpMessage.java index 20ecb02..592d885 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/HelpMessage.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/HelpMessage.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.msg.components; +package com.artemis.the.gr8.playerstats.core.msg.components; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.ComponentLike; @@ -6,6 +6,7 @@ import net.kyori.adventure.text.event.HoverEvent; import net.kyori.adventure.text.format.Style; import net.kyori.adventure.text.format.TextDecoration; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; @@ -28,116 +29,105 @@ private HelpMessage(ComponentFactory factory, boolean useHover, int listSize) { } } - public static HelpMessage constructPlainMsg(ComponentFactory factory, int listSize) { + @Contract("_, _ -> new") + public static @NotNull HelpMessage constructPlainMsg(ComponentFactory factory, int listSize) { return new HelpMessage(factory, false, listSize); } - public static HelpMessage constructHoverMsg(ComponentFactory factory, int listSize) { + @Contract("_, _ -> new") + public static @NotNull HelpMessage constructHoverMsg(ComponentFactory factory, int listSize) { return new HelpMessage(factory, true, listSize); } - private TextComponent buildPlainMsg(ComponentFactory factory, int listSize) { - String arrowSymbol = "→"; //alt + 26 - String bulletSymbol = "•"; //alt + 7 - - if (factory instanceof BukkitConsoleComponentFactory) { - arrowSymbol = "->"; - bulletSymbol = "*"; - } - TextComponent spaces = text(" "); //4 spaces - TextComponent arrow = text(arrowSymbol).color(factory.MSG_MAIN_2); - TextComponent bullet = text(bulletSymbol).color(factory.MSG_MAIN_2); - + private @NotNull TextComponent buildPlainMsg(ComponentFactory factory, int listSize) { return Component.newline() .append(factory.pluginPrefixAsTitle()) .append(newline()) .append(text("Type \"/statistic examples\" to see examples!").color(factory.BRACKETS).decorate(TextDecoration.ITALIC)) .append(newline()) - .append(text("Usage:").color(factory.MSG_MAIN_2)).append(space()) - .append(text("/statistic").color(factory.MSG_HOVER_ACCENT)) + .append(text("Usage:").color(factory.INFO_MSG)).append(space()) + .append(text("/statistic").color(factory.INFO_MSG_ACCENT_MEDIUM)) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("name").color(factory.MSG_HOVER_ACCENT)) + .append(factory.arrow()).append(space()) + .append(text("name").color(factory.INFO_MSG_ACCENT_MEDIUM)) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("{sub-statistic}").color(factory.MSG_HOVER_ACCENT)).append(space()) + .append(factory.arrow()).append(space()) + .append(text("{sub-statistic}").color(factory.INFO_MSG_ACCENT_MEDIUM)).append(space()) .append(text("(a block, item or entity)").color(factory.BRACKETS)) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("me | player | server | top").color(factory.MSG_HOVER_ACCENT)) + .append(factory.arrow()).append(space()) + .append(text("me | player | server | top").color(factory.INFO_MSG_ACCENT_MEDIUM)) .append(newline()) - .append(spaces).append(spaces).append(bullet).append(space()) - .append(text("me:").color(factory.MSG_ACCENT_2A)).append(space()) + .append(factory.bulletPointIndented()).append(space()) + .append(text("me:").color(factory.INFO_MSG_ACCENT_DARKEST)).append(space()) .append(text("your own statistic").color(factory.BRACKETS)) .append(newline()) - .append(spaces).append(spaces).append(bullet).append(space()) - .append(text("player:").color(factory.MSG_ACCENT_2A)).append(space()) + .append(factory.bulletPointIndented()).append(space()) + .append(text("player:").color(factory.INFO_MSG_ACCENT_DARKEST)).append(space()) .append(text("choose a player").color(factory.BRACKETS)) .append(newline()) - .append(spaces).append(spaces).append(bullet).append(space()) - .append(text("server:").color(factory.MSG_ACCENT_2A)).append(space()) + .append(factory.bulletPointIndented()).append(space()) + .append(text("server:").color(factory.INFO_MSG_ACCENT_DARKEST)).append(space()) .append(text("everyone on the server combined").color(factory.BRACKETS)) .append(newline()) - .append(spaces).append(spaces).append(bullet).append(space()) - .append(text("top:").color(factory.MSG_ACCENT_2A)).append(space()) + .append(factory.bulletPointIndented()).append(space()) + .append(text("top:").color(factory.INFO_MSG_ACCENT_DARKEST)).append(space()) .append(text("the top").color(factory.BRACKETS).append(space()).append(text(listSize))) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("{player-name}").color(factory.MSG_HOVER_ACCENT)); + .append(factory.arrow()).append(space()) + .append(text("{player-name}").color(factory.INFO_MSG_ACCENT_MEDIUM)); } - private TextComponent buildHoverMsg(ComponentFactory factory, int listSize) { - TextComponent spaces = text(" "); - TextComponent arrow = text("→").color(factory.MSG_MAIN_2); - + private @NotNull TextComponent buildHoverMsg(@NotNull ComponentFactory factory, int listSize) { return Component.newline() .append(factory.pluginPrefixAsTitle()) .append(newline()) .append(factory.subTitle("Hover over the arguments for more information!")) .append(newline()) - .append(text("Usage:").color(factory.MSG_MAIN_2)).append(space()) - .append(text("/statistic").color(factory.MSG_HOVER_ACCENT)) + .append(text("Usage:").color(factory.INFO_MSG)).append(space()) + .append(text("/statistic").color(factory.INFO_MSG_ACCENT_MEDIUM)) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("name").color(factory.MSG_HOVER_ACCENT) + .append(factory.arrow()).append(space()) + .append(text("name").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText(text("The name that describes the statistic").color(factory.MSG_HOVER) .append(newline()) - .append(text("Example: ").color(factory.MSG_MAIN_2)) - .append(text("\"animals_bred\"").color(factory.MSG_HOVER_ACCENT))))) + .append(text("Example: ").color(factory.INFO_MSG)) + .append(text("\"animals_bred\"").color(factory.INFO_MSG_ACCENT_MEDIUM))))) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("sub-statistic").color(factory.MSG_HOVER_ACCENT) + .append(factory.arrow()).append(space()) + .append(text("sub-statistic").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText( text("Some statistics need an item, block or entity as extra input").color(factory.MSG_HOVER) .append(newline()) - .append(text("Example: ").color(factory.MSG_MAIN_2) - .append(text("\"mine_block diorite\"").color(factory.MSG_HOVER_ACCENT)))))) + .append(text("Example: ").color(factory.INFO_MSG) + .append(text("\"mine_block diorite\"").color(factory.INFO_MSG_ACCENT_MEDIUM)))))) .append(newline()) - .append(spaces).append(arrow + .append(factory.arrow() .hoverEvent(HoverEvent.showText( - text("Choose one").color(factory.UNDERSCORE)))).append(space()) - .append(text("me").color(factory.MSG_HOVER_ACCENT) + text("Choose one").color(factory.MSG_CLICKED)))) + .append(space()) + .append(text("me").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText( text("See your own statistic").color(factory.MSG_HOVER)))) - .append(text(" | ").color(factory.MSG_HOVER_ACCENT)) - .append(text("player").color(factory.MSG_HOVER_ACCENT) + .append(text(" | ").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .append(text("player").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText( text("Choose any player that has played on your server").color(factory.MSG_HOVER)))) - .append(text(" | ").color(factory.MSG_HOVER_ACCENT)) - .append(text("server").color(factory.MSG_HOVER_ACCENT) + .append(text(" | ").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .append(text("server").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText( text("See the combined total for everyone on your server").color(factory.MSG_HOVER)))) - .append(text(" | ").color(factory.MSG_HOVER_ACCENT)) - .append(text("top").color(factory.MSG_HOVER_ACCENT) + .append(text(" | ").color(factory.INFO_MSG_ACCENT_MEDIUM)) + .append(text("top").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText( text("See the top").color(factory.MSG_HOVER).append(space()) .append(text(listSize))))) .append(newline()) - .append(spaces).append(arrow).append(space()) - .append(text("player-name").color(factory.MSG_HOVER_ACCENT) + .append(factory.arrow()).append(space()) + .append(text("player-name").color(factory.INFO_MSG_ACCENT_MEDIUM) .hoverEvent(HoverEvent.showText( text("In case you typed").color(factory.MSG_HOVER).append(space()) - .append(text("\"player\"").color(factory.MSG_HOVER_ACCENT)) + .append(text("\"player\"").color(factory.INFO_MSG_ACCENT_MEDIUM)) .append(text(", add the player's name"))))); } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/PrideComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/PrideComponentFactory.java new file mode 100644 index 0000000..7821683 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/PrideComponentFactory.java @@ -0,0 +1,65 @@ +package com.artemis.the.gr8.playerstats.core.msg.components; + +import net.kyori.adventure.text.TextComponent; +import org.jetbrains.annotations.NotNull; + +import java.util.Random; + +/** + * A festive version of the {@link ComponentFactory} + */ +public final class PrideComponentFactory extends ComponentFactory { + + public PrideComponentFactory() { + super(); + } + + @Override + public TextComponent getExampleName() { + return miniMessageToComponent("Artemis_the_gr8"); + } + + @Override + public TextComponent sharerName(String sharerName) { + return miniMessageToComponent(decorateWithRandomGradient(sharerName)); + } + + @Override + //12 underscores + public TextComponent pluginPrefixAsTitle() { + return miniMessageToComponent("____________ [PlayerStats] ____________"); + } + + @Override + public TextComponent pluginPrefix() { + return miniMessageToComponent("<#f74040>[" + + "<#F54D39>P" + + "<#F16E28>l" + + "<#ee8a19>a" + + "<#EEA019>y" + + "<#F7C522>e" + + "<#C1DA15>r" + + "<#84D937>S" + + "<#46D858>t" + + "<#01c1a7>a" + + "<#1F8BEB>t" + + "<#3341E6>s" + + "<#631ae6>]"); + } + + private @NotNull String decorateWithRandomGradient(@NotNull String input) { + Random random = new Random(); + String colorString = switch (random.nextInt(8)) { + case 0 -> ""; + case 1 -> ""; + case 2 -> ""; + case 3 -> ""; + case 4 -> ""; + case 5 -> ""; + case 6 -> ""; + case 7 -> ""; + default -> ""; + }; + return colorString + input + ""; + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/WinterComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/WinterComponentFactory.java new file mode 100644 index 0000000..d15e915 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/components/WinterComponentFactory.java @@ -0,0 +1,24 @@ +package com.artemis.the.gr8.playerstats.core.msg.components; + +import net.kyori.adventure.text.TextComponent; + +public final class WinterComponentFactory extends ComponentFactory { + + public WinterComponentFactory() { + super(); + } + + @Override + public TextComponent pluginPrefixAsTitle() { + return miniMessageToComponent( + "" + + "<#D6F1FE>\u2744 __________ [PlayerStats] __________ " + + "<#D6F1FE>\u2744"); + } + + @Override + public TextComponent pluginPrefix() { + return miniMessageToComponent( + "[PlayerStats]"); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/ComponentUtils.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/ComponentSerializer.java similarity index 72% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/components/ComponentUtils.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/ComponentSerializer.java index c5fc688..b4453fb 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/ComponentUtils.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/ComponentSerializer.java @@ -1,15 +1,21 @@ -package com.artemis.the.gr8.playerstats.msg.components; +package com.artemis.the.gr8.playerstats.core.msg.msgutils; -import com.artemis.the.gr8.playerstats.msg.msgutils.LanguageKeyHandler; -import com.artemis.the.gr8.playerstats.msg.msgutils.StringUtils; import net.kyori.adventure.text.*; import net.kyori.adventure.text.flattener.ComponentFlattener; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; /** * A small utility class for turning PlayerStats' custom Components into String. */ -public final class ComponentUtils { +public final class ComponentSerializer { + + private final LanguageKeyHandler languageKeyHandler; + + public ComponentSerializer() { + languageKeyHandler = LanguageKeyHandler.getInstance(); + } /** * Returns a LegacyComponentSerializer that is capable of serializing @@ -21,19 +27,19 @@ public final class ComponentUtils { * @return the Serializer * @see LanguageKeyHandler */ - public static LegacyComponentSerializer getTranslatableComponentSerializer() { + public @NotNull LegacyComponentSerializer getTranslatableComponentSerializer() { LegacyComponentSerializer serializer = getTextComponentSerializer(); ComponentFlattener flattener = ComponentFlattener.basic().toBuilder() .mapper(TranslatableComponent.class, trans -> { StringBuilder totalPrettyName = new StringBuilder(); - if (LanguageKeyHandler.isKeyForEntityKilledByArg(trans.key())) { + if (LanguageKeyHandler.isCustomKeyForEntityKilledByArg(trans.key())) { return ""; } - else if (LanguageKeyHandler.isKeyForEntityKilledBy(trans.key()) || - LanguageKeyHandler.isAlternativeKeyForEntityKilledBy(trans.key()) || - LanguageKeyHandler.isKeyForKillEntity(trans.key()) || - LanguageKeyHandler.isAlternativeKeyForKillEntity(trans.key())) { + else if (LanguageKeyHandler.isNormalKeyForEntityKilledBy(trans.key()) || + LanguageKeyHandler.isCustomKeyForEntityKilledBy(trans.key()) || + LanguageKeyHandler.isNormalKeyForKillEntity(trans.key()) || + LanguageKeyHandler.isCustomKeyForKillEntity(trans.key())) { TextComponent.Builder temp = Component.text(); trans.iterator(ComponentIteratorType.DEPTH_FIRST, ComponentIteratorFlag.INCLUDE_TRANSLATABLE_COMPONENT_ARGUMENTS) @@ -50,28 +56,25 @@ else if (LanguageKeyHandler.isKeyForEntityKilledBy(trans.key()) || } //isolate the translatable component with the entity inside else if (component instanceof TranslatableComponent translatable) { - if (translatable.key().contains("entity.")) { + if (LanguageKeyHandler.isEntityKey(translatable.key())) { temp.append(Component.space()) .append(Component.text("(") .append(Component.text( - StringUtils.prettify(LanguageKeyHandler.convertToName(translatable.key())))) + languageKeyHandler.convertLanguageKeyToDisplayName(translatable.key()))) .append(Component.text(")"))); totalPrettyName.append( serializer.serialize(temp.build())); } - else if (!LanguageKeyHandler.isKeyForEntityKilledByArg(translatable.key())) { + else if (!LanguageKeyHandler.isCustomKeyForEntityKilledByArg(translatable.key())) { totalPrettyName.append( - LanguageKeyHandler.getStatKeyTranslation( + languageKeyHandler.convertLanguageKeyToDisplayName( translatable.key())); } } }); } - else if (trans.key().startsWith("stat")) { - return LanguageKeyHandler.getStatKeyTranslation(trans.key()); - } else { - return StringUtils.prettify(LanguageKeyHandler.convertToName(trans.key())); + return languageKeyHandler.convertLanguageKeyToDisplayName(trans.key()); } return totalPrettyName.toString(); }) @@ -80,7 +83,8 @@ else if (trans.key().startsWith("stat")) { return serializer.toBuilder().flattener(flattener).build(); } - private static LegacyComponentSerializer getTextComponentSerializer() { + @Contract(" -> new") + private static @NotNull LegacyComponentSerializer getTextComponentSerializer() { return LegacyComponentSerializer .builder() .hexColors() diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/EasterEggProvider.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/EasterEggProvider.java similarity index 91% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/EasterEggProvider.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/EasterEggProvider.java index 34cfa24..3a3c510 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/EasterEggProvider.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/EasterEggProvider.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.msg.msgutils; +package com.artemis.the.gr8.playerstats.core.msg.msgutils; import me.clip.placeholderapi.PlaceholderAPI; import net.kyori.adventure.text.Component; @@ -8,7 +8,9 @@ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.bukkit.entity.Player; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Random; @@ -19,26 +21,13 @@ */ public final class EasterEggProvider { - private static boolean isEnabled; private static final Random random; - static{ - enable(); + static { random = new Random(); } - public static void enable() { - isEnabled = true; - } - public static void disable() { - isEnabled = false; - } - - public static Component getPlayerName(Player player) { - if (!isEnabled) { - return null; - } - + public static @Nullable Component getPlayerName(@NotNull Player player) { int sillyNumber = getSillyNumber(); String playerName = null; switch (player.getUniqueId().toString()) { @@ -117,7 +106,8 @@ private static boolean sillyNumberIsBetween(int sillyNumber, int lowerBound, int return sillyNumber >= lowerBound && sillyNumber <= upperBound; } - private static TagResolver papiTag(final @NotNull Player player) { + @Contract("_ -> new") + private static @NotNull TagResolver papiTag(final @NotNull Player player) { return TagResolver.resolver("papi", (argumentQueue, context) -> { final String papiPlaceholder = argumentQueue.popOr("papi tag requires an argument").value(); final String parsedPlaceholder = PlaceholderAPI.setPlaceholders(player, '%' + papiPlaceholder + '%'); diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/FontUtils.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/FontUtils.java similarity index 92% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/FontUtils.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/FontUtils.java index 8d3cc6c..17dae0b 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/FontUtils.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/FontUtils.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.msg.msgutils; +package com.artemis.the.gr8.playerstats.core.msg.msgutils; import org.bukkit.map.MinecraftFont; diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/FormattingFunction.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/FormattingFunction.java new file mode 100644 index 0000000..b368003 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/FormattingFunction.java @@ -0,0 +1,31 @@ +package com.artemis.the.gr8.playerstats.core.msg.msgutils; + +import net.kyori.adventure.text.TextComponent; +import org.bukkit.command.CommandSender; + +import java.util.function.BiFunction; + +public final class FormattingFunction { + + private final BiFunction formattingFunction; + + public FormattingFunction(BiFunction formattingFunction) { + this.formattingFunction = formattingFunction; + } + + public TextComponent getResultWithShareButton(Integer shareCode) { + return this.apply(shareCode, null); + } + + public TextComponent getResultWithSharerName(CommandSender sender) { + return this.apply(null, sender); + } + + public TextComponent getDefaultResult() { + return this.apply(null, null); + } + + private TextComponent apply(Integer shareCode, CommandSender sender) { + return formattingFunction.apply(shareCode, sender); + } +} diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/LanguageKeyHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/LanguageKeyHandler.java similarity index 62% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/LanguageKeyHandler.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/LanguageKeyHandler.java index 59bc63b..2fb6c3a 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/LanguageKeyHandler.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/LanguageKeyHandler.java @@ -1,73 +1,70 @@ -package com.artemis.the.gr8.playerstats.msg.msgutils; +package com.artemis.the.gr8.playerstats.core.msg.msgutils; -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.utils.EnumHandler; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import com.artemis.the.gr8.playerstats.enums.Unit; +import com.artemis.the.gr8.playerstats.core.utils.EnumHandler; +import com.artemis.the.gr8.playerstats.core.utils.FileHandler; +import com.artemis.the.gr8.playerstats.api.enums.Unit; import org.bukkit.Material; import org.bukkit.Statistic; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.entity.EntityType; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.ApiStatus.Internal; -import java.io.File; import java.util.Arrays; import java.util.HashMap; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * * A utility class that provides language keys to be * put in a TranslatableComponent. */ -public final class LanguageKeyHandler { +public final class LanguageKeyHandler extends FileHandler { - private static Main plugin; - private static HashMap statNameKeys; - private static File languageKeyFile; - private static FileConfiguration languageKeys; + private static volatile LanguageKeyHandler instance; + private static HashMap statisticKeys; + private final Pattern subStatKey; - /** - * Since this class uses a file to get the English translations - * of languageKeys, it needs an instance of the PlayerStats - * plugin to get access to this file. - * - * @param plugin an instance of PlayerStats' Main class - */ - public LanguageKeyHandler(Main plugin) { - LanguageKeyHandler.plugin = plugin; - statNameKeys = generateStatNameKeys(); - loadFile(); + private LanguageKeyHandler() { + super("language.yml"); + statisticKeys = generateStatisticKeys(); + subStatKey = Pattern.compile("(item|entity|block)\\.minecraft\\."); } - private static void loadFile() { - languageKeyFile = new File(plugin.getDataFolder(), "language.yml"); - if (!languageKeyFile.exists()) { - plugin.saveResource("language.yml", false); + public static LanguageKeyHandler getInstance() { + LanguageKeyHandler localVar = instance; + if (localVar != null) { + return localVar; } - languageKeys = YamlConfiguration.loadConfiguration(languageKeyFile); - } - @Internal - public static void reloadFile() { - if (!languageKeyFile.exists()) { - loadFile(); - } else { - languageKeys = YamlConfiguration.loadConfiguration(languageKeyFile); - MyLogger.logLowLevelMsg("Language file reloaded!"); + synchronized (LanguageKeyHandler.class) { + if (instance == null) { + instance = new LanguageKeyHandler(); + } + return instance; } } + @Contract(pure = true) + public @NotNull String getKeyForBlockUnit() { + return "soundCategory.block"; + } + + @Contract(pure = true) + public static boolean isEntityKey(@NotNull String key) { + return key.contains("entity.minecraft"); + } + /** * Checks if a given Key is the language key "stat_type.minecraft.killed". * * @param statKey the Key to check * @return true if this Key is key for kill-entity */ - public static boolean isKeyForKillEntity(String statKey) { + @Contract(pure = true) + public static boolean isNormalKeyForKillEntity(@NotNull String statKey) { return statKey.equalsIgnoreCase("stat_type.minecraft.killed"); } @@ -77,7 +74,8 @@ public static boolean isKeyForKillEntity(String statKey) { * @param statKey the Key to check * @return true if this Key is key for commands.kill.success.single */ - public static boolean isAlternativeKeyForKillEntity(String statKey) { + @Contract(pure = true) + public static boolean isCustomKeyForKillEntity(@NotNull String statKey) { return statKey.equalsIgnoreCase("commands.kill.success.single"); } @@ -86,7 +84,8 @@ public static boolean isAlternativeKeyForKillEntity(String statKey) { * * @return the key "commands.kill.success.single", which results in "Killed %s" */ - public static String getAlternativeKeyForKillEntity() { + @Contract(pure = true) + public static @NotNull String getCustomKeyForKillEntity() { return "commands.kill.success.single"; } @@ -96,27 +95,19 @@ public static String getAlternativeKeyForKillEntity() { * @param statKey the Key to check * @return true if this Key is a key for entity-killed-by */ - public static boolean isKeyForEntityKilledBy(String statKey) { + @Contract(pure = true) + public static boolean isNormalKeyForEntityKilledBy(@NotNull String statKey) { return statKey.equalsIgnoreCase("stat_type.minecraft.killed_by"); } /** - * Checks if a given Key is the language key "stat.minecraft.deaths". + * Checks if a given Key is the language key "subtitles.entity.generic.death". * @param statKey the Key to check - * @return true if this Key is key for stat.minecraft.deaths + * @return true if this Key is key for subtitles.entity.generic.death */ - public static boolean isAlternativeKeyForEntityKilledBy(String statKey) { - return statKey.equalsIgnoreCase("stat.minecraft.deaths"); - } - - /** - * Returns a language key to replace the default stat_type.minecraft.killed_by key. - * - * @return the key "stat.minecraft.deaths", which results in "Number of Deaths" - * (meant to be followed by {@link #getAlternativeKeyForEntityKilledByArg()}) - */ - public static String getAlternativeKeyForEntityKilledBy() { - return "stat.minecraft.deaths"; + @Contract(pure = true) + public static boolean isCustomKeyForEntityKilledBy(@NotNull String statKey) { + return statKey.equalsIgnoreCase("subtitles.entity.generic.death"); } /** @@ -126,83 +117,88 @@ public static String getAlternativeKeyForEntityKilledBy() { * @param statKey the Key to Check * @return true if this Key is the key for book.byAuthor */ - public static boolean isKeyForEntityKilledByArg(String statKey) { + @Contract(pure = true) + public static boolean isCustomKeyForEntityKilledByArg(@NotNull String statKey) { return statKey.equalsIgnoreCase("book.byAuthor"); } /** - * Returns a language key to complete the alternative key for Statistic.Entity_Killed_By. + * Returns a language key to replace the default stat_type.minecraft.killed_by key. * - * @return the key "book.byAuthor", which results in "by %". If used after - * {@link #getAlternativeKeyForEntityKilledBy()}, you will get "Number of Deaths" "by %s" + * @return the key "subtitles.entity.generic.death", which results in "Dying" + * (meant to be followed by {@link #getCustomKeyForEntityKilledByArg()}) */ - public static String getAlternativeKeyForEntityKilledByArg() { - return "book.byAuthor"; + @Contract(pure = true) + public static @NotNull String getCustomKeyForEntityKilledBy() { + return "subtitles.entity.generic.death"; } /** - * @param key the String to turn into a normal name - * @return a pretty name + * Returns a language key to complete the alternative key for statistic.entity_killed_by. + * + * @return the key "book.byAuthor", which results in "by %". If used after + * {@link #getCustomKeyForEntityKilledBy()}, you will get "Dying" "by %s" */ - public static String convertToName(String key) { - if (key.equalsIgnoreCase("soundCategory.block")) { + @Contract(pure = true) + public static @NotNull String getCustomKeyForEntityKilledByArg() { + return "book.byAuthor"; + } + + public String convertLanguageKeyToDisplayName(String key) { + if (key == null) return null; + if (isStatKey(key)) { + return getStatKeyTranslation(key); + } + else if (key.equalsIgnoreCase(getKeyForBlockUnit())) { return Unit.BLOCK.getLabel(); - } else if (isKeyForKillEntity(key)) { - return "times_killed"; - } else if (isKeyForEntityKilledBy(key)) { - return "number_of_times_killed_by"; - } else if (isKeyForEntityKilledByArg(key)) { //this one returns nothing, because it's an extra key I added - return ""; //to make the TranslatableComponent work } - String toReplace = ""; - if (key.contains("stat")) { - if (key.contains("type")) { - toReplace = "stat_type"; - } else { - toReplace = "stat"; - } - } else if (key.contains("entity")) { //for the two entity-related ones, put brackets around it to - toReplace = "entity"; //make up for the multiple-keys/args-serializer issues - } else if (key.contains("block")) { - toReplace = "block"; - } else if (key.contains("item")) { - toReplace = "item"; + + Matcher matcher = subStatKey.matcher(key); + if (matcher.find()) { + String rawName = matcher.replaceFirst(""); + return StringUtils.prettify(rawName); + } + return key; + } + + private boolean isStatKey(@NotNull String key) { + return (key.contains("stat") || + isCustomKeyForKillEntity(key) || + isCustomKeyForEntityKilledBy(key) || + isCustomKeyForEntityKilledByArg(key)); + } + + private String getStatKeyTranslation(String statKey) { + String realKey = convertToNormalStatKey(statKey); + if (realKey == null) { + return ""; } - toReplace = toReplace + ".minecraft."; - return key.replace(toReplace, ""); + return super.getFileConfiguration().getString(realKey); } private static @Nullable String convertToNormalStatKey(String statKey) { - if (isKeyForKillEntity(statKey)) { + if (isCustomKeyForKillEntity(statKey)) { return "stat_type.minecraft.killed"; - } else if (isKeyForEntityKilledBy(statKey)) { + } else if (isCustomKeyForEntityKilledBy(statKey)) { return "stat_type.minecraft.killed_by"; - } else if (isKeyForEntityKilledByArg(statKey)) { + } else if (isCustomKeyForEntityKilledByArg(statKey)) { return null; } else { return statKey; } } - public static String getStatKeyTranslation(String statKey) { - String realKey = convertToNormalStatKey(statKey); - if (realKey == null) { - return ""; - } - return languageKeys.getString(realKey); - } - /** * @param statistic the Statistic to get the Key for * @return the official Key from the NameSpacedKey for this Statistic, * or return null if no enum constant can be retrieved. */ - public String getStatKey(@NotNull Statistic statistic) { + public @NotNull String getStatKey(@NotNull Statistic statistic) { if (statistic.getType() == Statistic.Type.UNTYPED) { - return "stat.minecraft." + statNameKeys.get(statistic); + return "stat.minecraft." + statisticKeys.get(statistic); } else { - return "stat_type.minecraft." + statNameKeys.get(statistic); + return "stat_type.minecraft." + statisticKeys.get(statistic); } } @@ -242,7 +238,7 @@ else if (item.isBlock()) { if (block == null) return null; else if (block.toString().toLowerCase(Locale.ENGLISH).contains("wall_banner")) { //replace wall_banner with regular banner, since there is no key for wall banners String blockName = block.toString().toLowerCase(Locale.ENGLISH).replace("wall_", ""); - Material newBlock = EnumHandler.getBlockEnum(blockName); + Material newBlock = EnumHandler.getInstance().getBlockEnum(blockName); return (newBlock != null) ? "block.minecraft." + newBlock.getKey().getKey() : null; } else { @@ -262,7 +258,7 @@ else if (block.toString().toLowerCase(Locale.ENGLISH).contains("wall_banner")) { } } - private @NotNull HashMap generateStatNameKeys() { + private @NotNull HashMap generateStatisticKeys() { //get the enum names for all statistics first HashMap statNames = new HashMap<>(Statistic.values().length); Arrays.stream(Statistic.values()).forEach(statistic -> statNames.put(statistic, statistic.toString().toLowerCase(Locale.ENGLISH))); diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/NumberFormatter.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/NumberFormatter.java similarity index 83% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/NumberFormatter.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/NumberFormatter.java index 4f8c79c..09c4d29 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/NumberFormatter.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/NumberFormatter.java @@ -1,6 +1,8 @@ -package com.artemis.the.gr8.playerstats.msg.msgutils; +package com.artemis.the.gr8.playerstats.core.msg.msgutils; -import com.artemis.the.gr8.playerstats.enums.Unit; +import com.artemis.the.gr8.playerstats.api.StatNumberFormatter; +import com.artemis.the.gr8.playerstats.api.enums.Unit; +import org.jetbrains.annotations.NotNull; import java.text.DecimalFormat; @@ -10,7 +12,7 @@ * that are easier to understand (for example: from ticks to hours) and adds commas * to break up large numbers. */ -public final class NumberFormatter { +public final class NumberFormatter implements StatNumberFormatter { private final DecimalFormat format; @@ -21,11 +23,10 @@ public NumberFormatter() { } /** - * Turns the input number into a more readable format depending on its type - * (number-of-times, time-, damage- or distance-based) according to the - * corresponding config settings, and adds commas in groups of 3. + * Adds commas in groups of 3. */ - public String formatNumber(long number) { + @Override + public @NotNull String formatDefaultNumber(long number) { return format.format(number); } @@ -33,7 +34,8 @@ public String formatNumber(long number) { * The unit of damage-based statistics is half a heart by default. * This method turns the number into hearts. */ - public String formatDamageNumber(long number, Unit statUnit) { //7 statistics + @Override + public @NotNull String formatDamageNumber(long number, @NotNull Unit statUnit) { //7 statistics if (statUnit == Unit.HEART) { return format.format(Math.round(number / 2.0)); } else { @@ -47,7 +49,8 @@ public String formatDamageNumber(long number, Unit statUnit) { //7 statistics * and turns it into km or leaves it as cm otherwise, * depending on the config settings. */ - public String formatDistanceNumber(long number, Unit statUnit) { //15 statistics + @Override + public @NotNull String formatDistanceNumber(long number, @NotNull Unit statUnit) { //15 statistics switch (statUnit) { case CM -> { return format.format(number); @@ -69,6 +72,7 @@ public String formatDistanceNumber(long number, Unit statUnit) { //15 statistic * @return a String with the form "1D 2H 3M 4S" * (depending on the Unit range selected) */ + @Override public String formatTimeNumber(long number, Unit biggestUnit, Unit smallestUnit) { //5 statistics if (number <= 0) { return "-"; @@ -83,9 +87,9 @@ public String formatTimeNumber(long number, Unit biggestUnit, Unit smallestUnit) while(currUnit != null){ //Define amount of units - int amount = 0; + int amount; - //Current unit is equal to smallest unit, in this case round the remainder + //Current unit is equal to the smallest unit, in this case round the remainder if(currUnit == smallestUnit){ amount = (int) Math.round(leftoverSeconds / currUnit.getSeconds()); } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/StringUtils.java b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/StringUtils.java similarity index 50% rename from src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/StringUtils.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/StringUtils.java index 79cc7cf..017385d 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/StringUtils.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/msg/msgutils/StringUtils.java @@ -1,8 +1,10 @@ -package com.artemis.the.gr8.playerstats.msg.msgutils; +package com.artemis.the.gr8.playerstats.core.msg.msgutils; -import com.artemis.the.gr8.playerstats.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A small utility class that helps make enum constant @@ -10,6 +12,12 @@ */ public final class StringUtils { + private static final Pattern lowercaseLetterAfterSpace; + + static { + lowercaseLetterAfterSpace = Pattern.compile("(?<= )[a-z]"); + } + private StringUtils() { } @@ -20,14 +28,22 @@ private StringUtils() { */ public static String prettify(String input) { if (input == null) return null; + MyLogger.logHighLevelMsg("Prettifying [" + input + "]"); + StringBuilder capitals = new StringBuilder(input.toLowerCase(Locale.ENGLISH)); capitals.setCharAt(0, Character.toUpperCase(capitals.charAt(0))); - while (capitals.indexOf("_") != -1) { - MyLogger.logHighLevelMsg("Replacing underscores and capitalizing names..."); + while (capitals.indexOf("_") != -1) { int index = capitals.indexOf("_"); - capitals.setCharAt(index + 1, Character.toUpperCase(capitals.charAt(index + 1))); capitals.setCharAt(index, ' '); + MyLogger.logHighLevelMsg("Replacing underscores: " + capitals); + } + + Matcher matcher = lowercaseLetterAfterSpace.matcher(capitals); + while (matcher.find()) { + int index = matcher.start(); + capitals.setCharAt(index, Character.toUpperCase(capitals.charAt(index))); + MyLogger.logHighLevelMsg("Capitalizing names: " + capitals); } return capitals.toString(); } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/reload/ReloadAction.java b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/PlayerLoadAction.java similarity index 55% rename from src/main/java/com/artemis/the/gr8/playerstats/reload/ReloadAction.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/PlayerLoadAction.java index 4ae0515..603fd95 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/reload/ReloadAction.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/PlayerLoadAction.java @@ -1,9 +1,9 @@ -package com.artemis.the.gr8.playerstats.reload; +package com.artemis.the.gr8.playerstats.core.multithreading; -import com.artemis.the.gr8.playerstats.ThreadManager; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import com.artemis.the.gr8.playerstats.utils.UnixTimeHandler; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import com.artemis.the.gr8.playerstats.core.utils.UnixTimeHandler; import org.bukkit.OfflinePlayer; import java.util.UUID; @@ -13,7 +13,7 @@ /** * The action that is executed when a reload-command is triggered. */ -final class ReloadAction extends RecursiveAction { +final class PlayerLoadAction extends RecursiveAction { private static int threshold; @@ -21,33 +21,26 @@ final class ReloadAction extends RecursiveAction { private final int start; private final int end; - private final int lastPlayedLimit; private final ConcurrentHashMap offlinePlayerUUIDs; /** * Fills a ConcurrentHashMap with PlayerNames and UUIDs for all OfflinePlayers * that should be included in statistic calculations. * - * @param players array of all OfflinePlayers (straight from Bukkit) - * @param lastPlayedLimit whether to set a limit based on last-played-date + * @param players array of all OfflinePlayers to filter and load * @param offlinePlayerUUIDs the ConcurrentHashMap to put playerNames and UUIDs in * @see OfflinePlayerHandler */ - public ReloadAction(OfflinePlayer[] players, - int lastPlayedLimit, ConcurrentHashMap offlinePlayerUUIDs) { - - this(players, 0, players.length, lastPlayedLimit, offlinePlayerUUIDs); + public PlayerLoadAction(OfflinePlayer[] players, ConcurrentHashMap offlinePlayerUUIDs) { + this(players, 0, players.length, offlinePlayerUUIDs); } - private ReloadAction(OfflinePlayer[] players, int start, int end, - int lastPlayedLimit, ConcurrentHashMap offlinePlayerUUIDs) { + private PlayerLoadAction(OfflinePlayer[] players, int start, int end, ConcurrentHashMap offlinePlayerUUIDs) { threshold = ThreadManager.getTaskThreshold(); this.players = players; this.start = start; this.end = end; - - this.lastPlayedLimit = lastPlayedLimit; this.offlinePlayerUUIDs = offlinePlayerUUIDs; MyLogger.subActionCreated(Thread.currentThread().getName()); @@ -61,10 +54,10 @@ protected void compute() { } else { final int split = length / 2; - final ReloadAction subTask1 = new ReloadAction(players, start, (start + split), - lastPlayedLimit, offlinePlayerUUIDs); - final ReloadAction subTask2 = new ReloadAction(players, (start + split), end, - lastPlayedLimit, offlinePlayerUUIDs); + final PlayerLoadAction subTask1 = new PlayerLoadAction(players, start, (start + split), + offlinePlayerUUIDs); + final PlayerLoadAction subTask2 = new PlayerLoadAction(players, (start + split), end, + offlinePlayerUUIDs); //queue and compute all subtasks in the right order invokeAll(subTask1, subTask2); @@ -72,12 +65,16 @@ protected void compute() { } private void process() { + OfflinePlayerHandler offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + int lastPlayedLimit = ConfigHandler.getInstance().getLastPlayedLimit(); + for (int i = start; i < end; i++) { OfflinePlayer player = players[i]; String playerName = player.getName(); MyLogger.actionRunning(Thread.currentThread().getName()); if (playerName != null && - (lastPlayedLimit == 0 || UnixTimeHandler.hasPlayedSince(lastPlayedLimit, player.getLastPlayed()))) { + !offlinePlayerHandler.isExcludedPlayer(player.getUniqueId()) && + UnixTimeHandler.hasPlayedSince(lastPlayedLimit, player.getLastPlayed())) { offlinePlayerUUIDs.put(playerName, player.getUniqueId()); } } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/ReloadThread.java b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/ReloadThread.java new file mode 100644 index 0000000..4ff2ca5 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/ReloadThread.java @@ -0,0 +1,55 @@ +package com.artemis.the.gr8.playerstats.core.multithreading; + +import com.artemis.the.gr8.playerstats.core.Main; +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +/** The Thread that is in charge of reloading PlayerStats. */ +final class ReloadThread extends Thread { + + private final Main main; + private static OutputManager outputManager; + + private final StatThread statThread; + private final CommandSender sender; + + public ReloadThread(Main main, OutputManager m, int ID, @Nullable StatThread s, @Nullable CommandSender se) { + this.main = main; + outputManager = m; + + statThread = s; + sender = se; + + this.setName("ReloadThread-" + ID); + MyLogger.logHighLevelMsg(this.getName() + " created!"); + } + + /** + * This method will call reload() from Main. If a {@link StatThread} + * is still running, it will join the statThread and wait for it to finish. + */ + @Override + public void run() { + MyLogger.logHighLevelMsg(this.getName() + " started!"); + + if (statThread != null && statThread.isAlive()) { + try { + MyLogger.logLowLevelMsg(this.getName() + ": Waiting for " + statThread.getName() + " to finish up..."); + statThread.join(); + } catch (InterruptedException e) { + MyLogger.logException(e, "ReloadThread", "run(), trying to join " + statThread.getName()); + throw new RuntimeException(e); + } + } + + MyLogger.logLowLevelMsg("Reloading!"); + main.reloadPlugin(); + + if (sender != null) { + outputManager.sendFeedbackMsg(sender, StandardMessage.RELOADED_CONFIG); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatAction.java b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatAction.java similarity index 70% rename from src/main/java/com/artemis/the/gr8/playerstats/statistic/StatAction.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatAction.java index b11ce4f..f276bb4 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatAction.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatAction.java @@ -1,9 +1,8 @@ -package com.artemis.the.gr8.playerstats.statistic; +package com.artemis.the.gr8.playerstats.core.multithreading; -import com.artemis.the.gr8.playerstats.ThreadManager; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import com.artemis.the.gr8.playerstats.utils.MyLogger; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; import com.google.common.collect.ImmutableList; import org.bukkit.OfflinePlayer; @@ -17,10 +16,8 @@ final class StatAction extends RecursiveTask> { private static int threshold; - - private final OfflinePlayerHandler offlinePlayerHandler; private final ImmutableList playerNames; - private final RequestSettings requestSettings; + private final StatRequest.Settings requestSettings; private final ConcurrentHashMap allStats; /** @@ -29,15 +26,13 @@ final class StatAction extends RecursiveTask> * ForkJoinPool, and returns the ConcurrentHashMap when * everything is done. * - * @param offlinePlayerHandler the OfflinePlayerHandler to convert playerNames into Players * @param playerNames ImmutableList of playerNames for players that should be included in stat calculations * @param requestSettings a validated requestSettings object * @param allStats the ConcurrentHashMap to put the results on */ - public StatAction(OfflinePlayerHandler offlinePlayerHandler, ImmutableList playerNames, RequestSettings requestSettings, ConcurrentHashMap allStats) { + public StatAction(ImmutableList playerNames, StatRequest.Settings requestSettings, ConcurrentHashMap allStats) { threshold = ThreadManager.getTaskThreshold(); - this.offlinePlayerHandler = offlinePlayerHandler; this.playerNames = playerNames; this.requestSettings = requestSettings; this.allStats = allStats; @@ -51,8 +46,8 @@ protected ConcurrentHashMap compute() { return getStatsDirectly(); } else { - final StatAction subTask1 = new StatAction(offlinePlayerHandler, playerNames.subList(0, playerNames.size()/2), requestSettings, allStats); - final StatAction subTask2 = new StatAction(offlinePlayerHandler, playerNames.subList(playerNames.size()/2, playerNames.size()), requestSettings, allStats); + final StatAction subTask1 = new StatAction(playerNames.subList(0, playerNames.size()/2), requestSettings, allStats); + final StatAction subTask2 = new StatAction(playerNames.subList(playerNames.size()/2, playerNames.size()), requestSettings, allStats); //queue and compute all subtasks in the right order subTask1.fork(); @@ -62,12 +57,14 @@ protected ConcurrentHashMap compute() { } private ConcurrentHashMap getStatsDirectly() { + OfflinePlayerHandler offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + Iterator iterator = playerNames.iterator(); if (iterator.hasNext()) { do { String playerName = iterator.next(); MyLogger.actionRunning(Thread.currentThread().getName()); - OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName); + OfflinePlayer player = offlinePlayerHandler.getIncludedOfflinePlayer(playerName); int statistic = 0; switch (requestSettings.getStatistic().getType()) { case UNTYPED -> statistic = player.getStatistic(requestSettings.getStatistic()); diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java new file mode 100644 index 0000000..0c46665 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java @@ -0,0 +1,67 @@ +package com.artemis.the.gr8.playerstats.core.multithreading; + +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.statrequest.RequestManager; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.api.StatResult; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * The Thread that is in charge of getting and calculating statistics. + */ +final class StatThread extends Thread { + + private static OutputManager outputManager; + + private final ReloadThread reloadThread; + private final StatRequest statRequest; + + public StatThread(OutputManager m, int ID, StatRequest s, @Nullable ReloadThread r) { + outputManager = m; + reloadThread = r; + statRequest = s; + + this.setName("StatThread-" + statRequest.getSettings().getCommandSender().getName() + "-" + ID); + MyLogger.logHighLevelMsg(this.getName() + " created!"); + } + + @Override + public void run() throws IllegalStateException { + MyLogger.logHighLevelMsg(this.getName() + " started!"); + CommandSender statRequester = statRequest.getSettings().getCommandSender(); + + if (reloadThread != null && reloadThread.isAlive()) { + try { + MyLogger.logLowLevelMsg(this.getName() + ": Waiting for " + reloadThread.getName() + " to finish up..."); + outputManager.sendFeedbackMsg(statRequester, StandardMessage.STILL_RELOADING); + reloadThread.join(); + + } catch (InterruptedException e) { + MyLogger.logException(e, "StatThread", "Trying to join " + reloadThread.getName()); + throw new RuntimeException(e); + } + } + + long lastCalc = ThreadManager.getLastRecordedCalcTime(); + if (lastCalc > 6000) { + outputManager.sendFeedbackMsg(statRequester, StandardMessage.WAIT_A_MINUTE); + } else if (lastCalc > 2000) { + outputManager.sendFeedbackMsg(statRequester, StandardMessage.WAIT_A_MOMENT); + } + + try { + StatResult result = RequestManager.execute(statRequest); + outputManager.sendToCommandSender(statRequester, result.formattedComponent()); + } + catch (ConcurrentModificationException e) { + if (!statRequest.getSettings().isConsoleSender()) { + outputManager.sendFeedbackMsg(statRequester, StandardMessage.UNKNOWN_ERROR); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/ThreadManager.java b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/ThreadManager.java new file mode 100644 index 0000000..9acbe7e --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/ThreadManager.java @@ -0,0 +1,125 @@ +package com.artemis.the.gr8.playerstats.core.multithreading; + +import com.artemis.the.gr8.playerstats.core.Main; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.enums.StandardMessage; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import com.google.common.collect.ImmutableList; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The ThreadManager is in charge of the Threads that PlayerStats + * can utilize. It keeps track of past and currently active Threads, + * to ensure a Player cannot start multiple Threads at the same time + * (thereby limiting them to one stat-lookup at a time). It also + * passes appropriate references along to the {@link StatThread} + * or {@link ReloadThread}, to ensure those will never run at the + * same time. + */ +public final class ThreadManager { + + private final static int threshold = 10; + private int statThreadID; + private int reloadThreadID; + + private final Main main; + private final ConfigHandler config; + private static OutputManager outputManager; + + private ReloadThread activatedReloadThread; + private StatThread activatedStatThread; + private final HashMap statThreads; + private static long lastRecordedCalcTime; + + public ThreadManager(Main main, OutputManager outputManager) { + this.main = main; + this.config = ConfigHandler.getInstance(); + ThreadManager.outputManager = outputManager; + + statThreads = new HashMap<>(); + statThreadID = 0; + reloadThreadID = 0; + lastRecordedCalcTime = 0; + } + + static int getTaskThreshold() { + return threshold; + } + + public static @NotNull StatAction getStatAction(StatRequest.Settings requestSettings) { + OfflinePlayerHandler offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + + ImmutableList relevantPlayerNames = ImmutableList.copyOf(offlinePlayerHandler.getIncludedOfflinePlayerNames()); + ConcurrentHashMap resultingStatNumbers = new ConcurrentHashMap<>(relevantPlayerNames.size()); + StatAction task = new StatAction(relevantPlayerNames, requestSettings, resultingStatNumbers); + + MyLogger.actionCreated(relevantPlayerNames.size()); + return task; + } + + public static @NotNull PlayerLoadAction getPlayerLoadAction(OfflinePlayer[] playersToLoad, ConcurrentHashMap mapToFill) { + PlayerLoadAction task = new PlayerLoadAction(playersToLoad, mapToFill); + MyLogger.actionCreated(playersToLoad != null ? playersToLoad.length : 0); + return task; + } + + public void startReloadThread(CommandSender sender) { + if (activatedReloadThread == null || !activatedReloadThread.isAlive()) { + reloadThreadID += 1; + + activatedReloadThread = new ReloadThread(main, outputManager, reloadThreadID, activatedStatThread, sender); + activatedReloadThread.start(); + } + else { + MyLogger.logLowLevelMsg("Another reloadThread is already running! (" + activatedReloadThread.getName() + ")"); + } + } + + public void startStatThread(@NotNull StatRequest request) { + statThreadID += 1; + CommandSender sender = request.getSettings().getCommandSender(); + + if (config.limitStatRequests() && statThreads.containsKey(sender.getName())) { + Thread runningThread = statThreads.get(sender.getName()); + + if (runningThread.isAlive()) { + outputManager.sendFeedbackMsg(sender, StandardMessage.REQUEST_ALREADY_RUNNING); + } else { + startNewStatThread(request); + } + } else { + startNewStatThread(request); + } + } + + /** + * Store the duration in milliseconds of the last top-stat-lookup + * (or of loading the offline-player-list if no look-ups have been done yet). + */ + public static void recordCalcTime(long time) { + lastRecordedCalcTime = time; + } + + /** + * Returns the duration in milliseconds of the last top-stat-lookup + * (or of loading the offline-player-list if no look-ups have been done yet). + */ + public static long getLastRecordedCalcTime() { + return lastRecordedCalcTime; + } + + private void startNewStatThread(StatRequest request) { + activatedStatThread = new StatThread(outputManager, statThreadID, request, activatedReloadThread); + statThreads.put(request.getSettings().getCommandSender().getName(), activatedStatThread); + activatedStatThread.start(); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/ShareManager.java b/src/main/java/com/artemis/the/gr8/playerstats/core/sharing/ShareManager.java similarity index 75% rename from src/main/java/com/artemis/the/gr8/playerstats/ShareManager.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/sharing/ShareManager.java index 13990e4..0af4aad 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/ShareManager.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/sharing/ShareManager.java @@ -1,8 +1,7 @@ -package com.artemis.the.gr8.playerstats; +package com.artemis.the.gr8.playerstats.core.sharing; -import com.artemis.the.gr8.playerstats.statistic.result.InternalStatResult; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; import net.kyori.adventure.text.TextComponent; import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; @@ -26,30 +25,46 @@ */ public final class ShareManager { + private static volatile ShareManager instance; private static boolean isEnabled; - private static int waitingTime; + private int waitingTime; - private static volatile AtomicInteger resultID; - private static ConcurrentHashMap statResultQueue; - private static ConcurrentHashMap shareTimeStamp; - private static ArrayBlockingQueue sharedResults; + private volatile AtomicInteger NumberOfStoredResults; + private ConcurrentHashMap statResultQueue; + private ConcurrentHashMap shareTimeStamp; + private ArrayBlockingQueue sharedResults; - public ShareManager(ConfigHandler config) { - updateSettings(config); + private ShareManager() { + updateSettings(); } - public static boolean isEnabled() { + public static ShareManager getInstance() { + ShareManager localVar = instance; + if (localVar != null) { + return localVar; + } + + synchronized (ShareManager.class) { + if (instance == null) { + instance = new ShareManager(); + } + return instance; + } + } + + public boolean isEnabled() { return isEnabled; } - public static synchronized void updateSettings(ConfigHandler config) { + public void updateSettings() { + ConfigHandler config = ConfigHandler.getInstance(); isEnabled = config.allowStatSharing() && config.useHoverText(); waitingTime = config.getStatShareWaitingTime(); if (isEnabled) { sharedResults = new ArrayBlockingQueue<>(500); //reset the sharedResultsQueue - if (resultID == null) { //if we went from disabled to enabled, initialize - resultID = new AtomicInteger(); //always starts with value 0 + if (NumberOfStoredResults == null) { //if we went from disabled to enabled, initialize + NumberOfStoredResults = new AtomicInteger(); //always starts with value 0 statResultQueue = new ConcurrentHashMap<>(); shareTimeStamp = new ConcurrentHashMap<>(); } @@ -75,8 +90,7 @@ public int saveStatResult(String playerName, TextComponent statResult) { removeExcessResults(playerName); int ID = getNextIDNumber(); - //UUID shareCode = UUID.randomUUID(); - InternalStatResult result = new InternalStatResult(playerName, statResult, ID); + StoredResult result = new StoredResult(playerName, statResult, ID); int shareCode = result.hashCode(); statResultQueue.put(shareCode, result); MyLogger.logMediumLevelMsg("Saving statResults with no. " + ID); @@ -103,7 +117,7 @@ public boolean requestAlreadyShared(int shareCode) { * and returns the formattedComponent. If no formattedComponent was found, * returns null. */ - public @Nullable InternalStatResult getStatResult(String playerName, int shareCode) { + public @Nullable StoredResult getStatResult(String playerName, int shareCode) { if (statResultQueue.containsKey(shareCode)) { shareTimeStamp.put(playerName, Instant.now()); @@ -134,7 +148,7 @@ public boolean requestAlreadyShared(int shareCode) { * StatResults saved, remove the oldest one. */ private void removeExcessResults(String playerName) { - List alreadySavedResults = statResultQueue.values() + List alreadySavedResults = statResultQueue.values() .parallelStream() .filter(result -> result.executorName().equalsIgnoreCase(playerName)) .toList(); @@ -142,7 +156,7 @@ private void removeExcessResults(String playerName) { if (alreadySavedResults.size() > 25) { int hashCode = alreadySavedResults .parallelStream() - .min(Comparator.comparing(InternalStatResult::ID)) + .min(Comparator.comparing(StoredResult::ID)) .orElseThrow().hashCode(); MyLogger.logMediumLevelMsg("Removing old stat no. " + statResultQueue.get(hashCode).ID() + " for player " + playerName); statResultQueue.remove(hashCode); @@ -150,6 +164,6 @@ private void removeExcessResults(String playerName) { } private int getNextIDNumber() { - return resultID.incrementAndGet(); + return NumberOfStoredResults.incrementAndGet(); } } \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/sharing/StoredResult.java b/src/main/java/com/artemis/the/gr8/playerstats/core/sharing/StoredResult.java new file mode 100644 index 0000000..9080bd2 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/sharing/StoredResult.java @@ -0,0 +1,10 @@ +package com.artemis.the.gr8.playerstats.core.sharing; + +import net.kyori.adventure.text.TextComponent; + +/** + * This Record is used to store stat-results internally, + * so Players can share them by clicking a share-button. + */ +public record StoredResult(String executorName, TextComponent formattedValue, int ID) { +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/PlayerStatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/PlayerStatRequest.java new file mode 100644 index 0000000..17c4cd9 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/PlayerStatRequest.java @@ -0,0 +1,64 @@ +package com.artemis.the.gr8.playerstats.core.statrequest; + +import com.artemis.the.gr8.playerstats.api.RequestGenerator; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +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; + +public final class PlayerStatRequest extends StatRequest implements RequestGenerator { + + public PlayerStatRequest(String playerName) { + this(Bukkit.getConsoleSender(), playerName); + } + + public PlayerStatRequest(CommandSender sender, String playerName) { + super(sender); + super.configureForPlayer(playerName); + } + + @Override + public boolean isValid() { + if (!hasValidTarget()) { + return false; + } + return super.hasMatchingSubStat(); + } + + private boolean hasValidTarget() { + StatRequest.Settings settings = super.getSettings(); + if (settings.getPlayerName() == null) { + return false; + } + + OfflinePlayerHandler offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + if (offlinePlayerHandler.isExcludedPlayer(settings.getPlayerName())) { + return ConfigHandler.getInstance().allowPlayerLookupsForExcludedPlayers(); + } else { + return offlinePlayerHandler.isIncludedPlayer(settings.getPlayerName()); + } + } + + @Override + public StatRequest untyped(@NotNull Statistic statistic) { + super.configureUntyped(statistic); + return this; + } + + @Override + public StatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) { + super.configureBlockOrItemType(statistic, material); + return this; + } + + @Override + public StatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) { + super.configureEntityType(statistic, entityType); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/RequestManager.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/RequestManager.java new file mode 100644 index 0000000..3373cc9 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/RequestManager.java @@ -0,0 +1,202 @@ +package com.artemis.the.gr8.playerstats.core.statrequest; + +import com.artemis.the.gr8.playerstats.api.RequestGenerator; +import com.artemis.the.gr8.playerstats.api.StatManager; +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.api.StatResult; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.FormattingFunction; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; +import com.artemis.the.gr8.playerstats.core.sharing.ShareManager; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import net.kyori.adventure.text.TextComponent; +import org.bukkit.OfflinePlayer; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.stream.Collectors; + +/** + * Turns user input into a {@link StatRequest} that can be + * executed to get statistic data. + */ +public final class RequestManager implements StatManager { + + private static RequestProcessor processor; + private final OfflinePlayerHandler offlinePlayerHandler; + + public RequestManager(OutputManager outputManager) { + offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + processor = new RequestProcessor(outputManager); + } + + public static StatResult execute(@NotNull StatRequest request) { + return switch (request.getSettings().getTarget()) { + case PLAYER -> processor.processPlayerRequest(request.getSettings()); + case SERVER -> processor.processServerRequest(request.getSettings()); + case TOP -> processor.processTopRequest(request.getSettings()); + }; + } + + @Override + public boolean isExcludedPlayer(String playerName) { + return offlinePlayerHandler.isExcludedPlayer(playerName); + } + + @Contract("_ -> new") + @Override + public @NotNull RequestGenerator createPlayerStatRequest(String playerName) { + return new PlayerStatRequest(playerName); + } + + @Override + public @NotNull StatResult executePlayerStatRequest(@NotNull StatRequest request) { + return processor.processPlayerRequest(request.getSettings()); + } + + @Contract(" -> new") + @Override + public @NotNull RequestGenerator createServerStatRequest() { + return new ServerStatRequest(); + } + + @Override + public @NotNull StatResult executeServerStatRequest(@NotNull StatRequest request) { + return processor.processServerRequest(request.getSettings()); + } + + @Contract("_ -> new") + @Override + public @NotNull RequestGenerator> createTopStatRequest(int topListSize) { + return new TopStatRequest(topListSize); + } + + @Override + public @NotNull RequestGenerator> createTotalTopStatRequest() { + int playerCount = offlinePlayerHandler.getIncludedPlayerCount(); + return createTopStatRequest(playerCount); + } + + @Override + public @NotNull StatResult> executeTopRequest(@NotNull StatRequest> request) { + return processor.processTopRequest(request.getSettings()); + } + + private final class RequestProcessor { + + private static ConfigHandler config; + private static OutputManager outputManager; + private static ShareManager shareManager; + + public RequestProcessor(OutputManager outputManager) { + RequestProcessor.config = ConfigHandler.getInstance(); + RequestProcessor.outputManager = outputManager; + RequestProcessor.shareManager = ShareManager.getInstance(); + } + + public @NotNull StatResult processPlayerRequest(StatRequest.Settings requestSettings) { + int stat = getPlayerStat(requestSettings); + FormattingFunction formattingFunction = outputManager.formatPlayerStat(requestSettings, stat); + TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); + String resultAsString = outputManager.textComponentToString(formattedResult); + + return new StatResult<>(stat, formattedResult, resultAsString); + } + + public @NotNull StatResult processServerRequest(StatRequest.Settings requestSettings) { + long stat = getServerStat(requestSettings); + FormattingFunction formattingFunction = outputManager.formatServerStat(requestSettings, stat); + TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); + String resultAsString = outputManager.textComponentToString(formattedResult); + + return new StatResult<>(stat, formattedResult, resultAsString); + } + + public @NotNull StatResult> processTopRequest(StatRequest.Settings requestSettings) { + LinkedHashMap stats = getTopStats(requestSettings); + FormattingFunction formattingFunction = outputManager.formatTopStats(requestSettings, stats); + TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); + String resultAsString = outputManager.textComponentToString(formattedResult); + + return new StatResult<>(stats, formattedResult, resultAsString); + } + + private int getPlayerStat(@NotNull StatRequest.Settings requestSettings) { + OfflinePlayer player; + if (offlinePlayerHandler.isExcludedPlayer(requestSettings.getPlayerName()) && + config.allowPlayerLookupsForExcludedPlayers()) { + player = offlinePlayerHandler.getExcludedOfflinePlayer(requestSettings.getPlayerName()); + } else { + player = offlinePlayerHandler.getIncludedOfflinePlayer(requestSettings.getPlayerName()); + } + return switch (requestSettings.getStatistic().getType()) { + case UNTYPED -> player.getStatistic(requestSettings.getStatistic()); + case ENTITY -> player.getStatistic(requestSettings.getStatistic(), requestSettings.getEntity()); + case BLOCK -> player.getStatistic(requestSettings.getStatistic(), requestSettings.getBlock()); + case ITEM -> player.getStatistic(requestSettings.getStatistic(), requestSettings.getItem()); + }; + } + + private long getServerStat(StatRequest.Settings requestSettings) { + List numbers = getAllStatsAsync(requestSettings) + .values() + .parallelStream() + .toList(); + return numbers.parallelStream().mapToLong(Integer::longValue).sum(); + } + + private LinkedHashMap getTopStats(StatRequest.Settings requestSettings) { + return getAllStatsAsync(requestSettings).entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .limit(requestSettings.getTopListSize()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + + private TextComponent processFunction(CommandSender sender, FormattingFunction function) { + if (outputShouldBeStored(sender)) { + int shareCode = shareManager.saveStatResult(sender.getName(), function.getResultWithSharerName(sender)); + return function.getResultWithShareButton(shareCode); + } + return function.getDefaultResult(); + } + + private boolean outputShouldBeStored(CommandSender sender) { + return !(sender instanceof ConsoleCommandSender) && + shareManager.isEnabled() && + shareManager.senderHasPermission(sender); + } + + /** + * Invokes a bunch of worker pool threads to get the statistics for + * all players that are stored in the {@link OfflinePlayerHandler}). + */ + private @NotNull ConcurrentHashMap getAllStatsAsync(StatRequest.Settings requestSettings) { + long time = System.currentTimeMillis(); + + ForkJoinPool commonPool = ForkJoinPool.commonPool(); + ConcurrentHashMap allStats; + + try { + allStats = commonPool.invoke(ThreadManager.getStatAction(requestSettings)); + } catch (ConcurrentModificationException e) { + MyLogger.logWarning("The requestSettings could not be executed due to a ConcurrentModificationException. " + + "This likely happened because Bukkit hasn't fully initialized all player-data yet. " + + "Try again and it should be fine!"); + throw new ConcurrentModificationException(e.toString()); + } + + MyLogger.actionFinished(); + ThreadManager.recordCalcTime(System.currentTimeMillis() - time); + MyLogger.logMediumLevelTask("Calculated all stats", time); + + return allStats; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/ServerStatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/ServerStatRequest.java new file mode 100644 index 0000000..3d47394 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/ServerStatRequest.java @@ -0,0 +1,46 @@ +package com.artemis.the.gr8.playerstats.core.statrequest; + +import com.artemis.the.gr8.playerstats.api.RequestGenerator; +import com.artemis.the.gr8.playerstats.api.StatRequest; +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; + +public final class ServerStatRequest extends StatRequest implements RequestGenerator { + + + public ServerStatRequest() { + this(Bukkit.getConsoleSender()); + } + + public ServerStatRequest(CommandSender sender) { + super(sender); + super.configureForServer(); + } + + @Override + public boolean isValid() { + return super.hasMatchingSubStat(); + } + + @Override + public StatRequest untyped(@NotNull Statistic statistic) { + super.configureUntyped(statistic); + return this; + } + + @Override + public StatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) { + super.configureBlockOrItemType(statistic, material); + return this; + } + + @Override + public StatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) { + super.configureEntityType(statistic, entityType); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/TopStatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/TopStatRequest.java new file mode 100644 index 0000000..09e9f43 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statrequest/TopStatRequest.java @@ -0,0 +1,47 @@ +package com.artemis.the.gr8.playerstats.core.statrequest; + +import com.artemis.the.gr8.playerstats.api.RequestGenerator; +import com.artemis.the.gr8.playerstats.api.StatRequest; +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 java.util.LinkedHashMap; + +public final class TopStatRequest extends StatRequest> implements RequestGenerator> { + + public TopStatRequest(int topListSize) { + this(Bukkit.getConsoleSender(), topListSize); + } + + public TopStatRequest(CommandSender sender, int topListSize) { + super(sender); + super.configureForTop(topListSize); + } + + @Override + public boolean isValid() { + return super.hasMatchingSubStat(); + } + + @Override + public StatRequest> untyped(@NotNull Statistic statistic) { + super.configureUntyped(statistic); + return this; + } + + @Override + public StatRequest> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) { + super.configureBlockOrItemType(statistic, material); + return this; + } + + @Override + public StatRequest> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) { + super.configureEntityType(statistic, entityType); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/utils/EnumHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/EnumHandler.java similarity index 85% rename from src/main/java/com/artemis/the/gr8/playerstats/utils/EnumHandler.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/utils/EnumHandler.java index 617f5a7..44172dc 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/utils/EnumHandler.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/EnumHandler.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.utils; +package com.artemis.the.gr8.playerstats.core.utils; import org.bukkit.Material; import org.bukkit.Statistic; @@ -22,21 +22,36 @@ */ public final class EnumHandler { + private static volatile EnumHandler instance; private static List blockNames; private static List itemNames; private static List statNames; private static List subStatNames; - public EnumHandler() { + private EnumHandler() { prepareLists(); } + public static EnumHandler getInstance() { + EnumHandler localVar = instance; + if (localVar != null) { + return localVar; + } + + synchronized (EnumHandler.class) { + if (instance == null) { + instance = new EnumHandler(); + } + return instance; + } + } + /** * Returns all block-names in lowercase. * * @return the List */ - public List getBlockNames() { + public List getAllBlockNames() { return blockNames; } @@ -45,7 +60,7 @@ public List getBlockNames() { * * @return the List */ - public List getItemNames() { + public List getAllItemNames() { return itemNames; } @@ -54,7 +69,7 @@ public List getItemNames() { * * @return the List */ - public List getStatNames() { + public List getAllStatNames() { return statNames; } @@ -65,7 +80,7 @@ public List getStatNames() { * @return Material enum constant (uppercase), or null if none * can be found */ - public static @Nullable Material getItemEnum(String itemName) { + public @Nullable Material getItemEnum(String itemName) { if (itemName == null) return null; Material item = Material.matchMaterial(itemName); @@ -79,7 +94,7 @@ public List getStatNames() { * @return EntityType enum constant (uppercase), or null if none * can be found */ - public static @Nullable EntityType getEntityEnum(String entityName) { + public @Nullable EntityType getEntityEnum(String entityName) { try { return EntityType.valueOf(entityName.toUpperCase(Locale.ENGLISH)); } @@ -95,7 +110,7 @@ public List getStatNames() { * @return Material enum constant (uppercase), or null if none * can be found */ - public static @Nullable Material getBlockEnum(String materialName) { + public @Nullable Material getBlockEnum(String materialName) { if (materialName == null) return null; Material block = Material.matchMaterial(materialName); @@ -108,7 +123,7 @@ public List getStatNames() { * @param statName String (case-insensitive) * @return the Statistic enum constant, or null */ - public static @Nullable Statistic getStatEnum(@NotNull String statName) { + public @Nullable Statistic getStatEnum(@NotNull String statName) { try { return Statistic.valueOf(statName.toUpperCase(Locale.ENGLISH)); } @@ -134,7 +149,7 @@ public boolean isStatistic(@NotNull String statName) { * @param statName the String to check (case-insensitive) * @return true if this String is a Statistic of Type.Entity */ - public boolean isEntityStatistic(String statName) { + public boolean isEntityStatistic(@NotNull String statName) { return statName.equalsIgnoreCase(Statistic.ENTITY_KILLED_BY.toString()) || statName.equalsIgnoreCase(Statistic.KILL_ENTITY.toString()); } @@ -158,7 +173,7 @@ public boolean isSubStatEntry(@NotNull String statName) { * @return "block", "entity", "item", or "sub-statistic" if the * provided Type is null. */ - public static String getSubStatTypeName(Statistic.Type statType) { + public String getSubStatTypeName(Statistic.Type statType) { String subStat = "sub-statistic"; if (statType == null) return subStat; switch (statType) { diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/utils/FileHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/FileHandler.java new file mode 100644 index 0000000..7ea1837 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/FileHandler.java @@ -0,0 +1,109 @@ +package com.artemis.the.gr8.playerstats.core.utils; + +import com.artemis.the.gr8.playerstats.core.Main; +import com.tchristofferson.configupdater.ConfigUpdater; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public abstract class FileHandler { + + private final String fileName; + private File file; + private FileConfiguration fileConfiguration; + + public FileHandler(String fileName) { + this.fileName = fileName; + loadFile(); + } + + private void loadFile() { + JavaPlugin plugin = Main.getPluginInstance(); + + file = new File(plugin.getDataFolder(), fileName); + if (!file.exists()) { + plugin.saveResource(fileName, false); + } + fileConfiguration = YamlConfiguration.loadConfiguration(file); + } + + public void reload() { + if (!file.exists()) { + loadFile(); + } else { + fileConfiguration = YamlConfiguration.loadConfiguration(file); + MyLogger.logLowLevelMsg(fileName + " reloaded!"); + } + } + + public FileConfiguration getFileConfiguration() { + return fileConfiguration; + } + + public void addValues(@NotNull Map keyValuePairs) { + keyValuePairs.forEach(this::setValue); + save(); + updateFile(); + } + + /** + * @param key the Key under which the List will be stored + * (or expanded if it already exists) + * @param value the value(s) to expand the List with + */ + public void writeEntryToList(@NotNull String key, @NotNull String value) { + List existingList = fileConfiguration.getStringList(key); + + List updatedList = existingList.stream() + .filter(Objects::nonNull) + .collect(Collectors.toList()); + updatedList.add(value); + + setValue(key, updatedList); + save(); + updateFile(); + } + + public void removeEntryFromList(@NotNull String key, @NotNull String value) { + List currentValues = fileConfiguration.getStringList(key); + + if (currentValues.remove(value)) { + setValue(key, currentValues); + save(); + updateFile(); + } + } + + private void setValue(String key, Object value) { + fileConfiguration.set(key, value); + } + + private void save() { + try { + fileConfiguration.save(file); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Add new key-value pairs to the config without losing comments, + * using tchristofferson's Config-Updater + */ + private void updateFile() { + JavaPlugin plugin = Main.getPluginInstance(); + try { + ConfigUpdater.update(plugin, fileName, file); + } catch (IOException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/utils/MyLogger.java b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/MyLogger.java similarity index 87% rename from src/main/java/com/artemis/the/gr8/playerstats/utils/MyLogger.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/utils/MyLogger.java index 0685903..e2ec061 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/utils/MyLogger.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/MyLogger.java @@ -1,6 +1,6 @@ -package com.artemis.the.gr8.playerstats.utils; +package com.artemis.the.gr8.playerstats.core.utils; -import com.artemis.the.gr8.playerstats.enums.DebugLevel; +import com.artemis.the.gr8.playerstats.core.enums.DebugLevel; import org.bukkit.Bukkit; import org.bukkit.plugin.Plugin; import org.jetbrains.annotations.NotNull; @@ -53,12 +53,22 @@ public static void logLowLevelMsg(String content) { logger.info(content); } + public static void logLowLevelTask(String taskName, long startTime) { + printTime(taskName, startTime); + } + public static void logMediumLevelMsg(String content) { if (debugLevel != DebugLevel.LOW) { logger.info(content); } } + public static void logMediumLevelTask(String taskName, long startTime) { + if (debugLevel != DebugLevel.LOW) { + printTime(taskName, startTime); + } + } + public static void logHighLevelMsg(String content) { if (debugLevel == DebugLevel.HIGH) { logger.info(content); @@ -146,24 +156,13 @@ public static void actionFinished() { } } - public static void logMediumLevelTask(String className, String methodName, long startTime) { - if (debugLevel != DebugLevel.LOW) { - printTime(className, methodName, startTime); - } - } - - public static void logLowLevelTask(String className, String methodName, long startTime) { - printTime(className, methodName, startTime); - } - /** * Output to console how long a certain task has taken. * - * @param className Name of the class executing the task - * @param methodName Name or description of the task + * @param taskName name of the task that has been executed * @param startTime Timestamp marking the beginning of the task */ - private static void printTime(String className, String methodName, long startTime) { - logger.info(className + " " + methodName + ": " + (System.currentTimeMillis() - startTime) + "ms"); + private static void printTime(String taskName, long startTime) { + logger.info(taskName + " (" + (System.currentTimeMillis() - startTime) + "ms)"); } } \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/utils/OfflinePlayerHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/OfflinePlayerHandler.java new file mode 100644 index 0000000..78ed96e --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/OfflinePlayerHandler.java @@ -0,0 +1,218 @@ +package com.artemis.the.gr8.playerstats.core.utils; + +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Predicate; + +/** + * A utility class that deals with OfflinePlayers. It stores a list + * of all OfflinePlayer-names that need to be included in statistic + * calculations, and can retrieve the corresponding OfflinePlayer + * object for a given player-name. + */ +public final class OfflinePlayerHandler extends FileHandler { + + private static volatile OfflinePlayerHandler instance; + private final ConfigHandler config; + private static ConcurrentHashMap includedPlayerUUIDs; + private static ConcurrentHashMap excludedPlayerUUIDs; + + private OfflinePlayerHandler() { + super("excluded_players.yml"); + config = ConfigHandler.getInstance(); + + loadOfflinePlayers(); + } + + public static OfflinePlayerHandler getInstance() { + OfflinePlayerHandler localVar = instance; + if (localVar != null) { + return localVar; + } + + synchronized (OfflinePlayerHandler.class) { + if (instance == null) { + instance = new OfflinePlayerHandler(); + } + return instance; + } + } + + @Override + public void reload() { + super.reload(); + loadOfflinePlayers(); + } + + /** + * Checks if a given player is currently + * included for /statistic lookups. + * + * @param playerName String (case-sensitive) + * @return true if this player is included + */ + public boolean isIncludedPlayer(String playerName) { + return includedPlayerUUIDs.containsKey(playerName); + } + + public boolean isExcludedPlayer(String playerName) { + return excludedPlayerUUIDs.containsKey(playerName); + } + + public boolean isExcludedPlayer(UUID uniqueID) { + return excludedPlayerUUIDs.containsValue(uniqueID); + } + + public boolean addPlayerToExcludeList(String playerName) { + if (isIncludedPlayer(playerName)) { + UUID uuid = includedPlayerUUIDs.get(playerName); + + super.writeEntryToList("excluded", uuid.toString()); + includedPlayerUUIDs.remove(playerName); + excludedPlayerUUIDs.put(playerName, uuid); + return true; + } + return false; + } + + public boolean removePlayerFromExcludeList(String playerName) { + if (isExcludedPlayer(playerName)) { + UUID uuid = excludedPlayerUUIDs.get(playerName); + + super.removeEntryFromList("excluded", uuid.toString()); + excludedPlayerUUIDs.remove(playerName); + includedPlayerUUIDs.put(playerName, uuid); + return true; + } + return false; + } + + @Contract(" -> new") + public @NotNull ArrayList getExcludedPlayerNames() { + return Collections.list(excludedPlayerUUIDs.keys()); + } + + /** + * Gets an ArrayList of names from all OfflinePlayers that should + * be included in statistic calculations. + * + * @return the ArrayList + */ + @Contract(" -> new") + public @NotNull ArrayList getIncludedOfflinePlayerNames() { + return Collections.list(includedPlayerUUIDs.keys()); + } + + /** + * Gets the number of OfflinePlayers that are + * currently included in statistic calculations. + * + * @return the number of included OfflinePlayers + */ + public int getIncludedPlayerCount() { + return includedPlayerUUIDs.size(); + } + + /** + * 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 (case-sensitive) + * @return OfflinePlayer + * @throws IllegalArgumentException if this player is not on the list + * of players that should be included in statistic calculations + */ + public @NotNull OfflinePlayer getIncludedOfflinePlayer(String playerName) throws IllegalArgumentException { + if (includedPlayerUUIDs.get(playerName) != null) { + return Bukkit.getOfflinePlayer(includedPlayerUUIDs.get(playerName)); + } + else { + MyLogger.logWarning("Cannot calculate statistics for player-name: " + playerName + + "! Double-check if the name is spelled correctly (including capital letters), " + + "or if any of your config settings exclude them"); + throw new IllegalArgumentException("PlayerStats does not know a player by this name"); + } + } + + public @NotNull OfflinePlayer getExcludedOfflinePlayer(String playerName) throws IllegalArgumentException { + if (excludedPlayerUUIDs.get(playerName) != null) { + return Bukkit.getOfflinePlayer(excludedPlayerUUIDs.get(playerName)); + } + throw new IllegalArgumentException("There is no player on the exclude-list with this name"); + } + + private void loadOfflinePlayers() { + Executors.newSingleThreadExecutor().execute(() -> { + loadExcludedPlayerNames(); + loadIncludedOfflinePlayers(); + }); + } + + private void loadIncludedOfflinePlayers() { + long time = System.currentTimeMillis(); + + OfflinePlayer[] offlinePlayers; + if (config.whitelistOnly()) { + offlinePlayers = getWhitelistedPlayers(); + } else if (config.excludeBanned()) { + offlinePlayers = getNonBannedPlayers(); + } else { + offlinePlayers = Bukkit.getOfflinePlayers(); + } + + int size = includedPlayerUUIDs != null ? includedPlayerUUIDs.size() : 16; + includedPlayerUUIDs = new ConcurrentHashMap<>(size); + + ForkJoinPool.commonPool().invoke(ThreadManager.getPlayerLoadAction(offlinePlayers, includedPlayerUUIDs)); + + MyLogger.actionFinished(); + MyLogger.logLowLevelTask(("Loaded " + includedPlayerUUIDs.size() + " offline players"), time); + } + + private void loadExcludedPlayerNames() { + long time = System.currentTimeMillis(); + + excludedPlayerUUIDs = new ConcurrentHashMap<>(); + List excluded = super.getFileConfiguration().getStringList("excluded"); + excluded.stream() + .filter(Objects::nonNull) + .map(UUID::fromString) + .forEach(uuid -> { + OfflinePlayer player = Bukkit.getOfflinePlayer(uuid); + String playerName = player.getName(); + if (playerName != null) { + excludedPlayerUUIDs.put(playerName, uuid); + } + }); + + MyLogger.logLowLevelTask("Loaded " + excludedPlayerUUIDs.size() + " excluded players from file", time); + } + + private OfflinePlayer[] getWhitelistedPlayers() { + return Bukkit.getWhitelistedPlayers().toArray(OfflinePlayer[]::new); + } + + private @NotNull OfflinePlayer[] getNonBannedPlayers() { + if (Bukkit.getPluginManager().isPluginEnabled("LiteBans")) { + return Arrays.stream(Bukkit.getOfflinePlayers()) + .parallel() + .filter(Predicate.not(OfflinePlayer::isBanned)) + .toArray(OfflinePlayer[]::new); + } + + Set banList = Bukkit.getBannedPlayers(); + return Arrays.stream(Bukkit.getOfflinePlayers()) + .parallel() + .filter(Predicate.not(banList::contains)) + .toArray(OfflinePlayer[]::new); + } +} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/utils/UnixTimeHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/UnixTimeHandler.java similarity index 94% rename from src/main/java/com/artemis/the/gr8/playerstats/utils/UnixTimeHandler.java rename to src/main/java/com/artemis/the/gr8/playerstats/core/utils/UnixTimeHandler.java index cf6e9a0..fa7f4d4 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/utils/UnixTimeHandler.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/utils/UnixTimeHandler.java @@ -1,4 +1,4 @@ -package com.artemis.the.gr8.playerstats.utils; +package com.artemis.the.gr8.playerstats.core.utils; /** * A small utility class that calculates with unix time. diff --git a/src/main/java/com/artemis/the/gr8/playerstats/enums/PluginColor.java b/src/main/java/com/artemis/the/gr8/playerstats/enums/PluginColor.java deleted file mode 100644 index 9663992..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/enums/PluginColor.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.artemis.the.gr8.playerstats.enums; - -import net.kyori.adventure.text.format.NamedTextColor; -import net.kyori.adventure.text.format.TextColor; - -import java.util.Random; - -/** - * This enum represents the colorscheme PlayerStats uses in its output messages. - * The first set of colors is used throughout the plugin, while the set of NAME-colors - * represents the colors that player-names can be in the "shared by player-name" - * section of shared statistics - */ -public enum PluginColor { - /** - * ChatColor Gray (#AAAAAA) - */ - GRAY (NamedTextColor.GRAY), - - /** - * A Dark Purple that is mainly used for title-underscores (#6E3485). - */ - DARK_PURPLE (TextColor.fromHexString("#6E3485")), - - /** - * A Light Purple that is meant to simulate the color of a clicked link. - * Used for the "Hover Here" part of shared statistics (#845EC2) - * */ - LIGHT_PURPLE (TextColor.fromHexString("#845EC2")), - - /** - * ChatColor Blue (#5555FF) - */ - BLUE (NamedTextColor.BLUE), - - /** - * A Medium Blue that is used for default feedback and error messages (#55AAFF). - */ - MEDIUM_BLUE (TextColor.fromHexString("#55AAFF")), - - /** - * A Light Blue that is used for hover-messages and the share-button (#55C6FF). - */ - LIGHT_BLUE (TextColor.fromHexString("#55C6FF")), - - /** - * ChatColor Gold (#FFAA00) - */ - GOLD (NamedTextColor.GOLD), - - /** - * A Medium Gold that is used for the example message and for hover-text accents (#FFD52B). - */ - MEDIUM_GOLD (TextColor.fromHexString("#FFD52B")), - - /** - * A Light Gold that is used for the example message and for hover-text accents (#FFEA40). - */ - LIGHT_GOLD (TextColor.fromHexString("#FFEA40")), - - /** - * A Light Yellow that is used for final accents in the example message (#FFFF8E). - */ - LIGHT_YELLOW (TextColor.fromHexString("#FFFF8E")), - - /** - * The color of vanilla Minecraft hearts (#FF1313). - */ - RED (TextColor.fromHexString("#FF1313")), - - /** - * ChatColor Blue (#5555FF) - */ - NAME_1 (NamedTextColor.BLUE), //#5555FF - blue - - /** - * A shade of blue between Blue and Medium Blue (#4287F5) - */ - NAME_2 (TextColor.fromHexString("#4287F5")), - - /** - * Medium Blue (#55AAFF) - */ - NAME_3 (TextColor.fromHexString("#55AAFF")), - - /** - * A shade of magenta/purple (#D65DB1) - */ - NAME_4 (TextColor.fromHexString("#D65DB1")), - - /** - * A dark shade of orange (#EE8A19) - */ - NAME_5 (TextColor.fromHexString("#EE8A19")), - - /** - * A shade of green/aqua/cyan-ish (#01C1A7) - */ - NAME_6 (TextColor.fromHexString("#01C1A7")), - - /** - * A light shade of green (#46D858) - */ - NAME_7 (TextColor.fromHexString("#46D858")); - - - private final TextColor color; - - PluginColor(TextColor color) { - this.color = color; - } - - /** - * Returns the TextColor value belonging to the corresponding enum constant. - */ - public TextColor getColor() { - return color; - } - - /** - * Gets the nearest NamedTextColor for the corresponding enum constant. - */ - public TextColor getConsoleColor() { - return NamedTextColor.nearestTo(color); - } - - /** - * Randomly selects one of the 7 different NAME-colors. - */ - public static TextColor getRandomNameColor() { - return getRandomNameColor(false); - } - - /** - * Randomly selects one of the 7 different NAME-colors, and if isConsole is true, - * returns the closest NamedTextColor - */ - public static TextColor getRandomNameColor(boolean isConsole) { - Random randomizer = new Random(); - PluginColor color = switch (randomizer.nextInt(7)) { - case 0 -> NAME_1; - case 2 -> NAME_3; - case 3 -> NAME_4; - case 4 -> NAME_5; - case 5 -> NAME_6; - case 6 -> NAME_7; - default -> NAME_2; - }; - return getCorrespondingColor(color, isConsole); - } - - private static TextColor getCorrespondingColor(PluginColor nameColor, boolean isConsole) { - return isConsole ? nameColor.getConsoleColor() : nameColor.getColor(); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/InternalFormatter.java b/src/main/java/com/artemis/the/gr8/playerstats/msg/InternalFormatter.java deleted file mode 100644 index 4041c7c..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/InternalFormatter.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.artemis.the.gr8.playerstats.msg; - -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import com.artemis.the.gr8.playerstats.statistic.StatCalculator; -import net.kyori.adventure.text.*; -import org.jetbrains.annotations.ApiStatus.Internal; - -import java.util.LinkedHashMap; - -/** The {@link InternalFormatter} formats raw numbers into pretty messages. - * This Formatter takes a {@link RequestSettings} object and combines it - * with the raw data returned by the {@link StatCalculator}, and transforms - * those into a pretty message with all the relevant information in it. - * @see MessageBuilder - */ -@Internal -public interface InternalFormatter { - - /** @return a TextComponent with the following parts: - *
[player-name]: [number] [stat-name] {sub-stat-name} - */ - TextComponent formatAndSavePlayerStat(RequestSettings requestSettings, int playerStat); - - /** @return a TextComponent with the following parts: - *
[Total on] [server-name]: [number] [stat-name] [sub-stat-name] - */ - TextComponent formatAndSaveServerStat(RequestSettings requestSettings, long serverStat); - - /** @return a TextComponent with the following parts: - *
[PlayerStats] [Top 10] [stat-name] [sub-stat-name] - *
[1.] [player-name] [number] - *
[2.] [player-name] [number] - *
[3.] etc... - */ - TextComponent formatAndSaveTopStat(RequestSettings requestSettings, LinkedHashMap topStats); -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/OutputManager.java b/src/main/java/com/artemis/the/gr8/playerstats/msg/OutputManager.java deleted file mode 100644 index 10f54fd..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/OutputManager.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.artemis.the.gr8.playerstats.msg; - -import com.artemis.the.gr8.playerstats.ShareManager; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.enums.StandardMessage; -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import com.artemis.the.gr8.playerstats.msg.components.BukkitConsoleComponentFactory; -import com.artemis.the.gr8.playerstats.msg.components.PrideComponentFactory; -import net.kyori.adventure.platform.bukkit.BukkitAudiences; -import net.kyori.adventure.text.TextComponent; -import org.bukkit.Bukkit; -import org.bukkit.Statistic; -import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.time.LocalDate; -import java.time.Month; -import java.util.EnumMap; -import java.util.LinkedHashMap; -import java.util.function.BiFunction; -import java.util.function.Function; - -import static com.artemis.the.gr8.playerstats.enums.StandardMessage.*; - -/** - * This class manages all PlayerStats output. It is the only - * place where messages are sent. It gets its messages from a - * {@link MessageBuilder} configured for either a Console or - * for Players (mainly to deal with the lack of hover-text, - * and for Bukkit consoles to make up for the lack of hex-colors). - */ -public final class OutputManager implements InternalFormatter { - - private static BukkitAudiences adventure; - private static ConfigHandler config; - private static ShareManager shareManager; - private static MessageBuilder messageBuilder; - private static MessageBuilder consoleMessageBuilder; - - private static EnumMap> standardMessages; - - public OutputManager(BukkitAudiences adventure, ConfigHandler config, ShareManager shareManager) { - OutputManager.adventure = adventure; - OutputManager.config = config; - OutputManager.shareManager = shareManager; - - getMessageBuilders(); - prepareFunctions(); - } - - public static void updateMessageBuilders() { - getMessageBuilders(); - } - - @Override - public TextComponent formatAndSavePlayerStat(@NotNull RequestSettings requestSettings, int playerStat) { - BiFunction playerStatFunction = - getMessageBuilder(requestSettings).formattedPlayerStatFunction(playerStat, requestSettings); - - return processFunction(requestSettings.getCommandSender(), playerStatFunction); - } - - @Override - public TextComponent formatAndSaveServerStat(@NotNull RequestSettings requestSettings, long serverStat) { - BiFunction serverStatFunction = - getMessageBuilder(requestSettings).formattedServerStatFunction(serverStat, requestSettings); - - return processFunction(requestSettings.getCommandSender(), serverStatFunction); - } - - @Override - public TextComponent formatAndSaveTopStat(@NotNull RequestSettings requestSettings, @NotNull LinkedHashMap topStats) { - BiFunction topStatFunction = - getMessageBuilder(requestSettings).formattedTopStatFunction(topStats, requestSettings); - - return processFunction(requestSettings.getCommandSender(), topStatFunction); - } - - public void sendFeedbackMsg(@NotNull CommandSender sender, StandardMessage message) { - if (message != null) { - adventure.sender(sender).sendMessage(standardMessages.get(message) - .apply(getMessageBuilder(sender))); - } - } - - public void sendFeedbackMsgWaitAMoment(@NotNull CommandSender sender, boolean longWait) { - adventure.sender(sender).sendMessage(getMessageBuilder(sender) - .waitAMoment(longWait)); - } - - public void sendFeedbackMsgMissingSubStat(@NotNull CommandSender sender, Statistic.Type statType) { - adventure.sender(sender).sendMessage(getMessageBuilder(sender) - .missingSubStatName(statType)); - } - - public void sendFeedbackMsgWrongSubStat(@NotNull CommandSender sender, Statistic.Type statType, @Nullable String subStatName) { - if (subStatName == null) { - sendFeedbackMsgMissingSubStat(sender, statType); - } else { - adventure.sender(sender).sendMessage(getMessageBuilder(sender) - .wrongSubStatType(statType, subStatName)); - } - } - - public void sendExamples(@NotNull CommandSender sender) { - adventure.sender(sender).sendMessage(getMessageBuilder(sender) - .usageExamples()); - } - - public void sendHelp(@NotNull CommandSender sender) { - adventure.sender(sender).sendMessage(getMessageBuilder(sender) - .helpMsg()); - } - - public void sendToAllPlayers(@NotNull TextComponent component) { - adventure.players().sendMessage(component); - } - - public void sendToCommandSender(@NotNull CommandSender sender, @NotNull TextComponent component) { - adventure.sender(sender).sendMessage(component); - } - - private TextComponent processFunction(CommandSender sender, @NotNull BiFunction statResultFunction) { - boolean saveOutput = !(sender instanceof ConsoleCommandSender) && - ShareManager.isEnabled() && - shareManager.senderHasPermission(sender); - - if (saveOutput) { - int shareCode = - shareManager.saveStatResult(sender.getName(), statResultFunction.apply(null, sender)); - return statResultFunction.apply(shareCode, null); - } - else { - return statResultFunction.apply(null, null); - } - } - - private MessageBuilder getMessageBuilder(CommandSender sender) { - return sender instanceof ConsoleCommandSender ? consoleMessageBuilder : messageBuilder; - } - - private MessageBuilder getMessageBuilder(RequestSettings requestSettings) { - if (!requestSettings.isConsoleSender()) { - return messageBuilder; - } else { - return consoleMessageBuilder; - } - } - - private static void getMessageBuilders() { - messageBuilder = getClientMessageBuilder(); - consoleMessageBuilder = getConsoleMessageBuilder(); - } - - private static MessageBuilder getClientMessageBuilder() { - if (useRainbowStyle()) { - return MessageBuilder.fromComponentFactory(config, new PrideComponentFactory(config)); - } - return MessageBuilder.defaultBuilder(config); - } - - private static MessageBuilder getConsoleMessageBuilder() { - MessageBuilder consoleBuilder; - if (isBukkit()) { - consoleBuilder = MessageBuilder.fromComponentFactory(config, new BukkitConsoleComponentFactory(config)); - } else { - consoleBuilder = getClientMessageBuilder(); - } - consoleBuilder.setConsoleBuilder(true); - consoleBuilder.toggleHoverUse(false); - return consoleBuilder; - } - - private static boolean useRainbowStyle() { - return config.useRainbowMode() || (config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE)); - } - - private static boolean isBukkit() { - return Bukkit.getName().equalsIgnoreCase("CraftBukkit"); - } - - private void prepareFunctions() { - standardMessages = new EnumMap<>(StandardMessage.class); - - standardMessages.put(RELOADED_CONFIG, (MessageBuilder::reloadedConfig)); - standardMessages.put(STILL_RELOADING, (MessageBuilder::stillReloading)); - standardMessages.put(MISSING_STAT_NAME, (MessageBuilder::missingStatName)); - standardMessages.put(MISSING_PLAYER_NAME, (MessageBuilder::missingPlayerName)); - standardMessages.put(REQUEST_ALREADY_RUNNING, (MessageBuilder::requestAlreadyRunning)); - standardMessages.put(STILL_ON_SHARE_COOLDOWN, (MessageBuilder::stillOnShareCoolDown)); - standardMessages.put(RESULTS_ALREADY_SHARED, (MessageBuilder::resultsAlreadyShared)); - standardMessages.put(STAT_RESULTS_TOO_OLD, (MessageBuilder::statResultsTooOld)); - standardMessages.put(UNKNOWN_ERROR, (MessageBuilder::unknownError)); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/PrideComponentFactory.java b/src/main/java/com/artemis/the/gr8/playerstats/msg/components/PrideComponentFactory.java deleted file mode 100644 index dab0dde..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/msg/components/PrideComponentFactory.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.artemis.the.gr8.playerstats.msg.components; - -import com.artemis.the.gr8.playerstats.config.ConfigHandler; - -import com.artemis.the.gr8.playerstats.enums.PluginColor; -import net.kyori.adventure.text.TextComponent; -import net.kyori.adventure.text.format.TextColor; -import net.kyori.adventure.text.minimessage.MiniMessage; - -import java.util.Random; - -import static net.kyori.adventure.text.Component.*; - -/** - * A festive version of the {@link ComponentFactory} - */ -public class PrideComponentFactory extends ComponentFactory { - - public PrideComponentFactory(ConfigHandler c) { - super(c); - } - - @Override - protected void prepareColors() { - PREFIX = PluginColor.GOLD.getColor(); - BRACKETS = PluginColor.GRAY.getColor(); - UNDERSCORE = PluginColor.DARK_PURPLE.getColor(); - HEARTS = PluginColor.RED.getColor(); - - MSG_MAIN = PluginColor.GRAY.getColor(); //difference 1 - MSG_ACCENT = PluginColor.LIGHT_GOLD.getColor(); //difference 2 - - MSG_MAIN_2 = PluginColor.GOLD.getColor(); - MSG_ACCENT_2A = PluginColor.MEDIUM_GOLD.getColor(); - MSG_ACCENT_2B = PluginColor.LIGHT_YELLOW.getColor(); - - MSG_HOVER = PluginColor.LIGHT_BLUE.getColor(); - MSG_CLICKED = PluginColor.LIGHT_PURPLE.getColor(); - MSG_HOVER_ACCENT = PluginColor.LIGHT_GOLD.getColor(); - } - - @Override - public TextColor getExampleNameColor() { - return getSharerNameColor(); - } - - @Override - public TextColor getSharerNameColor() { - return PluginColor.getRandomNameColor(); - } - - @Override - public TextComponent pluginPrefixAsTitle() { - String title = "____________ [PlayerStats] ____________"; //12 underscores - return text() - .append(MiniMessage.miniMessage().deserialize(title)) - .build(); - } - - @Override - public TextComponent pluginPrefix() { - Random randomizer = new Random(); - if (randomizer.nextBoolean()) { - return backwardsPluginPrefixComponent(); - } - return rainbowPrefix(); - } - - public TextComponent rainbowPrefix() { - return text() - .append(MiniMessage.miniMessage() - .deserialize("<#f74040>[" + - "<#F54D39>P" + - "<#F16E28>l" + - "<#ee8a19>a" + - "<#EEA019>y" + - "<#F7C522>e" + - "<#C1DA15>r" + - "<#84D937>S" + - "<#46D858>t" + - "<#01c1a7>a" + - "<#1F8BEB>t" + - "<#3341E6>s" + - "<#631ae6>]")) - .build(); - } - - private TextComponent backwardsPluginPrefixComponent() { - return text() - .append(MiniMessage.miniMessage() - .deserialize("<#631ae6>[" + - "<#3341E6>P" + - "<#1F8BEB>l" + - "<#01c1a7>a" + - "<#46D858>y" + - "<#84D937>e" + - "<#C1DA15>r" + - "<#F7C522>S" + - "<#EEA019>t" + - "<#ee8a19>a" + - "<#f67824>t" + - "<#f76540>s" + - "<#f74040>]")) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/reload/ReloadThread.java b/src/main/java/com/artemis/the/gr8/playerstats/reload/ReloadThread.java deleted file mode 100644 index 69d3e7b..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/reload/ReloadThread.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.artemis.the.gr8.playerstats.reload; - -import com.artemis.the.gr8.playerstats.ShareManager; -import com.artemis.the.gr8.playerstats.ThreadManager; -import com.artemis.the.gr8.playerstats.enums.StandardMessage; -import com.artemis.the.gr8.playerstats.msg.OutputManager; -import com.artemis.the.gr8.playerstats.msg.msgutils.LanguageKeyHandler; -import com.artemis.the.gr8.playerstats.statistic.StatCalculator; -import com.artemis.the.gr8.playerstats.statistic.StatThread; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import com.artemis.the.gr8.playerstats.config.ConfigHandler; -import com.artemis.the.gr8.playerstats.enums.DebugLevel; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.command.CommandSender; -import org.jetbrains.annotations.Nullable; -import java.util.Arrays; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ForkJoinPool; -import java.util.function.Predicate; - -/** The Thread that is in charge of reloading PlayerStats. */ -public final class ReloadThread extends Thread { - - private static ConfigHandler config; - private static OutputManager outputManager; - - private final int reloadThreadID; - private final StatThread statThread; - - private final CommandSender sender; - - public ReloadThread(ConfigHandler c, OutputManager m, int ID, @Nullable StatThread s, @Nullable CommandSender se) { - config = c; - outputManager = m; - - reloadThreadID = ID; - statThread = s; - sender = se; - - this.setName("ReloadThread-" + reloadThreadID); - MyLogger.logHighLevelMsg(this.getName() + " created!"); - } - - /** - * This method will perform a series of tasks. If a {@link StatThread} - * is still running, it will join the statThread and wait for it to finish. - * Then, it will reload the config, update the offlinePlayerList in the - * {@link OfflinePlayerHandler}, update the {@link DebugLevel}, update - * the share-settings in {@link ShareManager} and topListSize-settings - * in {@link StatCalculator}, and update the MessageBuilders in the - * {@link OutputManager}. - */ - @Override - public void run() { - long time = System.currentTimeMillis(); - MyLogger.logHighLevelMsg(this.getName() + " started!"); - - if (statThread != null && statThread.isAlive()) { - try { - MyLogger.logLowLevelMsg(this.getName() + ": Waiting for " + statThread.getName() + " to finish up..."); - statThread.join(); - } catch (InterruptedException e) { - MyLogger.logException(e, "ReloadThread", "run(), trying to join " + statThread.getName()); - throw new RuntimeException(e); - } - } - - if (reloadThreadID != 1 && config.reloadConfig()) { //during a reload - MyLogger.logLowLevelMsg("Reloading!"); - reloadEverything(); - - if (sender != null) { - outputManager.sendFeedbackMsg(sender, StandardMessage.RELOADED_CONFIG); - } - } - else { //during first start-up - MyLogger.setDebugLevel(config.getDebugLevel()); - OfflinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers()); - ThreadManager.recordCalcTime(System.currentTimeMillis() - time); - } - } - - private void reloadEverything() { - MyLogger.setDebugLevel(config.getDebugLevel()); - LanguageKeyHandler.reloadFile(); - OutputManager.updateMessageBuilders(); - OfflinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers()); - ShareManager.updateSettings(config); - } - - private ConcurrentHashMap loadOfflinePlayers() { - long time = System.currentTimeMillis(); - - OfflinePlayer[] offlinePlayers; - if (config.whitelistOnly()) { - offlinePlayers = Bukkit.getWhitelistedPlayers().toArray(OfflinePlayer[]::new); - MyLogger.logMediumLevelTask("ReloadThread", - "retrieved whitelist", time); - } - else if (config.excludeBanned()) { - if (Bukkit.getPluginManager().getPlugin("LiteBans") != null) { - offlinePlayers = Arrays.stream(Bukkit.getOfflinePlayers()) - .parallel() - .filter(Predicate.not(OfflinePlayer::isBanned)) - .toArray(OfflinePlayer[]::new); - } else { - Set bannedPlayers = Bukkit.getBannedPlayers(); - offlinePlayers = Arrays.stream(Bukkit.getOfflinePlayers()) - .parallel() - .filter(offlinePlayer -> !bannedPlayers.contains(offlinePlayer)).toArray(OfflinePlayer[]::new); - } - MyLogger.logMediumLevelTask("ReloadThread", - "retrieved banlist", time); - } - else { - offlinePlayers = Bukkit.getOfflinePlayers(); - MyLogger.logMediumLevelTask("ReloadThread", - "retrieved list of Offline Players", time); - } - - int size = offlinePlayers != null ? offlinePlayers.length : 16; - ConcurrentHashMap playerMap = new ConcurrentHashMap<>(size); - - ReloadAction task = new ReloadAction(offlinePlayers, config.getLastPlayedLimit(), playerMap); - MyLogger.actionCreated((offlinePlayers != null) ? offlinePlayers.length : 0); - ForkJoinPool.commonPool().invoke(task); - MyLogger.actionFinished(); - - MyLogger.logLowLevelTask("ReloadThread", - ("loaded " + playerMap.size() + " offline players"), time); - return playerMap; - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatCalculator.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatCalculator.java deleted file mode 100644 index 67e0a7a..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatCalculator.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic; - -import com.artemis.the.gr8.playerstats.ThreadManager; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import com.google.common.collect.ImmutableList; -import org.bukkit.OfflinePlayer; -import org.jetbrains.annotations.NotNull; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ForkJoinPool; -import java.util.stream.Collectors; - -public final class StatCalculator { - - private final OfflinePlayerHandler offlinePlayerHandler; - - public StatCalculator(OfflinePlayerHandler offlinePlayerHandler) { - this.offlinePlayerHandler = offlinePlayerHandler; - } - - public int getPlayerStat(RequestSettings requestSettings) { - OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(requestSettings.getPlayerName()); - return switch (requestSettings.getStatistic().getType()) { - case UNTYPED -> player.getStatistic(requestSettings.getStatistic()); - case ENTITY -> player.getStatistic(requestSettings.getStatistic(), requestSettings.getEntity()); - case BLOCK -> player.getStatistic(requestSettings.getStatistic(), requestSettings.getBlock()); - case ITEM -> player.getStatistic(requestSettings.getStatistic(), requestSettings.getItem()); - }; - } - - public LinkedHashMap getTopStats(RequestSettings requestSettings) { - return getAllStatsAsync(requestSettings).entrySet().stream() - .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) - .limit(requestSettings.getTopListSize()) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); - } - - public long getServerStat(RequestSettings requestSettings) { - List numbers = getAllStatsAsync(requestSettings) - .values() - .parallelStream() - .toList(); - return numbers.parallelStream().mapToLong(Integer::longValue).sum(); - } - - /** - * Invokes a bunch of worker pool threads to get the statistics for - * all players that are stored in the {@link OfflinePlayerHandler}). - */ - private @NotNull ConcurrentHashMap getAllStatsAsync(RequestSettings requestSettings) { - long time = System.currentTimeMillis(); - - ForkJoinPool commonPool = ForkJoinPool.commonPool(); - ConcurrentHashMap allStats; - - try { - allStats = commonPool.invoke(getStatTask(requestSettings)); - } catch (ConcurrentModificationException e) { - MyLogger.logWarning("The requestSettings could not be executed due to a ConcurrentModificationException. " + - "This likely happened because Bukkit hasn't fully initialized all player-data yet. " + - "Try again and it should be fine!"); - throw new ConcurrentModificationException(e.toString()); - } - - MyLogger.actionFinished(); - ThreadManager.recordCalcTime(System.currentTimeMillis() - time); - MyLogger.logMediumLevelTask("StatThread", "calculated all stats", time); - - return allStats; - } - - private StatAction getStatTask(RequestSettings requestSettings) { - int size = offlinePlayerHandler.getOfflinePlayerCount() != 0 ? offlinePlayerHandler.getOfflinePlayerCount() : 16; - ConcurrentHashMap allStats = new ConcurrentHashMap<>(size); - ImmutableList playerNames = ImmutableList.copyOf(offlinePlayerHandler.getOfflinePlayerNames()); - - StatAction task = new StatAction(offlinePlayerHandler, playerNames, requestSettings, allStats); - MyLogger.actionCreated(playerNames.size()); - - return task; - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatThread.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatThread.java deleted file mode 100644 index bb6f999..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/StatThread.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic; - -import com.artemis.the.gr8.playerstats.ThreadManager; -import com.artemis.the.gr8.playerstats.msg.OutputManager; -import com.artemis.the.gr8.playerstats.utils.MyLogger; -import com.artemis.the.gr8.playerstats.enums.StandardMessage; -import com.artemis.the.gr8.playerstats.enums.Target; -import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings; -import com.artemis.the.gr8.playerstats.reload.ReloadThread; -import net.kyori.adventure.text.TextComponent; -import org.jetbrains.annotations.Nullable; - -import java.util.*; - -/** - * The Thread that is in charge of getting and calculating statistics. - */ -public final class StatThread extends Thread { - - private static OutputManager outputManager; - private static StatCalculator statCalculator; - - private final ReloadThread reloadThread; - private final RequestSettings requestSettings; - - public StatThread(OutputManager m, StatCalculator t, int ID, RequestSettings s, @Nullable ReloadThread r) { - outputManager = m; - statCalculator = t; - - reloadThread = r; - requestSettings = s; - - this.setName("StatThread-" + requestSettings.getCommandSender().getName() + "-" + ID); - MyLogger.logHighLevelMsg(this.getName() + " created!"); - } - - @Override - public void run() throws IllegalStateException, NullPointerException { - MyLogger.logHighLevelMsg(this.getName() + " started!"); - - if (requestSettings == null) { - throw new NullPointerException("No statistic requestSettings was found!"); - } - if (reloadThread != null && reloadThread.isAlive()) { - try { - MyLogger.logLowLevelMsg(this.getName() + ": Waiting for " + reloadThread.getName() + " to finish up..."); - outputManager.sendFeedbackMsg(requestSettings.getCommandSender(), StandardMessage.STILL_RELOADING); - reloadThread.join(); - - } catch (InterruptedException e) { - MyLogger.logException(e, "StatThread", "Trying to join " + reloadThread.getName()); - throw new RuntimeException(e); - } - } - - long lastCalc = ThreadManager.getLastRecordedCalcTime(); - if (lastCalc > 2000) { - outputManager.sendFeedbackMsgWaitAMoment(requestSettings.getCommandSender(), lastCalc > 20000); - } - - Target selection = requestSettings.getTarget(); - try { - TextComponent statResult = switch (selection) { - case PLAYER -> outputManager.formatAndSavePlayerStat(requestSettings, statCalculator.getPlayerStat(requestSettings)); - case TOP -> outputManager.formatAndSaveTopStat(requestSettings, statCalculator.getTopStats(requestSettings)); - case SERVER -> outputManager.formatAndSaveServerStat(requestSettings, statCalculator.getServerStat(requestSettings)); - }; - outputManager.sendToCommandSender(requestSettings.getCommandSender(), statResult); - } - catch (ConcurrentModificationException e) { - if (!requestSettings.isConsoleSender()) { - outputManager.sendFeedbackMsg(requestSettings.getCommandSender(), StandardMessage.UNKNOWN_ERROR); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/PlayerStatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/PlayerStatRequest.java deleted file mode 100644 index 2162485..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/PlayerStatRequest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.request; - -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.statistic.result.PlayerStatResult; -import com.artemis.the.gr8.playerstats.api.RequestGenerator; -import com.artemis.the.gr8.playerstats.msg.components.ComponentUtils; -import net.kyori.adventure.text.TextComponent; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.entity.EntityType; -import org.jetbrains.annotations.NotNull; - -public final class PlayerStatRequest extends StatRequest implements RequestGenerator { - - private final RequestHandler requestHandler; - - public PlayerStatRequest(RequestSettings request) { - super(request); - requestHandler = new RequestHandler(request); - } - - @Override - public PlayerStatRequest untyped(@NotNull Statistic statistic) { - RequestSettings completedRequest = requestHandler.untyped(statistic); - return new PlayerStatRequest(completedRequest); - } - - @Override - public PlayerStatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) { - RequestSettings completedRequest = requestHandler.blockOrItemType(statistic, material); - return new PlayerStatRequest(completedRequest); - } - - @Override - public PlayerStatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) { - RequestSettings completedRequest = requestHandler.entityType(statistic, entityType); - return new PlayerStatRequest(completedRequest); - } - - @Override - public PlayerStatResult execute() { - return getStatResult(super.requestSettings); - } - - private PlayerStatResult getStatResult(RequestSettings completedRequest) { - int stat = Main - .getStatCalculator() - .getPlayerStat(completedRequest); - - TextComponent prettyComponent = Main - .getStatFormatter() - .formatAndSavePlayerStat(completedRequest, stat); - - String prettyString = ComponentUtils - .getTranslatableComponentSerializer() - .serialize(prettyComponent); - - return new PlayerStatResult(stat, prettyComponent, prettyString); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/RequestHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/RequestHandler.java deleted file mode 100644 index 9fdfe21..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/RequestHandler.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.request; - -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.utils.EnumHandler; -import com.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler; -import com.artemis.the.gr8.playerstats.enums.Target; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; -import org.bukkit.entity.EntityType; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; - -public final class RequestHandler { - - private final RequestSettings requestSettings; - - public RequestHandler(RequestSettings request) { - requestSettings = request; - } - - public static RequestSettings getBasicPlayerStatRequest(String playerName) { - RequestSettings request = RequestSettings.getBasicAPIRequest(); - request.setTarget(Target.PLAYER); - request.setPlayerName(playerName); - return request; - } - - public static RequestSettings getBasicServerStatRequest() { - RequestSettings request = RequestSettings.getBasicAPIRequest(); - request.setTarget(Target.SERVER); - return request; - } - - public static RequestSettings getBasicTopStatRequest(int topListSize) { - RequestSettings request = RequestSettings.getBasicAPIRequest(); - request.setTarget(Target.TOP); - request.setTopListSize(topListSize != 0 ? topListSize : Main.getConfigHandler().getTopListMaxSize()); - return request; - } - - /** - * @param sender the CommandSender that requested this specific statistic - */ - public static RequestSettings getBasicInternalStatRequest(CommandSender sender) { - RequestSettings request = RequestSettings.getBasicRequest(sender); - request.setTopListSize(Main.getConfigHandler().getTopListMaxSize()); - return request; - } - - public RequestSettings untyped(@NotNull Statistic statistic) throws IllegalArgumentException { - if (statistic.getType() == Statistic.Type.UNTYPED) { - requestSettings.setStatistic(statistic); - return requestSettings; - } - throw new IllegalArgumentException("This statistic is not of Type.Untyped"); - } - - public RequestSettings blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException { - Statistic.Type type = statistic.getType(); - if (type == Statistic.Type.BLOCK && material.isBlock()) { - requestSettings.setBlock(material); - } - else if (type == Statistic.Type.ITEM && material.isItem()){ - requestSettings.setItem(material); - } - else { - throw new IllegalArgumentException("Either this statistic is not of Type.Block or Type.Item, or no valid block or item has been provided"); - } - requestSettings.setStatistic(statistic); - requestSettings.setSubStatEntryName(material.toString()); - return requestSettings; - } - - public RequestSettings entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException { - if (statistic.getType() == Statistic.Type.ENTITY) { - requestSettings.setStatistic(statistic); - requestSettings.setSubStatEntryName(entityType.toString()); - requestSettings.setEntity(entityType); - return requestSettings; - } - throw new IllegalArgumentException("This statistic is not of Type.Entity"); - } - - /** - * This will create a {@link RequestSettings} object from the provided args, - * with the requesting Player (or Console) as CommandSender. This CommandSender - * will receive feedback messages if the RequestSettings could not be created. - * - * @param args an Array of args such as a CommandSender would put in Minecraft chat: - *
    - *
  • a statName (example: "mine_block") - *
  • if applicable, a subStatEntryName (example: diorite) - *
  • a target for this lookup: can be "top", "server", "player" - * (or "me" to indicate the current CommandSender) - *
  • if "player" was chosen, include a playerName - *
- * @return the generated RequestSettings - */ - public RequestSettings getRequestFromArgs(String[] args) { - EnumHandler enumHandler = Main.getEnumHandler(); - OfflinePlayerHandler offlinePlayerHandler = Main.getOfflinePlayerHandler(); - CommandSender sender = requestSettings.getCommandSender(); - - for (String arg : args) { - //check for statName - if (enumHandler.isStatistic(arg) && requestSettings.getStatistic() == null) { - requestSettings.setStatistic(EnumHandler.getStatEnum(arg)); - } - //check for subStatEntry and playerFlag - else if (enumHandler.isSubStatEntry(arg)) { - if (arg.equalsIgnoreCase("player") && !requestSettings.getPlayerFlag()) { - requestSettings.setPlayerFlag(true); - } else { - if (requestSettings.getSubStatEntryName() == null) requestSettings.setSubStatEntryName(arg); - } - } - //check for selection - else if (arg.equalsIgnoreCase("top")) { - requestSettings.setTarget(Target.TOP); - } else if (arg.equalsIgnoreCase("server")) { - requestSettings.setTarget(Target.SERVER); - } else if (arg.equalsIgnoreCase("me")) { - if (sender instanceof Player) { - requestSettings.setPlayerName(sender.getName()); - requestSettings.setTarget(Target.PLAYER); - } else if (sender instanceof ConsoleCommandSender) { - requestSettings.setTarget(Target.SERVER); - } - } else if (offlinePlayerHandler.isRelevantPlayer(arg) && requestSettings.getPlayerName() == null) { - requestSettings.setPlayerName(arg); - requestSettings.setTarget(Target.PLAYER); - } - } - patchRequest(requestSettings); - return requestSettings; - } - - /** - * Adjust the RequestSettings object if needed: unpack the playerFlag - * into a subStatEntry, try to retrieve the corresponding Enum Constant - * for any relevant block/entity/item, and remove any unnecessary - * subStatEntries. - */ - private void patchRequest(RequestSettings requestSettings) { - if (requestSettings.getStatistic() != null) { - Statistic.Type type = requestSettings.getStatistic().getType(); - - if (requestSettings.getPlayerFlag()) { //unpack the playerFlag - if (type == Statistic.Type.ENTITY && requestSettings.getSubStatEntryName() == null) { - requestSettings.setSubStatEntryName("player"); - } else { - requestSettings.setTarget(Target.PLAYER); - } - } - - String subStatEntry = requestSettings.getSubStatEntryName(); - switch (type) { //attempt to convert relevant subStatEntries into their corresponding Enum Constant - case BLOCK -> { - Material block = EnumHandler.getBlockEnum(subStatEntry); - if (block != null) requestSettings.setBlock(block); - } - case ENTITY -> { - EntityType entity = EnumHandler.getEntityEnum(subStatEntry); - if (entity != null) requestSettings.setEntity(entity); - } - case ITEM -> { - Material item = EnumHandler.getItemEnum(subStatEntry); - if (item != null) requestSettings.setItem(item); - } - case UNTYPED -> { //remove unnecessary subStatEntries - if (subStatEntry != null) requestSettings.setSubStatEntryName(null); - } - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/RequestSettings.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/RequestSettings.java deleted file mode 100644 index 454f674..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/RequestSettings.java +++ /dev/null @@ -1,183 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.request; - -import com.artemis.the.gr8.playerstats.api.RequestGenerator; -import com.artemis.the.gr8.playerstats.enums.Target; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.command.CommandSender; -import org.bukkit.command.ConsoleCommandSender; -import org.bukkit.entity.EntityType; -import org.jetbrains.annotations.NotNull; - -/** - * The object PlayerStats uses to calculate and format the requested - * statistic. The settings in this RequestSettings object can be - * configured from two different sources: - *
- Internally: by PlayerStats itself when /stat is called, - * using the args provided by the CommandSender. - *
- Externally: through the API methods provided by the - * {@link RequestGenerator} interface. - *
- *
For this RequestSettings object to be valid, the following - * values need to be set: - *
    - *
  • a {@link Statistic} statistic
  • - *
  • if this Statistic is not of {@link Statistic.Type} Untyped, - * a subStatEntryName needs to be set, together with one - * of the following values: - *
    - for Type.Block: a {@link Material} blockMaterial - *
    - for Type.Item: a {@link Material} itemMaterial - *
    - for Type.Entity: an {@link EntityType} entityType - *
  • a {@link Target} target (defaults to Top) - *
  • if the target is Target.Player, a - * playerName needs to be added - *
- */ -public final class RequestSettings { - - private final CommandSender sender; - private Statistic statistic; - private String playerName; - private Target target; - private int topListSize; - - private String subStatEntryName; - private EntityType entity; - private Material block; - private Material item; - private boolean playerFlag; - - /** - * Create a new {@link RequestSettings} with default values: - *
- CommandSender sender (provided) - *
- Target target = {@link Target#TOP} - *
- int topListSize = 10 - *
- boolean playerFlag = false - * - * @param sender the CommandSender who prompted this RequestGenerator - */ - private RequestSettings(@NotNull CommandSender sender) { - this.sender = sender; - target = Target.TOP; - playerFlag = false; - } - - public static RequestSettings getBasicRequest(CommandSender sender) { - return new RequestSettings(sender); - } - - public static RequestSettings getBasicAPIRequest() { - return new RequestSettings(Bukkit.getConsoleSender()); - } - - public @NotNull CommandSender getCommandSender() { - return sender; - } - - public boolean isConsoleSender() { - return sender instanceof ConsoleCommandSender; - } - - public void setStatistic(Statistic statistic) { - this.statistic = statistic; - } - - public Statistic getStatistic() { - return statistic; - } - - public void setSubStatEntryName(String subStatEntry) { - this.subStatEntryName = subStatEntry; - } - - public String getSubStatEntryName() { - return subStatEntryName; - } - - public void setPlayerName(String playerName) { - this.playerName = playerName; - } - - public String getPlayerName() { - return playerName; - } - - public void setPlayerFlag(boolean playerFlag) { - this.playerFlag = playerFlag; - } - - public boolean getPlayerFlag() { - return playerFlag; - } - - public void setTarget(@NotNull Target target) { - this.target = target; - } - - public @NotNull Target getTarget() { - return target; - } - - public void setTopListSize(int topListSize) { - this.topListSize = topListSize; - } - - public int getTopListSize() { - return this.topListSize; - } - - public void setEntity(EntityType entity) { - this.entity = entity; - } - - public EntityType getEntity() { - return entity; - } - - public void setBlock(Material block) { - this.block = block; - } - - public Material getBlock() { - return block; - } - - public void setItem(Material item) { - this.item = item; - } - - public Material getItem() { - return item; - } - - public boolean isValid() { - if (statistic == null) { - return false; - } else if (target == Target.PLAYER && playerName == null) { - return false; - } else if (statistic.getType() != Statistic.Type.UNTYPED && - subStatEntryName == null) { - return false; - } else { - return hasMatchingSubStat(); - } - } - - private boolean hasMatchingSubStat() { - switch (statistic.getType()) { - case BLOCK -> { - return block != null; - } - case ENTITY -> { - return entity != null; - } - case ITEM -> { - return item != null; - } - default -> { - return true; - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/ServerStatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/ServerStatRequest.java deleted file mode 100644 index 42916b0..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/ServerStatRequest.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.request; - -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.statistic.result.ServerStatResult; -import com.artemis.the.gr8.playerstats.api.RequestGenerator; -import com.artemis.the.gr8.playerstats.msg.components.ComponentUtils; -import net.kyori.adventure.text.TextComponent; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.entity.EntityType; -import org.jetbrains.annotations.NotNull; - -public final class ServerStatRequest extends StatRequest implements RequestGenerator { - - private final RequestHandler requestHandler; - - public ServerStatRequest(RequestSettings request) { - super(request); - requestHandler = new RequestHandler(requestSettings); - } - - @Override - public ServerStatRequest untyped(@NotNull Statistic statistic) { - RequestSettings completedRequest = requestHandler.untyped(statistic); - return new ServerStatRequest(completedRequest); - } - - @Override - public ServerStatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) { - RequestSettings completedRequest = requestHandler.blockOrItemType(statistic, material); - return new ServerStatRequest(completedRequest); - } - - @Override - public ServerStatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) { - RequestSettings completedRequest = requestHandler.entityType(statistic, entityType); - return new ServerStatRequest(completedRequest); - } - - @Override - public ServerStatResult execute() { - return getStatResult(requestSettings); - } - - private ServerStatResult getStatResult(RequestSettings completedRequest) { - long stat = Main - .getStatCalculator() - .getServerStat(completedRequest); - - TextComponent prettyComponent = Main - .getStatFormatter() - .formatAndSaveServerStat(completedRequest, stat); - - String prettyString = ComponentUtils - .getTranslatableComponentSerializer() - .serialize(prettyComponent); - - return new ServerStatResult(stat, prettyComponent, prettyString); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/StatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/StatRequest.java deleted file mode 100644 index c5567e2..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/StatRequest.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.request; - -import com.artemis.the.gr8.playerstats.api.PlayerStats; -import com.artemis.the.gr8.playerstats.statistic.result.StatResult; -import com.artemis.the.gr8.playerstats.enums.Target; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.entity.EntityType; -import org.jetbrains.annotations.Nullable; - -/** - * Holds all the information PlayerStats needs to perform - * a lookup, and can be executed to get the results. Calling - * {@link #execute()} on a Top- or ServerRequest can take some - * time (especially if there is a substantial amount of - * OfflinePlayers on this particular server), so I strongly - * advice you to call this asynchronously! - */ -public abstract class StatRequest { - - protected final RequestSettings requestSettings; - - protected StatRequest(RequestSettings request) { - requestSettings = request; - } - - /** - * Executes this StatRequest. For a Top- or ServerRequest, this can - * take some time! - * - * @return a StatResult containing the value of this lookup, both as - * numerical value and as formatted message - * @see PlayerStats - * @see StatResult - */ - public abstract StatResult execute(); - - /** - * Gets the Statistic that calling {@link #execute()} will calculate - * the data for. - * @return the Statistic - */ - public Statistic getStatisticSetting() { - return requestSettings.getStatistic(); - } - - /** - * If the Statistic setting for this StatRequest is of Type.Block, - * this will get the Material that was set. - * - * @return a Material for which #isBlock is true, or null if no - * Material was set - */ - public @Nullable Material getBlockSetting() { - return requestSettings.getBlock(); - } - - /** - * If the Statistic setting for this StatRequest is of Type.Item, - * this will get the Material that was set. - * - * @return a Material for which #isItem is true, or null if no - * Material was set - */ - public @Nullable Material getItemSetting() { - return requestSettings.getItem(); - } - - /** - * If the Statistic setting for this StatRequest is of Type.Entity, - * this will get the EntityType that was set. - * - * @return an EntityType, or null if no EntityType was set - */ - public @Nullable EntityType getEntitySetting() { - return requestSettings.getEntity(); - } - - /** - * Gets the Target that will be used when calling {@link #execute()}. - * - * @return the Target for this lookup (either Player, Server or Top) - */ - public Target getTargetSetting() { - return requestSettings.getTarget(); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/TopStatRequest.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/TopStatRequest.java deleted file mode 100644 index 8cf8f90..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/request/TopStatRequest.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.request; - -import com.artemis.the.gr8.playerstats.Main; -import com.artemis.the.gr8.playerstats.statistic.result.TopStatResult; -import com.artemis.the.gr8.playerstats.api.RequestGenerator; -import com.artemis.the.gr8.playerstats.msg.components.ComponentUtils; -import net.kyori.adventure.text.TextComponent; -import org.bukkit.Material; -import org.bukkit.Statistic; -import org.bukkit.entity.EntityType; -import org.jetbrains.annotations.NotNull; - -import java.util.LinkedHashMap; - -public final class TopStatRequest extends StatRequest> implements RequestGenerator> { - - private final RequestHandler requestHandler; - - public TopStatRequest(RequestSettings request) { - super(request); - requestHandler = new RequestHandler(request); - } - - @Override - public TopStatRequest untyped(@NotNull Statistic statistic) { - RequestSettings completedRequest = requestHandler.untyped(statistic); - return new TopStatRequest(completedRequest); - } - - @Override - public TopStatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) { - RequestSettings completedRequest = requestHandler.blockOrItemType(statistic, material); - return new TopStatRequest(completedRequest); - } - - @Override - public TopStatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) { - RequestSettings completedRequest = requestHandler.entityType(statistic, entityType); - return new TopStatRequest(completedRequest); - } - - @Override - public TopStatResult execute() { - return getStatResult(super.requestSettings); - } - - private TopStatResult getStatResult(RequestSettings completedRequest) { - LinkedHashMap stat = Main - .getStatCalculator() - .getTopStats(completedRequest); - - TextComponent prettyComponent = Main - .getStatFormatter() - .formatAndSaveTopStat(completedRequest, stat); - - String prettyString = ComponentUtils - .getTranslatableComponentSerializer() - .serialize(prettyComponent); - - return new TopStatResult(stat, prettyComponent, prettyString); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/InternalStatResult.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/InternalStatResult.java deleted file mode 100644 index 4afc930..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/InternalStatResult.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.result; - -import com.artemis.the.gr8.playerstats.msg.components.ComponentUtils; -import net.kyori.adventure.text.TextComponent; - -/** - * This Record is used to store stat-results internally, - * so Players can share them by clicking a share-button. - */ -public record InternalStatResult(String executorName, TextComponent formattedValue, int ID) implements StatResult { - - /** - * Gets the ID number for this StatResult. Unlike for the - * other {@link StatResult} implementations, this one does - * not return the actual statistic data, because this - * implementation is meant for internal saving-and-sharing only. - * This method is only for Interface-consistency, - * InternalStatResult#ID is better. - * - @return Integer that represents this StatResult's ID number - */ - @Override - public Integer getNumericalValue() { - return ID; - } - - @Override - public TextComponent getFormattedTextComponent() { - return formattedValue; - } - - @Override - public String getFormattedString() { - return ComponentUtils.getTranslatableComponentSerializer() - .serialize(formattedValue); - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/PlayerStatResult.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/PlayerStatResult.java deleted file mode 100644 index 960d2e7..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/PlayerStatResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.result; - -import net.kyori.adventure.text.TextComponent; - -public record PlayerStatResult(int value, TextComponent formattedComponent, String formattedString) implements StatResult { - - @Override - public Integer getNumericalValue() { - return value; - } - - @Override - public TextComponent getFormattedTextComponent() { - return formattedComponent; - } - - @Override - public String getFormattedString() { - return formattedString; - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/ServerStatResult.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/ServerStatResult.java deleted file mode 100644 index 0eddb56..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/ServerStatResult.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.result; - -import net.kyori.adventure.text.TextComponent; - -public record ServerStatResult(long value, TextComponent formattedComponent, String formattedString) implements StatResult { - - @Override - public Long getNumericalValue() { - return value; - } - - @Override - public TextComponent getFormattedTextComponent() { - return formattedComponent; - } - - @Override - public String getFormattedString() { - return formattedString; - } -} diff --git a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/TopStatResult.java b/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/TopStatResult.java deleted file mode 100644 index dcd8eba..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/statistic/result/TopStatResult.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.artemis.the.gr8.playerstats.statistic.result; - -import net.kyori.adventure.text.TextComponent; - -import java.util.LinkedHashMap; - -public record TopStatResult(LinkedHashMap value, TextComponent formattedComponent, String formattedString) implements StatResult> { - - @Override - public LinkedHashMap getNumericalValue() { - return value; - } - - @Override - public TextComponent getFormattedTextComponent() { - return formattedComponent; - } - - @Override - public String getFormattedString() { - return formattedString; - } -} \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java b/src/main/java/com/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java deleted file mode 100644 index 93d2e06..0000000 --- a/src/main/java/com/artemis/the/gr8/playerstats/utils/OfflinePlayerHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.artemis.the.gr8.playerstats.utils; - -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; - -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A utility class that deals with OfflinePlayers. It stores a list - * of all OfflinePlayer-names that need to be included in statistic - * calculations, and can retrieve the corresponding OfflinePlayer - * object for a given player-name. - */ -public final class OfflinePlayerHandler { - - private static ConcurrentHashMap offlinePlayerUUIDs; - private static ArrayList playerNames; - - public OfflinePlayerHandler() { - offlinePlayerUUIDs = new ConcurrentHashMap<>(); - playerNames = new ArrayList<>(); - } - - /** - * Get a new HashMap that stores the players to include in stat calculations. - * This HashMap is stored as a private variable in OfflinePlayerHandler. - * - * @param playerList ConcurrentHashMap with keys: playerNames and values: UUIDs - */ - public static void updateOfflinePlayerList(ConcurrentHashMap playerList) { - offlinePlayerUUIDs = playerList; - playerNames = Collections.list(offlinePlayerUUIDs.keys()); - } - - /** - * Checks if a given playerName is on the private HashMap of players - * that should be included in statistic calculations. - * - * @param playerName String (case-sensitive) - * @return true if this Player should be included in calculations - */ - public boolean isRelevantPlayer(String playerName) { - return offlinePlayerUUIDs.containsKey(playerName); - } - - /** - * Gets the number of OfflinePlayers that are included in - * statistic calculations. - * - * @return the number of included OfflinePlayers - */ - public int getOfflinePlayerCount() { - return offlinePlayerUUIDs.size(); - } - - /** - * Gets an ArrayList of names from all OfflinePlayers that should - * be included in statistic calculations. - * - * @return the ArrayList - */ - public ArrayList getOfflinePlayerNames() { - return playerNames; - } - - /** - * 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 (case-sensitive) - * @return OfflinePlayer - * @throws IllegalArgumentException if this player is not on the list - * of players that should be included in statistic calculations - */ - public OfflinePlayer getOfflinePlayer(String playerName) throws IllegalArgumentException { - if (offlinePlayerUUIDs.get(playerName) != null) { - return Bukkit.getOfflinePlayer(offlinePlayerUUIDs.get(playerName)); - } - else { - MyLogger.logWarning("Cannot calculate statistics for player-name: " + playerName + - "! Double-check if the name is spelled correctly (including capital letters), " + - "or if any of your config settings exclude them"); - throw new IllegalArgumentException("Cannot convert this player-name into a valid Player to calculate statistics for"); - } - } -} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 27c6b78..9065560 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,7 +1,7 @@ # ------------------------------------------------------------------------------------------------------ # # PlayerStats Configuration # # ------------------------------------------------------------------------------------------------------ # -config-version: 6 +config-version: 7 # # ------------------------------- # # @@ -33,6 +33,11 @@ exclude-banned-players: false # Leave this on 0 to include all players number-of-days-since-last-joined: 0 +# Players that are excluded through the previous settings or the excluded-players-file will not +# show up in top or server statistics. This setting controls whether you can still see their stats with +# the /stat player command +allow-player-lookups-for-excluded-players: true + # # ------------------------------- # # # # Format & Display # # diff --git a/src/main/resources/excluded_players.yml b/src/main/resources/excluded_players.yml new file mode 100644 index 0000000..b2cc778 --- /dev/null +++ b/src/main/resources/excluded_players.yml @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------------ # +# PlayerStats Excluded Players # +# ------------------------------------------------------------------------------------------------------ # + +# Players whose UUIDs are stored in this file, will be hidden from /statistic results. +# This can be used to exclude alt accounts, for example. +# To exclude groups of players (such as banned players), see the config.yml (section 'General') + +# UUIDs can be added directly to this file, or through the /statexclude command in game. +# Format: +# - player1UUID +# - player2UUID +excluded: + - \ No newline at end of file diff --git a/src/main/resources/language.yml b/src/main/resources/language.yml index 23ee04b..b6aee02 100644 --- a/src/main/resources/language.yml +++ b/src/main/resources/language.yml @@ -2,6 +2,9 @@ # PlayerStats Language File # # ------------------------------------------------------------------------------------------------------ # +# If "translate-to-client-language" in the config.yml is set to false (section 'Format & Display'), +# values from this file will be used instead + stat_type.minecraft.mined: "Times Mined" stat_type.minecraft.crafted: "Times Crafted" stat_type.minecraft.used: "Times Used" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 72bcee5..3c9988d 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,6 @@ -main: com.artemis.the.gr8.playerstats.Main +main: com.artemis.the.gr8.playerstats.core.Main name: PlayerStats -version: 1.8 +version: 2.0 api-version: 1.13 description: adds commands to view player statistics in chat author: Artemis_the_gr8 @@ -11,22 +11,31 @@ commands: aliases: - stat - stats - description: general statistic command + description: show player statistics in private chat + usage: "§6/stat info" permission: playerstats.stat statisticshare: aliases: - statshare - statsshare - description: shares last stat lookup in chat - usage: "§b/statshare" + description: share last stat lookup in chat + usage: "§6/This command can only be executed by clicking the \"share\" button in /stat results. + If you don't see this button, you don't have share-permission, or sharing is turned off." permission: playerstats.share statisticreload: aliases: - statreload - statsreload description: reloads the config - usage: "§a/statreload" + usage: "§6/statreload" permission: playerstats.reload + statisticexclude: + aliases: + - statexclude + - statsexclude + description: hide this player's statistics from /stat results + usage: "§6/statexclude info" + permission: playerstats.exclude permissions: playerstats.stat: description: allows usage of /statistic @@ -34,6 +43,9 @@ permissions: playerstats.share: description: allows sharing stats in chat default: true + playerstats.exclude: + description: allows usage of /statexclude + default: op playerstats.reload: description: allows usage of /statreload default: op \ No newline at end of file