Merge pull request #131 from itHotL/v2.0

V2.0
This commit is contained in:
Elise 2023-02-28 12:20:17 +01:00 committed by GitHub
commit 11a78decae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 3357 additions and 3022 deletions

View File

@ -4,7 +4,7 @@
<groupId>io.github.ithotl</groupId>
<artifactId>PlayerStats</artifactId>
<name>PlayerStats</name>
<version>1.8</version>
<version>2.0</version>
<description>Statistics Plugin</description>
<url>https://www.spigotmc.org/resources/playerstats.102347/</url>
<developers>
@ -49,7 +49,7 @@
<configuration>
<transformers>
<transformer>
<mainClass>com.artemis.the.gr8.playerstats.Main</mainClass>
<mainClass>com.artemis.the.gr8.playerstats.core.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
@ -60,15 +60,15 @@
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.artemis.the.gr8.lib.kyori</shadedPattern>
<shadedPattern>com.artemis.the.gr8.playerstats.lib.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.artemis.the.gr8.util.tchristofferson</shadedPattern>
<shadedPattern>com.artemis.the.gr8.playerstats.lib.tchristofferson</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.artemis.the.gr8.util.bstats</shadedPattern>
<shadedPattern>com.artemis.the.gr8.playerstats.lib.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
@ -107,7 +107,7 @@
<executions>
<execution>
<id>attach-sources</id>
<phase>deploy</phase>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
@ -120,7 +120,7 @@
<executions>
<execution>
<id>sign-artifacts</id>
<phase>deploy</phase>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
@ -133,7 +133,7 @@
<executions>
<execution>
<id>attach-javadocs</id>
<phase>deploy</phase>
<phase>verify</phase>
<goals>
<goal>jar</goal>
</goals>

16
pom.xml
View File

@ -6,7 +6,7 @@
<groupId>io.github.ithotl</groupId>
<artifactId>PlayerStats</artifactId>
<version>1.8</version>
<version>2.0</version>
<name>PlayerStats</name>
<description>Statistics Plugin</description>
@ -141,7 +141,7 @@
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.artemis.the.gr8.playerstats.Main</mainClass>
<mainClass>com.artemis.the.gr8.playerstats.core.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
@ -152,15 +152,15 @@
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.artemis.the.gr8.lib.kyori</shadedPattern>
<shadedPattern>com.artemis.the.gr8.playerstats.lib.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.artemis.the.gr8.util.tchristofferson</shadedPattern>
<shadedPattern>com.artemis.the.gr8.playerstats.lib.tchristofferson</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.artemis.the.gr8.util.bstats</shadedPattern>
<shadedPattern>com.artemis.the.gr8.playerstats.lib.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
@ -200,7 +200,7 @@
<executions>
<execution>
<id>attach-sources</id>
<phase>deploy</phase>
<phase>verify</phase> <!-- change to verify when deploying -->
<goals>
<goal>jar-no-fork</goal>
</goals>
@ -214,7 +214,7 @@
<executions>
<execution>
<id>sign-artifacts</id>
<phase>deploy</phase>
<phase>verify</phase> <!-- change to verify when deploying -->
<goals>
<goal>sign</goal>
</goals>
@ -228,7 +228,7 @@
<executions>
<execution>
<id>attach-javadocs</id>
<phase>deploy</phase>
<phase>verify</phase> <!-- change to verify when deploying -->
<goals>
<goal>jar</goal>
</goals>

View File

@ -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);
}
}

View File

@ -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<String, Thread> 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();
}
}

View File

@ -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!
*
* <p> 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.
*
* <p> 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.
* <p> 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
* <code>ClassDefNotFoundExceptions</code>.
* 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();
}

View File

@ -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);
}
}

View File

@ -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 @@ import org.jetbrains.annotations.NotNull;
/**
* 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<T> {

View File

@ -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<Integer> playerStatRequest(String playerName);
RequestGenerator<Integer> 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<Integer> executePlayerStatRequest(StatRequest<Integer> 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<Long> serverStatRequest();
RequestGenerator<Long> 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<Long> executeServerStatRequest(StatRequest<Long> 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<LinkedHashMap<String, Integer>> topStatRequest(int topListSize);
RequestGenerator<LinkedHashMap<String, Integer>> 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<LinkedHashMap<String, Integer>> totalTopStatRequest();
RequestGenerator<LinkedHashMap<String, Integer>> 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<LinkedHashMap<String, Integer>> executeTopRequest(StatRequest<LinkedHashMap<String, Integer>> request);
}

View File

@ -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);
}

View File

@ -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<T> {
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;
}
}
}

View File

@ -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 @@ import net.kyori.adventure.text.TextComponent;
* <br> [2.] [player-name] [.....] [formatted-number]
* <br> [3.] etc...
* </ul>
* <p>
* 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
* <a href="https://docs.adventure.kyori.net/platform/bukkit.html">Adventure's website</a>.
*
* <p>You can also use the provided {@link #getFormattedString()} method to get the
* <p>You can also use the provided {@link #formattedString()} method to get the
* same information in String-format. Don't use Adventure's <code>#content()</code>
* or <code>#toString()</code> 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<T> {
public record StatResult<T>(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<String, Integer>} for topStat
* @return {@code Integer} for playerStat, {@code Long} for serverStat, and {@code LinkedHashMap<String, Integer>}
* 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;
}
}

View File

@ -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 @@ import org.jetbrains.annotations.Nullable;
* @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 <code>NumberFormatter</code>
*/
default NumberFormatter getNumberFormatter() {
return new NumberFormatter();
}
String textComponentToString(TextComponent component);
/**
* Gets the default prefix PlayerStats uses.

View File

@ -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

View File

@ -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 @@ public enum Unit {
}
/**
* 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) {

View File

@ -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:
* <ul>
* <li>Is a <code>statistic</code> set?
* <li>Is a <code>subStatEntry</code> needed, and if so, is a corresponding Material/EntityType present?
* <li>If the <code>target</code> is Player, is a valid <code>playerName</code> provided?
* </ul>
*
* @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());
}
}
}
}

View File

@ -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<String> 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<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
List<String> 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<String> getFirstArgSuggestions(String currentArg) {
List<String> suggestions = enumHandler.getStatNames();
suggestions.add("examples");
suggestions.add("help");
return getTabSuggestions(suggestions, currentArg);
}
private List<String> getTabSuggestions(List<String> completeList, String currentArg) {
return completeList.stream()
.filter(item -> item.toLowerCase(Locale.ENGLISH).contains(currentArg.toLowerCase(Locale.ENGLISH)))
.collect(Collectors.toList());
}
private List<String> 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;
}
}
}
}

View File

@ -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<String> itemBrokenSuggestions;
private static List<String> entitySuggestions;
public TabCompleteHelper(EnumHandler enumHandler) {
this.enumHandler = enumHandler;
prepareLists();
}
public List<String> getAllItemNames() {
return enumHandler.getItemNames();
}
public List<String> getItemBrokenSuggestions() {
return itemBrokenSuggestions;
}
public List<String> getAllBlockNames() {
return enumHandler.getBlockNames();
}
public List<String> 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());
}
}

View File

@ -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 <a href="https://github.com/tchristofferson/Config-Updater">tchristofferson's Config-Updater</a>
*/
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);
}
}
}

View File

@ -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();
}
}

View File

@ -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<String> 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;
}
}

View File

@ -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;
}

View File

@ -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 @@ import org.jetbrains.annotations.NotNull;
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 @@ public final class ShareCommand implements CommandExecutor {
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 {

View File

@ -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:
* <ul>
* <li>Is a <code>statistic</code> set?
* <li>Is a <code>subStatEntry</code> needed, and if so,
* is a corresponding Material/EntityType present?
* <li>If the <code>target</code> is Player, is a valid
* <code>playerName</code> provided?
* </ul>
*
* @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<String> 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<String> currentArgs = new ArrayList<>(Arrays.asList(argsToProcess));
currentArgs.remove(argToRemove);
return currentArgs.toArray(String[]::new);
}
}
}

View File

@ -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<String> statCommandTargets;
private List<String> excludeCommandOptions;
private List<String> itemsThatCanBreak;
private List<String> entitiesThatCanDie;
public TabCompleter() {
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
enumHandler = EnumHandler.getInstance();
prepareLists();
}
@Override
public @Nullable List<String> 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<String> getExcludeCommandSuggestions(@NotNull String[] args) {
if (args.length == 0) {
return null;
}
List<String> 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<String> getStatCommandSuggestions(@NotNull String[] args) {
if (args.length == 0) {
return null;
}
List<String> 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<String> getDynamicTabSuggestions(@NotNull List<String> completeList, String currentArg) {
return completeList.stream()
.filter(item -> item.toLowerCase(Locale.ENGLISH).contains(currentArg.toLowerCase(Locale.ENGLISH)))
.collect(Collectors.toList());
}
private @NotNull List<String> firstStatCommandArgSuggestions() {
List<String> suggestions = enumHandler.getAllStatNames();
suggestions.add("examples");
suggestions.add("info");
suggestions.add("help");
return suggestions;
}
private List<String> 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());
}
}

View File

@ -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.
* <br>
* <br>PlayerStats 1.1: "config-version" doesn't exist.
* <br>PlayerStats 1.2: "config-version" is 2.
@ -42,41 +57,17 @@ public final class ConfigHandler {
* <br>PlayerStats 1.5: "config-version" is 5.
* <br>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<String, Object> 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 final class ConfigHandler {
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.
*

View File

@ -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<String, Object> defaultValuesToAdjust;
public DefaultValueGetter(FileConfiguration configuration) {
config = configuration;
defaultValuesToAdjust = new HashMap<>();
}
public Map<String, Object> 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);
}
}
}

View File

@ -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.

View File

@ -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);
}
}

View File

@ -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 @@ package com.artemis.the.gr8.playerstats.enums;
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
}

View File

@ -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;

View File

@ -1,25 +1,21 @@
package com.artemis.the.gr8.playerstats.msg;
package com.artemis.the.gr8.playerstats.core.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.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 com.artemis.the.gr8.playerstats.statistic.request.RequestSettings;
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 @@ import static net.kyori.adventure.text.Component.*;
* @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 final class MessageBuilder implements ApiFormatter {
}
@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 final class MessageBuilder implements ApiFormatter {
}
@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() {
public @NotNull TextComponent excludeFailed() {
return composePluginMessage("This player is already hidden from /stat results!");
}
public @NotNull TextComponent includeSuccess(String playerName) {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please provide a valid statistic name!"));
.append(componentFactory.message().content("Removed ")
.append(componentFactory.messageAccent().content(playerName))
.append(text(" from the exclude-list!")));
}
public TextComponent missingSubStatName(Statistic.Type statType) {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please add a valid " + EnumHandler.getSubStatTypeName(statType) + " to look up this statistic!"));
public @NotNull TextComponent includeFailed() {
return composePluginMessage("This is not a player that has been excluded with the /statexclude command!");
}
public TextComponent missingPlayerName() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please specify a valid player-name!"));
public @NotNull TextComponent waitAMinute() {
return composePluginMessage("Calculating statistics, this may take a minute...");
}
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatName) {
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 @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 final class MessageBuilder implements ApiFormatter {
.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()
.append(space())
.append(componentFactory.message().content(
"Something went wrong with your request, " +
"please try again or see /statistic for a usage explanation!"));
public @NotNull TextComponent unknownError() {
return composePluginMessage("Something went wrong with your request, " +
"please try again or see /statistic for a usage explanation!");
}
public TextComponent usageExamples() {
private @NotNull TextComponent composePluginMessage(String content) {
return getPluginPrefix()
.append(space())
.append(componentFactory.message().content(content));
}
@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<String> 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 final class MessageBuilder implements ApiFormatter {
* 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 final class MessageBuilder implements ApiFormatter {
* <br>- If both parameters are null, the formattedComponent will be returned
* as is.
*/
public BiFunction<Integer, CommandSender, TextComponent> 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 final class MessageBuilder implements ApiFormatter {
* <br>- If both parameters are null, the formattedComponent will be returned
* as is.
*/
public BiFunction<Integer, CommandSender, TextComponent> 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 final class MessageBuilder implements ApiFormatter {
* <br>- If both parameters are null, the formattedComponent will be returned
* as is.
*/
public BiFunction<Integer, CommandSender, TextComponent> formattedTopStatFunction(@NotNull LinkedHashMap<String, Integer> topStats, @NotNull RequestSettings request) {
public @NotNull FormattingFunction formattedTopStatFunction(@NotNull LinkedHashMap<String, Integer> 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<Integer, CommandSender, TextComponent> biFunction = (shareCode, sender) -> {
TextComponent.Builder topBuilder = text();
//if we're adding a share-button
@ -391,9 +414,10 @@ public final class MessageBuilder implements ApiFormatter {
}
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 @@ public final class MessageBuilder implements ApiFormatter {
.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 @@ public final class MessageBuilder implements ApiFormatter {
.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 @@ public final class MessageBuilder implements ApiFormatter {
}
}
private TextComponent getTopStatListComponent(LinkedHashMap<String, Integer> topStats, Statistic statistic) {
private @NotNull TextComponent getTopStatListComponent(@NotNull LinkedHashMap<String, Integer> topStats, Statistic statistic) {
TextComponent.Builder topList = Component.text();
Set<String> playerNames = topStats.keySet();
boolean useDots = config.useDots();
@ -472,7 +496,7 @@ public final class MessageBuilder implements ApiFormatter {
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 @@ public final class MessageBuilder implements ApiFormatter {
}
private TextComponent getStatAndSubStatNameComponent(Statistic statistic, @Nullable String subStatName, Target target) {
if (config.useTranslatableComponents()) {
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));
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()) {
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 @@ public final class MessageBuilder implements ApiFormatter {
ArrayList<Unit> 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 @@ public final class MessageBuilder implements ApiFormatter {
}
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 @@ public final class MessageBuilder implements ApiFormatter {
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 @@ public final class MessageBuilder implements ApiFormatter {
/**
* 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 @@ public final class MessageBuilder implements ApiFormatter {
/**
* 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 @@ public final class MessageBuilder implements ApiFormatter {
return componentFactory.sharerName(sender.getName());
}
private BiFunction<Integer, CommandSender, TextComponent> 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<Integer, CommandSender, TextComponent> biFunction = (shareCode, sender) -> {
TextComponent.Builder statBuilder = text();
//if we're adding a share-button
@ -706,10 +727,11 @@ public final class MessageBuilder implements ApiFormatter {
}
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 @@ public final class MessageBuilder implements ApiFormatter {
* <p>2. maxHoverUnit</p>
* <p>3. minHoverUnit</p>
*/
private ArrayList<Unit> getTimeUnitRange(long statNumber) {
private @NotNull ArrayList<Unit> getTimeUnitRange(long statNumber) {
ArrayList<Unit> unitRange = new ArrayList<>();
if (!config.autoDetectTimeUnit(false)) {
unitRange.add(Unit.fromString(config.getTimeUnit(false)));

View File

@ -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<StandardMessage, Function<MessageBuilder, TextComponent>> 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:
* <br>[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:
* <br>[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:
* <br>[PlayerStats] [Top 10] [stat-name] [sub-stat-name]
* <br> [1.] [player-name] [number]
* <br> [2.] [player-name] [number]
* <br> [3.] etc...
*/
public @NotNull FormattingFunction formatTopStats(@NotNull StatRequest.Settings requestSettings, @NotNull LinkedHashMap<String, Integer> 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<String> 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);
}
}

View File

@ -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(
"<gradient:#a405e3:#f74040:#f73b3b:#ff9300:#f74040:#a405e3>" +
"<#FF9300>\ud83d\udd25</#FF9300> __________ [PlayerStats] __________ " +
"<#FF9300>\ud83d\udd25</#FF9300></gradient>");
}
@Override
public TextComponent pluginPrefix() {
return miniMessageToComponent(
"<gradient:#a405e3:#f74040:#ff9300>[PlayerStats]</gradient>");
}
}

View File

@ -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 @@ import static net.kyori.adventure.text.Component.text;
* 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 @@ public class BukkitConsoleComponentFactory extends ComponentFactory {
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

View File

@ -1,11 +1,11 @@
package com.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.core.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;
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.event.HoverEvent;
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 @@ public class ComponentFactory {
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 TextColor getSharerNameColor() {
return getColorFromString(config.getSharerNameDecoration(false));
public boolean isConsoleFactory() {
return false;
}
public TextComponent getExampleName() {
return text("Artemis_the_gr8").color(FEEDBACK_MSG);
}
/**
@ -95,11 +105,10 @@ public class ComponentFactory {
* 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 class ComponentFactory {
* 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 class ComponentFactory {
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 class ComponentFactory {
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 class ComponentFactory {
}
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 class ComponentFactory {
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 clientHeartWithHoverText() {
TextComponent basicHeartComponent = basicHeartComponent('\u2764')
public TextComponent heartBetweenBrackets() {
return surroundWithBrackets(heart());
}
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 @@ public class ComponentFactory {
*
* @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,16 +398,16 @@ public class ComponentFactory {
* @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,
private @NotNull TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber,
@Nullable String hoverUnitName,
@Nullable String hoverUnitKey,
@Nullable TextComponent heartComponent, Target target) {
@ -415,7 +431,7 @@ public class ComponentFactory {
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 @@ public class ComponentFactory {
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());

View File

@ -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();
}
}

View File

@ -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 @@ public final class ExampleMessage implements TextComponent {
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

View File

@ -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<Component> children() {
return excludeInfo.children();
}
@Override
public @NotNull TextComponent children(@NotNull List<? extends ComponentLike> 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);
}
}

View File

@ -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(
"<gradient:#ff9300:#f74040:#f73b3b:#ff9300:#f74040:#ff9300>" +
"<white>\u2620</white> __________ [PlayerStats] __________ " +
"<white>\u2620</white></gradient>");
}
@Override
public TextComponent pluginPrefix() {
return miniMessageToComponent(
"<gradient:#f74040:gold:#f74040>[PlayerStats]</gradient>");
}
@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 -> "<gradient:#fcad23:red>";
case 1 -> "<gradient:#fcad23:#f967b2:#F79438:#ffe30f>";
case 2 -> "<gradient:red:#fcad23:red>";
case 3 -> "<gradient:#f28e30:#f5cb42:#f74040>";
case 4 -> "<gradient:#F79438:#f967b2>";
case 5 -> "<gradient:#f967b2:#fcad23:#f967b2>";
default -> "<gradient:#fcad23:#f967b2:#F74040>";
};
return colorString + input + "</gradient>";
}
}

View File

@ -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.TextComponent;
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 @@ public final class HelpMessage implements TextComponent {
}
}
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")))));
}

View File

@ -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("<gradient:#f74040:gold:#FF6600:#f74040>Artemis_the_gr8</gradient>");
}
@Override
public TextComponent sharerName(String sharerName) {
return miniMessageToComponent(decorateWithRandomGradient(sharerName));
}
@Override
//12 underscores
public TextComponent pluginPrefixAsTitle() {
return miniMessageToComponent("<rainbow:16>____________ [PlayerStats] ____________</rainbow>");
}
@Override
public TextComponent pluginPrefix() {
return miniMessageToComponent("<#f74040>[</#f74040>" +
"<#F54D39>P</#F54D39>" +
"<#F16E28>l</#F16E28>" +
"<#ee8a19>a</#ee8a19>" +
"<#EEA019>y</#EEA019>" +
"<#F7C522>e</#F7C522>" +
"<#C1DA15>r</#C1DA15>" +
"<#84D937>S</#84D937>" +
"<#46D858>t</#46D858>" +
"<#01c1a7>a</#01c1a7>" +
"<#1F8BEB>t</#1F8BEB>" +
"<#3341E6>s</#3341E6>" +
"<#631ae6>]</#631ae6>");
}
private @NotNull String decorateWithRandomGradient(@NotNull String input) {
Random random = new Random();
String colorString = switch (random.nextInt(8)) {
case 0 -> "<gradient:#03b6fc:#f854df>";
case 1 -> "<gradient:#14f7a0:#4287f5>";
case 2 -> "<gradient:#f971ae:#fcad23>";
case 3 -> "<gradient:#309de6:#af45ed>";
case 4 -> "<gradient:#f971ae:#af45ed:#4287f5>";
case 5 -> "<gradient:#FFEA40:#fcad23:#F79438>";
case 6 -> "<gradient:#309de6:#01c1a7:#F7F438>";
case 7 -> "<gradient:#F79438:#f967b2>";
default -> "<gradient:#F7F438:#01c1a7>";
};
return colorString + input + "</gradient>";
}
}

View File

@ -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(
"<gradient:#4f20f7:#4bc3fa:#05ebb1:#4f20f7>" +
"<#D6F1FE>\u2744</#D6F1FE> __________ [PlayerStats] __________ " +
"<#D6F1FE>\u2744</#D6F1FE></gradient>");
}
@Override
public TextComponent pluginPrefix() {
return miniMessageToComponent(
"<gradient:#4CA5F9:#15D6C4:#409ef7:#4F2FF7>[PlayerStats]</gradient>");
}
}

View File

@ -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 @@ public final class ComponentUtils {
}
//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 @@ public final class ComponentUtils {
return serializer.toBuilder().flattener(flattener).build();
}
private static LegacyComponentSerializer getTextComponentSerializer() {
@Contract(" -> new")
private static @NotNull LegacyComponentSerializer getTextComponentSerializer() {
return LegacyComponentSerializer
.builder()
.hexColors()

View File

@ -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.Tag;
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 @@ import java.util.Random;
*/
public final class EasterEggProvider {
private static boolean isEnabled;
private static final Random random;
static {
enable();
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 @@ public final class EasterEggProvider {
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 + '%');

View File

@ -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;

View File

@ -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<Integer, CommandSender, TextComponent> formattingFunction;
public FormattingFunction(BiFunction<Integer, CommandSender, TextComponent> 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);
}
}

View File

@ -1,64 +1,60 @@
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<Statistic, String> statNameKeys;
private static File languageKeyFile;
private static FileConfiguration languageKeys;
private static volatile LanguageKeyHandler instance;
private static HashMap<Statistic, String> 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);
}
languageKeys = YamlConfiguration.loadConfiguration(languageKeyFile);
public static LanguageKeyHandler getInstance() {
LanguageKeyHandler localVar = instance;
if (localVar != null) {
return localVar;
}
@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");
}
/**
@ -67,7 +63,8 @@ public final class LanguageKeyHandler {
* @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 final class LanguageKeyHandler {
* @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 final class LanguageKeyHandler {
*
* @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 final class LanguageKeyHandler {
* @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,70 +117,75 @@ public final class LanguageKeyHandler {
* @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";
}
toReplace = toReplace + ".minecraft.";
return key.replace(toReplace, "");
}
private static @Nullable String convertToNormalStatKey(String statKey) {
if (isKeyForKillEntity(statKey)) {
return "stat_type.minecraft.killed";
} else if (isKeyForEntityKilledBy(statKey)) {
return "stat_type.minecraft.killed_by";
} else if (isKeyForEntityKilledByArg(statKey)) {
return null;
} else {
return statKey;
Matcher matcher = subStatKey.matcher(key);
if (matcher.find()) {
String rawName = matcher.replaceFirst("");
return StringUtils.prettify(rawName);
}
return key;
}
public static String getStatKeyTranslation(String statKey) {
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 "";
}
return languageKeys.getString(realKey);
return super.getFileConfiguration().getString(realKey);
}
private static @Nullable String convertToNormalStatKey(String statKey) {
if (isCustomKeyForKillEntity(statKey)) {
return "stat_type.minecraft.killed";
} else if (isCustomKeyForEntityKilledBy(statKey)) {
return "stat_type.minecraft.killed_by";
} else if (isCustomKeyForEntityKilledByArg(statKey)) {
return null;
} else {
return statKey;
}
}
/**
@ -197,12 +193,12 @@ public final class LanguageKeyHandler {
* @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 @@ public final class LanguageKeyHandler {
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 @@ public final class LanguageKeyHandler {
}
}
private @NotNull HashMap<Statistic, String> generateStatNameKeys() {
private @NotNull HashMap<Statistic, String> generateStatisticKeys() {
//get the enum names for all statistics first
HashMap<Statistic, String> statNames = new HashMap<>(Statistic.values().length);
Arrays.stream(Statistic.values()).forEach(statistic -> statNames.put(statistic, statistic.toString().toLowerCase(Locale.ENGLISH)));

View File

@ -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 @@ import java.text.DecimalFormat;
* 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 final class 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 final class NumberFormatter {
* 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 final class NumberFormatter {
* 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 final class NumberFormatter {
* @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 final class NumberFormatter {
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());
}

View File

@ -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 @@ import java.util.Locale;
*/
public final class StringUtils {
private static final Pattern lowercaseLetterAfterSpace;
static {
lowercaseLetterAfterSpace = Pattern.compile("(?<= )[a-z]");
}
private StringUtils() {
}
@ -20,14 +28,22 @@ public final class 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();
}

View File

@ -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 @@ import java.util.concurrent.RecursiveAction;
/**
* 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<String, UUID> 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<String, UUID> offlinePlayerUUIDs) {
this(players, 0, players.length, lastPlayedLimit, offlinePlayerUUIDs);
public PlayerLoadAction(OfflinePlayer[] players, ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
this(players, 0, players.length, offlinePlayerUUIDs);
}
private ReloadAction(OfflinePlayer[] players, int start, int end,
int lastPlayedLimit, ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
private PlayerLoadAction(OfflinePlayer[] players, int start, int end, ConcurrentHashMap<String, UUID> 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 @@ final class ReloadAction extends RecursiveAction {
}
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 @@ final class ReloadAction extends RecursiveAction {
}
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());
}
}

View File

@ -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);
}
}
}

View File

@ -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 @@ import java.util.concurrent.RecursiveTask;
final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>> {
private static int threshold;
private final OfflinePlayerHandler offlinePlayerHandler;
private final ImmutableList<String> playerNames;
private final RequestSettings requestSettings;
private final StatRequest.Settings requestSettings;
private final ConcurrentHashMap<String, Integer> allStats;
/**
@ -29,15 +26,13 @@ final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>>
* 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<String> playerNames, RequestSettings requestSettings, ConcurrentHashMap<String, Integer> allStats) {
public StatAction(ImmutableList<String> playerNames, StatRequest.Settings requestSettings, ConcurrentHashMap<String, Integer> allStats) {
threshold = ThreadManager.getTaskThreshold();
this.offlinePlayerHandler = offlinePlayerHandler;
this.playerNames = playerNames;
this.requestSettings = requestSettings;
this.allStats = allStats;
@ -51,8 +46,8 @@ final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>>
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 @@ final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>>
}
private ConcurrentHashMap<String, Integer> getStatsDirectly() {
OfflinePlayerHandler offlinePlayerHandler = OfflinePlayerHandler.getInstance();
Iterator<String> 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());

View File

@ -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);
}
}
}
}

View File

@ -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<String, Thread> 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<String> relevantPlayerNames = ImmutableList.copyOf(offlinePlayerHandler.getIncludedOfflinePlayerNames());
ConcurrentHashMap<String, Integer> 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<String, UUID> 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();
}
}

View File

@ -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 @@ import static java.time.temporal.ChronoUnit.SECONDS;
*/
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<Integer, InternalStatResult> statResultQueue;
private static ConcurrentHashMap<String, Instant> shareTimeStamp;
private static ArrayBlockingQueue<Integer> sharedResults;
private volatile AtomicInteger NumberOfStoredResults;
private ConcurrentHashMap<Integer, StoredResult> statResultQueue;
private ConcurrentHashMap<String, Instant> shareTimeStamp;
private ArrayBlockingQueue<Integer> 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 final class ShareManager {
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 final class ShareManager {
* 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 final class ShareManager {
* StatResults saved, remove the oldest one.
*/
private void removeExcessResults(String playerName) {
List<InternalStatResult> alreadySavedResults = statResultQueue.values()
List<StoredResult> alreadySavedResults = statResultQueue.values()
.parallelStream()
.filter(result -> result.executorName().equalsIgnoreCase(playerName))
.toList();
@ -142,7 +156,7 @@ public final class ShareManager {
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 @@ public final class ShareManager {
}
private int getNextIDNumber() {
return resultID.incrementAndGet();
return NumberOfStoredResults.incrementAndGet();
}
}

View File

@ -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) {
}

View File

@ -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<Integer> implements RequestGenerator<Integer> {
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<Integer> untyped(@NotNull Statistic statistic) {
super.configureUntyped(statistic);
return this;
}
@Override
public StatRequest<Integer> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
super.configureBlockOrItemType(statistic, material);
return this;
}
@Override
public StatRequest<Integer> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
super.configureEntityType(statistic, entityType);
return this;
}
}

View File

@ -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<Integer> createPlayerStatRequest(String playerName) {
return new PlayerStatRequest(playerName);
}
@Override
public @NotNull StatResult<Integer> executePlayerStatRequest(@NotNull StatRequest<Integer> request) {
return processor.processPlayerRequest(request.getSettings());
}
@Contract(" -> new")
@Override
public @NotNull RequestGenerator<Long> createServerStatRequest() {
return new ServerStatRequest();
}
@Override
public @NotNull StatResult<Long> executeServerStatRequest(@NotNull StatRequest<Long> request) {
return processor.processServerRequest(request.getSettings());
}
@Contract("_ -> new")
@Override
public @NotNull RequestGenerator<LinkedHashMap<String, Integer>> createTopStatRequest(int topListSize) {
return new TopStatRequest(topListSize);
}
@Override
public @NotNull RequestGenerator<LinkedHashMap<String, Integer>> createTotalTopStatRequest() {
int playerCount = offlinePlayerHandler.getIncludedPlayerCount();
return createTopStatRequest(playerCount);
}
@Override
public @NotNull StatResult<LinkedHashMap<String, Integer>> executeTopRequest(@NotNull StatRequest<LinkedHashMap<String, Integer>> 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<Integer> 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<Long> 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<LinkedHashMap<String, Integer>> processTopRequest(StatRequest.Settings requestSettings) {
LinkedHashMap<String, Integer> 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<Integer> numbers = getAllStatsAsync(requestSettings)
.values()
.parallelStream()
.toList();
return numbers.parallelStream().mapToLong(Integer::longValue).sum();
}
private LinkedHashMap<String, Integer> 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<String, Integer> getAllStatsAsync(StatRequest.Settings requestSettings) {
long time = System.currentTimeMillis();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
ConcurrentHashMap<String, Integer> 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;
}
}
}

View File

@ -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<Long> implements RequestGenerator<Long> {
public ServerStatRequest() {
this(Bukkit.getConsoleSender());
}
public ServerStatRequest(CommandSender sender) {
super(sender);
super.configureForServer();
}
@Override
public boolean isValid() {
return super.hasMatchingSubStat();
}
@Override
public StatRequest<Long> untyped(@NotNull Statistic statistic) {
super.configureUntyped(statistic);
return this;
}
@Override
public StatRequest<Long> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
super.configureBlockOrItemType(statistic, material);
return this;
}
@Override
public StatRequest<Long> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
super.configureEntityType(statistic, entityType);
return this;
}
}

View File

@ -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<LinkedHashMap<String, Integer>> implements RequestGenerator<LinkedHashMap<String, Integer>> {
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<LinkedHashMap<String, Integer>> untyped(@NotNull Statistic statistic) {
super.configureUntyped(statistic);
return this;
}
@Override
public StatRequest<LinkedHashMap<String, Integer>> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
super.configureBlockOrItemType(statistic, material);
return this;
}
@Override
public StatRequest<LinkedHashMap<String, Integer>> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
super.configureEntityType(statistic, entityType);
return this;
}
}

View File

@ -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 @@ import java.util.stream.Stream;
*/
public final class EnumHandler {
private static volatile EnumHandler instance;
private static List<String> blockNames;
private static List<String> itemNames;
private static List<String> statNames;
private static List<String> 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<String> getBlockNames() {
public List<String> getAllBlockNames() {
return blockNames;
}
@ -45,7 +60,7 @@ public final class EnumHandler {
*
* @return the List
*/
public List<String> getItemNames() {
public List<String> getAllItemNames() {
return itemNames;
}
@ -54,7 +69,7 @@ public final class EnumHandler {
*
* @return the List
*/
public List<String> getStatNames() {
public List<String> getAllStatNames() {
return statNames;
}
@ -65,7 +80,7 @@ public final class EnumHandler {
* @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 final class EnumHandler {
* @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 final class EnumHandler {
* @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 final class EnumHandler {
* @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 final class EnumHandler {
* @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 final class EnumHandler {
* @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) {

View File

@ -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<String, Object> 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<String> existingList = fileConfiguration.getStringList(key);
List<String> 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<String> 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 <a href="https://github.com/tchristofferson/Config-Updater">tchristofferson's Config-Updater</a>
*/
private void updateFile() {
JavaPlugin plugin = Main.getPluginInstance();
try {
ConfigUpdater.update(plugin, fileName, file);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -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 final class MyLogger {
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 final class MyLogger {
}
}
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)");
}
}

View File

@ -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<String, UUID> includedPlayerUUIDs;
private static ConcurrentHashMap<String, UUID> 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<String> 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<String> 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<String> 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<OfflinePlayer> banList = Bukkit.getBannedPlayers();
return Arrays.stream(Bukkit.getOfflinePlayers())
.parallel()
.filter(Predicate.not(banList::contains))
.toArray(OfflinePlayer[]::new);
}
}

View File

@ -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.

View File

@ -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();
}
}

View File

@ -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:
* <br>[player-name]: [number] [stat-name] {sub-stat-name}
*/
TextComponent formatAndSavePlayerStat(RequestSettings requestSettings, int playerStat);
/** @return a TextComponent with the following parts:
* <br>[Total on] [server-name]: [number] [stat-name] [sub-stat-name]
*/
TextComponent formatAndSaveServerStat(RequestSettings requestSettings, long serverStat);
/** @return a TextComponent with the following parts:
* <br>[PlayerStats] [Top 10] [stat-name] [sub-stat-name]
* <br> [1.] [player-name] [number]
* <br> [2.] [player-name] [number]
* <br> [3.] etc...
*/
TextComponent formatAndSaveTopStat(RequestSettings requestSettings, LinkedHashMap<String, Integer> topStats);
}

View File

@ -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<StandardMessage, Function<MessageBuilder, TextComponent>> 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<Integer, CommandSender, TextComponent> playerStatFunction =
getMessageBuilder(requestSettings).formattedPlayerStatFunction(playerStat, requestSettings);
return processFunction(requestSettings.getCommandSender(), playerStatFunction);
}
@Override
public TextComponent formatAndSaveServerStat(@NotNull RequestSettings requestSettings, long serverStat) {
BiFunction<Integer, CommandSender, TextComponent> serverStatFunction =
getMessageBuilder(requestSettings).formattedServerStatFunction(serverStat, requestSettings);
return processFunction(requestSettings.getCommandSender(), serverStatFunction);
}
@Override
public TextComponent formatAndSaveTopStat(@NotNull RequestSettings requestSettings, @NotNull LinkedHashMap<String, Integer> topStats) {
BiFunction<Integer, CommandSender, TextComponent> 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<Integer, CommandSender, TextComponent> 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));
}
}

View File

@ -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 = "<rainbow:16>____________ [PlayerStats] ____________</rainbow>"; //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>[</#f74040>" +
"<#F54D39>P</#F54D39>" +
"<#F16E28>l</#F16E28>" +
"<#ee8a19>a</#ee8a19>" +
"<#EEA019>y</#EEA019>" +
"<#F7C522>e</#F7C522>" +
"<#C1DA15>r</#C1DA15>" +
"<#84D937>S</#84D937>" +
"<#46D858>t</#46D858>" +
"<#01c1a7>a</#01c1a7>" +
"<#1F8BEB>t</#1F8BEB>" +
"<#3341E6>s</#3341E6>" +
"<#631ae6>]</#631ae6>"))
.build();
}
private TextComponent backwardsPluginPrefixComponent() {
return text()
.append(MiniMessage.miniMessage()
.deserialize("<#631ae6>[</#631ae6>" +
"<#3341E6>P</#3341E6>" +
"<#1F8BEB>l</#1F8BEB>" +
"<#01c1a7>a</#01c1a7>" +
"<#46D858>y</#46D858>" +
"<#84D937>e</#84D937>" +
"<#C1DA15>r</#C1DA15>" +
"<#F7C522>S</#F7C522>" +
"<#EEA019>t</#EEA019>" +
"<#ee8a19>a</#ee8a19>" +
"<#f67824>t</#f67824>" +
"<#f76540>s</#f76540>" +
"<#f74040>]</#f74040>"))
.build();
}
}

View File

@ -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<String, UUID> 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<OfflinePlayer> 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<String, UUID> 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;
}
}

View File

@ -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<String, Integer> 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<Integer> 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<String, Integer> getAllStatsAsync(RequestSettings requestSettings) {
long time = System.currentTimeMillis();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
ConcurrentHashMap<String, Integer> 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<String, Integer> allStats = new ConcurrentHashMap<>(size);
ImmutableList<String> playerNames = ImmutableList.copyOf(offlinePlayerHandler.getOfflinePlayerNames());
StatAction task = new StatAction(offlinePlayerHandler, playerNames, requestSettings, allStats);
MyLogger.actionCreated(playerNames.size());
return task;
}
}

View File

@ -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);
}
}
}
}

View File

@ -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<Integer> implements RequestGenerator<Integer> {
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);
}
}

View File

@ -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:
* <ul>
* <li> a <code>statName</code> (example: "mine_block")
* <li> if applicable, a <code>subStatEntryName</code> (example: diorite)
* <li> a <code>target</code> for this lookup: can be "top", "server", "player"
* (or "me" to indicate the current CommandSender)
* <li> if "player" was chosen, include a <code>playerName</code>
* </ul>
* @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);
}
}
}
}
}

View File

@ -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:
* <br>- Internally: by PlayerStats itself when /stat is called,
* using the args provided by the CommandSender.
* <br>- Externally: through the API methods provided by the
* {@link RequestGenerator} interface.
* <br>
* <br>For this RequestSettings object to be valid, the following
* values need to be set:
* <ul>
* <li> a {@link Statistic} <code>statistic</code> </li>
* <li> if this Statistic is not of {@link Statistic.Type} Untyped,
* a <code>subStatEntryName</code> needs to be set, together with one
* of the following values:
* <br>- for Type.Block: a {@link Material} <code>blockMaterial</code>
* <br>- for Type.Item: a {@link Material} <code>itemMaterial</code>
* <br>- for Type.Entity: an {@link EntityType} <code>entityType</code>
* <li> a {@link Target} <code>target</code> (defaults to Top)
* <li> if the <code>target</code> is Target.Player, a
* <code>playerName</code> needs to be added
* </ul>
*/
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:
* <br>- CommandSender sender (provided)
* <br>- Target target = {@link Target#TOP}
* <br>- int topListSize = 10
* <br>- 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;
}
}
}
}

View File

@ -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<Long> implements RequestGenerator<Long> {
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);
}
}

View File

@ -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<T> {
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<T> 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();
}
}

View File

@ -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<LinkedHashMap<String, Integer>> implements RequestGenerator<LinkedHashMap<String, Integer>> {
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<String, Integer> stat = Main
.getStatCalculator()
.getTopStats(completedRequest);
TextComponent prettyComponent = Main
.getStatFormatter()
.formatAndSaveTopStat(completedRequest, stat);
String prettyString = ComponentUtils
.getTranslatableComponentSerializer()
.serialize(prettyComponent);
return new TopStatResult(stat, prettyComponent, prettyString);
}
}

View File

@ -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<Integer> {
/**
* 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);
}
}

View File

@ -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<Integer> {
@Override
public Integer getNumericalValue() {
return value;
}
@Override
public TextComponent getFormattedTextComponent() {
return formattedComponent;
}
@Override
public String getFormattedString() {
return formattedString;
}
}

View File

@ -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<Long> {
@Override
public Long getNumericalValue() {
return value;
}
@Override
public TextComponent getFormattedTextComponent() {
return formattedComponent;
}
@Override
public String getFormattedString() {
return formattedString;
}
}

View File

@ -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<String, Integer> value, TextComponent formattedComponent, String formattedString) implements StatResult<LinkedHashMap<String,Integer>> {
@Override
public LinkedHashMap<String, Integer> getNumericalValue() {
return value;
}
@Override
public TextComponent getFormattedTextComponent() {
return formattedComponent;
}
@Override
public String getFormattedString() {
return formattedString;
}
}

View File

@ -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<String, UUID> offlinePlayerUUIDs;
private static ArrayList<String> 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<String, UUID> 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<String> 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");
}
}
}

View File

@ -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 # #

View File

@ -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:
-

View File

@ -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"

View File

@ -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