Merge branch 'api'

# Conflicts:
#	src/main/java/com/artemis/the/gr8/playerstats/enums/Unit.java
#	src/main/java/com/artemis/the/gr8/playerstats/msg/msgutils/NumberFormatter.java
This commit is contained in:
Artemis-the-gr8 2022-08-17 13:14:46 +02:00
commit 320e6a6c22
90 changed files with 4335 additions and 3007 deletions

View File

@ -7,7 +7,8 @@
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="JavadocGenerationManager">
<option name="OUTPUT_DIRECTORY" value="$PROJECT_DIR$/javadoc" />
<option name="OUTPUT_DIRECTORY" value="$PROJECT_DIR$/target/apidocs" />
<option name="OPTION_SCOPE" value="public" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">

View File

@ -1,9 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.artemis.the.gr8</groupId>
<groupId>io.github.ithotl</groupId>
<artifactId>PlayerStats</artifactId>
<version>1.6.1</version>
<name>PlayerStats</name>
<version>1.7</version>
<description>Statistics Plugin</description>
<url>https://www.spigotmc.org/resources/playerstats.102347/</url>
<developers>
<developer>
<name>Artemis</name>
<email>artemis.the.gr8@gmail.com</email>
<url>https://github.com/Artemis-the-gr8</url>
</developer>
</developers>
<licenses>
<license>
<name>MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<scm>
<connection>scm:git:git://github.com/itHotL/PlayerStats.git</connection>
<developerConnection>scm:git:git://github.com/itHotL/PlayerStats.git</developerConnection>
<url>https://github.com/itHotL/PlayerStats/tree/main</url>
</scm>
<build>
<plugins>
<plugin>
@ -28,7 +49,7 @@
<configuration>
<transformers>
<transformer>
<mainClass>com.gmail.artemis.the.gr8.playerstats.Main</mainClass>
<mainClass>com.artemis.the.gr8.playerstats.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
@ -39,15 +60,15 @@
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.lib.kyori</shadedPattern>
<shadedPattern>com.artemis.the.gr8.lib.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.tchristofferson</shadedPattern>
<shadedPattern>com.artemis.the.gr8.util.tchristofferson</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.bstats</shadedPattern>
<shadedPattern>com.artemis.the.gr8.util.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
@ -92,6 +113,66 @@
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
<failOnError>false</failOnError>
<quiet>true</quiet>
<show>protected</show>
<groups>
<group>
<title>API</title>
<packages>com.artemis.the.gr8.playerstats.api</packages>
</group>
</groups>
<detectLinks>true</detectLinks>
<dependencyLinks>
<dependencyLink>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<url>https://hub.spigotmc.org/javadocs/bukkit/</url>
</dependencyLink>
<dependencyLink>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<url>https://jd.adventure.kyori.net/api/4.11.0/</url>
</dependencyLink>
<dependencyLink>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<url>https://jd.adventure.kyori.net/text-minimessage/4.11.0/</url>
</dependencyLink>
<dependencyLink>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bukkit</artifactId>
<url>https://jd.adventure.kyori.net/</url>
</dependencyLink>
</dependencyLinks>
</configuration>
</plugin>
</plugins>
</build>
<repositories>

103
pom.xml
View File

@ -4,9 +4,34 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.artemis.the.gr8</groupId>
<groupId>io.github.ithotl</groupId>
<artifactId>PlayerStats</artifactId>
<version>1.6.1</version>
<version>1.7</version>
<name>PlayerStats</name>
<description>Statistics Plugin</description>
<url>https://www.spigotmc.org/resources/playerstats.102347/</url>
<licenses>
<license>
<name>MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
</license>
</licenses>
<developers>
<developer>
<name>Artemis</name>
<email>artemis.the.gr8@gmail.com</email>
<url>https://github.com/Artemis-the-gr8</url>
</developer>
</developers>
<scm>
<url>https://github.com/itHotL/PlayerStats/tree/main</url>
<connection>scm:git:git://github.com/itHotL/PlayerStats.git</connection>
<developerConnection>scm:git:git://github.com/itHotL/PlayerStats.git</developerConnection>
</scm>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@ -14,7 +39,6 @@
<maven.compiler.target>16</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>spigot-repo</id> <!-- Spigot API -->
@ -117,7 +141,7 @@
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.gmail.artemis.the.gr8.playerstats.Main</mainClass>
<mainClass>com.artemis.the.gr8.playerstats.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
@ -128,15 +152,15 @@
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.lib.kyori</shadedPattern>
<shadedPattern>com.artemis.the.gr8.lib.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.tchristofferson</shadedPattern>
<shadedPattern>com.artemis.the.gr8.util.tchristofferson</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.bstats</shadedPattern>
<shadedPattern>com.artemis.the.gr8.util.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
@ -182,6 +206,71 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.0.1</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<additionalOptions>-Xdoclint:none</additionalOptions>
<failOnError>false</failOnError>
<quiet>true</quiet>
<show>protected</show>
<groups>
<group>
<title>API</title>
<packages>com.artemis.the.gr8.playerstats.api</packages>
</group>
</groups>
<detectLinks>true</detectLinks>
<dependencyLinks>
<dependencyLink>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<url>https://hub.spigotmc.org/javadocs/bukkit/</url>
</dependencyLink>
<dependencyLink>
<groupId>net.kyori</groupId>
<artifactId>adventure-api</artifactId>
<url>https://jd.adventure.kyori.net/api/4.11.0/</url>
</dependencyLink>
<dependencyLink>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<url>https://jd.adventure.kyori.net/text-minimessage/4.11.0/</url>
</dependencyLink>
<dependencyLink>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bukkit</artifactId>
<url>https://jd.adventure.kyori.net/</url>
</dependencyLink>
</dependencyLinks>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,189 @@
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,9 +1,8 @@
package com.gmail.artemis.the.gr8.playerstats;
package com.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.InternalStatResult;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
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 net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
@ -19,8 +18,12 @@ import java.util.concurrent.atomic.AtomicInteger;
import static java.time.temporal.ChronoUnit.SECONDS;
/** The manager of all Player-prompted statistic-sharing. If sharing is enabled, this class will save the
results of past stat-lookups, so the results can be retrieved and shared when a Player clicks the share-button.*/
/**
* The manager of all Player-prompted statistic-sharing.
* If sharing is enabled, this class will save the results
* of past stat-lookups, so the results can be retrieved
* and shared when a Player clicks the share-button.
*/
public final class ShareManager {
private static boolean isEnabled;
@ -58,8 +61,8 @@ public final class ShareManager {
sharedResults = null;
}
if (config.allowStatSharing() && !config.useHoverText()) {
MyLogger.logMsg("Stat-sharing does not work without hover-text enabled! " +
"Enable hover-text, or disable stat-sharing to stop seeing this message.", true);
MyLogger.logWarning("Stat-sharing does not work without hover-text enabled! " +
"Enable hover-text, or disable stat-sharing to stop seeing this message.");
}
}
}
@ -76,7 +79,7 @@ public final class ShareManager {
InternalStatResult result = new InternalStatResult(playerName, statResult, ID);
int shareCode = result.hashCode();
statResultQueue.put(shareCode, result);
MyLogger.logMsg("Saving statResults with no. " + ID, DebugLevel.MEDIUM);
MyLogger.logMediumLevelMsg("Saving statResults with no. " + ID);
return shareCode;
}
@ -93,17 +96,20 @@ public final class ShareManager {
return sharedResults.contains(shareCode);
}
/** Takes a formattedValue from the internal ConcurrentHashmap,
puts the current time in the shareTimeStamp (ConcurrentHashMap),
puts the shareCode (int hashCode) in the sharedResults (ArrayBlockingQueue),
and returns the formattedValue. If no formattedValue was found, returns null.*/
/**
* Takes a formattedComponent from the internal ConcurrentHashmap,
* puts the current time in the shareTimeStamp (ConcurrentHashMap),
* puts the shareCode (int hashCode) in the sharedResults (ArrayBlockingQueue),
* and returns the formattedComponent. If no formattedComponent was found,
* returns null.
*/
public @Nullable InternalStatResult getStatResult(String playerName, int shareCode) {
if (statResultQueue.containsKey(shareCode)) {
shareTimeStamp.put(playerName, Instant.now());
if (!sharedResults.offer(shareCode)) { //create a new ArrayBlockingQueue if our queue is full
MyLogger.logMsg("500 stat-results have been shared, " +
"creating a new internal queue with the most recent 50 share-code-values and discarding the rest...", DebugLevel.MEDIUM);
MyLogger.logMediumLevelMsg("500 stat-results have been shared, " +
"creating a new internal queue with the most recent 50 share-code-values and discarding the rest...");
ArrayBlockingQueue<Integer> newQueue = new ArrayBlockingQueue<>(500);
synchronized (this) { //put the last 50 values in the new Queue
@ -123,8 +129,10 @@ public final class ShareManager {
}
}
/** If the given player already has more than x (in this case 25) StatResults saved,
remove the oldest one.*/
/**
* If the given player already has more than x (in this case 25)
* StatResults saved, remove the oldest one.
*/
private void removeExcessResults(String playerName) {
List<InternalStatResult> alreadySavedResults = statResultQueue.values()
.parallelStream()
@ -136,7 +144,7 @@ public final class ShareManager {
.parallelStream()
.min(Comparator.comparing(InternalStatResult::ID))
.orElseThrow().hashCode();
MyLogger.logMsg("Removing old stat no. " + statResultQueue.get(hashCode).ID() + " for player " + playerName, DebugLevel.MEDIUM);
MyLogger.logMediumLevelMsg("Removing old stat no. " + statResultQueue.get(hashCode).ID() + " for player " + playerName);
statResultQueue.remove(hashCode);
}
}

View File

@ -0,0 +1,105 @@
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

@ -0,0 +1,252 @@
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 net.kyori.adventure.text.TextComponent;
import org.bukkit.Statistic;
import org.jetbrains.annotations.Nullable;
/**
* Formats messages meant for usage outside PlayerStats.
* <p> For more information about the default formatting
* PlayerStats uses, see the class description of
* StatResult.
* @see StatResult
*/
public interface ApiFormatter {
/**
* Turns a TextComponent into its String representation. This method is equipped
* to turn all PlayerStats' formatted statResults into String, using a custom
* Serializer.
*
* @param component the Component to turn into String
* @return a String representation of this TextComponent, without hover/click events,
* 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();
}
/**
* Gets the default prefix PlayerStats uses.
* @return [PlayerStats]
*/
TextComponent getPluginPrefix();
/**
* Gets the special rainbow version of PlayerStats' prefix.
* @return [PlayerStats] in rainbow color
* s*/
TextComponent getRainbowPluginPrefix();
/**
* Gets the version of the prefix that is surrounded by underscores.
* This is meant to be used as a title above a message or statistic display.
* @return ________ [PlayerStats] ________
*/
TextComponent getPluginPrefixAsTitle();
/**
* Gets the special rainbow version of the title-prefix.
*
* @return ________ [PlayerStats] ________ in rainbow colors
*/
TextComponent getRainbowPluginPrefixAsTitle();
/**
* Gets a formatted message that displays the name of this Statistic as it is
* displayed by PlayerStats. If this Statistic is not of Type.Untyped,
* include the name of the relevant sub-statistic (block, item or entity).
*
* @param statistic the Statistic enum constant to display the name of
* @param subStatName where necessary, the name of the Material or EntityType
* to include, acquired by doing #toString() on the Material/EntityType in question
* @return [stat-name] [sub-stat-name]
*/
TextComponent getStatTitle(Statistic statistic, @Nullable String subStatName);
/**
* Gets a formatted message that displays the name of this Statistic as it is
* displayed by PlayerStats in a top-stat-message. If this Statistic is not
* of Type.Untyped, include the name of the relevant sub-statistic
* (block, item or entity).
*
* @param statistic the Statistic enum constant for this message
* @param subStatName the name of the Material or EntityType to include,
* acquired by doing #toString() on the Material/EntityType in question
* @param topStatSize the size of the top-list this title is for
* @return Top [topStatSize] [stat-name] [sub-stat-name]
*/
TextComponent getTopStatTitle(int topStatSize, Statistic statistic, @Nullable String subStatName);
/**
* Formats the input into a single top-statistic line. The stat-number
* is formatted into the most suitable {@link Unit} based on the provided
* Statistic. For Type.Time, the resulting formatted number will have as
* many additional smaller units as are specified in the config,
* unless <code>formatTopStatLineForTypeTime()</code> is used.
*
* @param positionInTopList the rank-number in this list of the Player
* @param playerName the name of the Player on this line
* @param statNumber the result of Player#getStatistic()
* @param statistic the Statistic enum constant for this message
* @return a single line from a top-x statistic:
* <br> [positionInTopList]. [player-name] ......... [stat-number]
*/
TextComponent formatTopStatLine(int positionInTopList, String playerName, long statNumber, Statistic statistic);
/**
* Formats the input into a single top-statistic line. The stat-number
* is formatted into the provided {@link Unit}. For Type.Time, the
* resulting formatted number will have as many additional smaller
* units as are specified in the config, unless
* <code>formatTopStatLineForTypeTime()</code> is used.
*
* @param positionInTopList the rank-number in this list of the Player
* @param playerName the name of the Player on this line
* @param statNumber the result of Player#getStatistic()
* @param unit the Unit to format the <code>statNumber</code> with
* @return a single line from a top-x statistic:
* <br> [positionInTopList]. [player-name] ......... [stat-number]
* */
TextComponent formatTopStatLine(int positionInTopList, String playerName, long statNumber, Unit unit);
/**
* Formats the input into a single top-statistic line for a time-based
* statistic with the Unit-range that is between <code>bigUnit</code>
* and <code>smallUnit</code> (both inclusive).
*
* @param positionInTopList the rank-number in this list of the Player
* @param playerName the name of the Player on this line
* @param statNumber the result of Player#getStatistic()
* @param bigUnit the biggest Unit to use of {@link Unit.Type#TIME}
* @param smallUnit the smallest Unit to use of {@link Unit.Type#TIME}
* @return a single line from a stop-x statistic:
* <br>[positionInTopList]. [player-name] ......... [1D 2H 3M 4S]
*/
TextComponent formatTopStatLineForTypeTime(int positionInTopList, String playerName, long statNumber, Unit bigUnit, Unit smallUnit);
/**
* Formats the input into a server statistic message. The stat-number
* is formatted into the most suitable {@link Unit} based on the provided
* Statistic. For Type.Time, the resulting formatted number will have as
* many additional smaller units as are specified in the config,
* unless <code>formatServerStatForTypeTime()</code> is used.
*
* @param statNumber the result of all Player#getStatistic() values combined
* @param statistic te Statistic enum constant for this message
* @return [Total on this server]: [stat-number] [stat-name]
*/
TextComponent formatServerStat(long statNumber, Statistic statistic);
/**
* Formats the input into a server statistic message for a statistic
* that has a sub-statistic (block, item or entity).
*
* @param statistic the Statistic enum constant for this message
* @param statNumber the result of all Player#getStatistic() values combined
* @param subStatName the name of the Material or EntityType of this
* statistic-lookup, acquired by doing #toString() on the Material/
* EntityType in question
* @return [Total on this server]: [stat-number] [stat-name] [sub-stat-name]
*/
TextComponent formatServerStat(long statNumber, Statistic statistic, String subStatName);
/**
* Formats the input into a server statistic message with the specified
* {@link Unit}. The stat-number is formatted into the most suitable
* {@link Unit} based on the provided Statistic. For Type.Time, the
* resulting formatted number will have as many additional smaller
* units as are specified in the config, unless
* <code>formatServerStatForTypeTime()</code> is used.
*
* @param statistic the Statistic enum constant for this message
* @param statNumber the result of all Player#getStatistic() values combined
* @param unit the Unit to use to format te <code>statNumber</code>
* @return [Total on this server]: [stat-number] [stat-name] [unit-name]
*/
TextComponent formatServerStat(long statNumber, Statistic statistic, Unit unit);
/**
* Formats the input into a server statistic message for a time-based
* statistic with the Unit-range that is between <code>bigUnit</code>
* and <code>smallUnit</code> (both inclusive).
*
* @param statistic the Statistic enum constant for this message
* @param statNumber the result of all Player#getStatistic() values combined
* @param bigUnit the biggest Unit to use of {@link Unit.Type#TIME}
* @param smallUnit the smallest Unit to use of {@link Unit.Type#TIME}
* @return [Total on this server]: [1D 2H 3M 4S] [stat-name]
*/
TextComponent formatServerStatForTypeTime(long statNumber, Statistic statistic, Unit bigUnit, Unit smallUnit);
/**
* Formats the input into a player statistic message. For Unit.Type.Time,
* the resulting formatted number will have as many additional smaller
* units as are specified in the config, unless
* <code>formatPlayerStatForTypeTime</code> is used.
*
* @param playerName the name of the Player
* @param statistic the Statistic enum constant for this message
* @param statNumber the result of Player#getStatistic()
* @return [player-name]: [stat-number] [stat-name]
*/
TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic);
/**
* Formats the input into a player statistic message for a statistic
* that has a sub-statistic (block, item or entity).
*
* @param playerName the name of the Player
* @param statistic the Statistic enum constant for this message
* @param statNumber the result of Player#getStatistic()
* @param subStatName the name of the Material or EntityType of
* this statistic-lookup, acquired by doing #toString() on the
* Material/EntityType in question
* @return [player-name]: [stat-number] [stat-name] [sub-stat-name]
*/
TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic, String subStatName);
/**
* Formats the input into a player statistic message with the specified
* {@link Unit}. For Unit.Type.Time, the resulting formatted number will
* have as many additional smaller units as are specified in the config,
* unless <code>formatPlayerStatForTypeTime</code> is used.
*
* @param playerName the name of the Player
* @param statistic the Statistic enum constant for this message
* @param statNumber the result of Player#getStatistic()
* @param unit the Unit to use when formatting the <code>statNumber</code>
* @return [player-name]: [stat-number] [stat-name] [stat-unit]
*/
TextComponent formatPlayerStat(String playerName, int statNumber, Statistic statistic, Unit unit);
/**
* Formats the input into a player statistic message for a time-based
* statistic with the Unit-range that is between <code>bigUnit</code>
* and <code>smallUnit</code> (both inclusive).
*
* @param playerName the name of the Player
* @param statNumber the result of Player#getStatistic()
* @param statistic the Statistic enum constant for this message
* @param bigUnit the biggest Unit to use of {@link Unit.Type#TIME}
* @param smallUnit the smallest Unit to use of {@link Unit.Type#TIME}
* @return [player-name]: [1D 2H 3M 4S] [stat-name]
*/
TextComponent formatPlayerStatForTypeTime(String playerName, int statNumber, Statistic statistic, Unit bigUnit, Unit smallUnit);
}

View File

@ -0,0 +1,36 @@
package com.artemis.the.gr8.playerstats.api;
import com.artemis.the.gr8.playerstats.Main;
import com.artemis.the.gr8.playerstats.statistic.request.StatRequest;
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.
*
* @see StatManager
* @see ApiFormatter
*/
public interface PlayerStats {
/** Gets an instance of the {@link PlayerStatsAPI}.
* @return the PlayerStats API
* @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();
}
StatManager getStatManager();
ApiFormatter getFormatter();
}

View File

@ -0,0 +1,53 @@
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

@ -0,0 +1,46 @@
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;
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}
* to get the desired statistic data.
*/
public interface RequestGenerator<T> {
/**
* Gets an executable Request object for a Statistic of Statistic.Type {@code Untyped}.
*
* @param statistic a Statistic of Type.Untyped
* @return a {@link StatRequest}
* @throws IllegalArgumentException if <code>statistic</code> is not of Type.Untyped
* */
StatRequest<T> untyped(@NotNull Statistic statistic) throws IllegalArgumentException;
/**
* Gets an executable Request object for a Statistic of Statistic.Type Block or Item.
*
* @param statistic a Statistic of Type.Block or Type.Item
* @param material a block if the <code>statistic</code> is of Type.Block,
* and an item if the <code>statistic</code> is of Type.Item
* @return a {@link StatRequest}
* @throws IllegalArgumentException if <code>statistic</code> is not of Type.Block
* (with a block as <code>material</code>), or <code>statistic</code> is not of Type.Item
* (with an item as <code>material</code>)
*/
StatRequest<T> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException;
/** Gets an executable Request object for a Statistic of Statistic.Type Entity.
* @param statistic a Statistic of Type.Entity
* @param entityType an EntityType
* @return a {@link StatRequest}
* @throws IllegalArgumentException if <code>statistic</code> is not of Type.Entity*/
StatRequest<T> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException;
}

View File

@ -0,0 +1,42 @@
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 {
/** 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);
/** 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();
/** Gets a RequestGenerator that can be used to create a TopStatRequest
* for a top-list of the specified size. This RequestGenerator will
* make sure all default settings for a top-statistic-lookup are configured.
*
* @param topListSize how big the top-x should be (10 by default)
* @return the RequestGenerator*/
RequestGenerator<LinkedHashMap<String, Integer>> topStatRequest(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
* PlayerStats' settings). This RequestGenerator will make sure
* all default settings for a top-statistic-lookup are configured.
*
* @return the RequestGenerator*/
RequestGenerator<LinkedHashMap<String, Integer>> totalTopStatRequest();
}

View File

@ -0,0 +1,4 @@
/**
* The PlayerStats API
*/
package com.artemis.the.gr8.playerstats.api;

View File

@ -1,6 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
package com.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.artemis.the.gr8.playerstats.ThreadManager;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;

View File

@ -1,10 +1,10 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
package com.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.ShareManager;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.InternalStatResult;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
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 org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@ -38,7 +38,7 @@ public final class ShareCommand implements CommandExecutor {
}
else {
InternalStatResult result = shareManager.getStatResult(sender.getName(), shareCode);
if (result == null) { //at this point the only possible cause of formattedValue being null is the request being older than 25 player-requests ago
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 {
outputManager.sendToAllPlayers(result.formattedValue());

View File

@ -1,11 +1,11 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
package com.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequestHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
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;
@ -32,10 +32,10 @@ public final class StatCommand implements CommandExecutor {
outputManager.sendExamples(sender);
}
else {
StatRequest baseRequest = StatRequestHandler.getBasicInternalStatRequest(sender);
StatRequestHandler statRequestHandler = new StatRequestHandler(baseRequest);
RequestSettings baseRequest = RequestHandler.getBasicInternalStatRequest(sender);
RequestHandler requestHandler = new RequestHandler(baseRequest);
StatRequest completedRequest = statRequestHandler.getRequestFromArgs(args);
RequestSettings completedRequest = requestHandler.getRequestFromArgs(args);
if (completedRequest.isValid()) {
threadManager.startStatThread(completedRequest);
} else {
@ -46,31 +46,33 @@ public final class StatCommand implements CommandExecutor {
return true;
}
/** If a given {@link StatRequest} does not result in a valid statistic look-up,
this will send a feedback message to the CommandSender that made the request.
<br> 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 statRequest the StatRequest to give feedback on
/**
* 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(StatRequest statRequest) {
CommandSender sender = statRequest.getCommandSender();
private void sendFeedback(RequestSettings requestSettings) {
CommandSender sender = requestSettings.getCommandSender();
if (statRequest.getStatistic() == null) {
if (requestSettings.getStatistic() == null) {
outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_STAT_NAME);
}
else if (statRequest.getTarget() == Target.PLAYER && statRequest.getPlayerName() == null) {
else if (requestSettings.getTarget() == Target.PLAYER && requestSettings.getPlayerName() == null) {
outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_PLAYER_NAME);
}
else {
Statistic.Type type = statRequest.getStatistic().getType();
if (type != Statistic.Type.UNTYPED && statRequest.getSubStatEntryName() == null) {
Statistic.Type type = requestSettings.getStatistic().getType();
if (type != Statistic.Type.UNTYPED && requestSettings.getSubStatEntryName() == null) {
outputManager.sendFeedbackMsgMissingSubStat(sender, type);
} else {
outputManager.sendFeedbackMsgWrongSubStat(sender, type, statRequest.getSubStatEntryName());
outputManager.sendFeedbackMsgWrongSubStat(sender, type, requestSettings.getSubStatEntryName());
}
}
}

View File

@ -1,8 +1,8 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
package com.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.commands.cmdutils.TabCompleteHelper;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
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;

View File

@ -1,6 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.commands.cmdutils;
package com.artemis.the.gr8.playerstats.commands.cmdutils;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.artemis.the.gr8.playerstats.utils.EnumHandler;
import org.bukkit.Material;
import org.bukkit.entity.EntityType;

View File

@ -0,0 +1,586 @@
package com.artemis.the.gr8.playerstats.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 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;
/** Handles all PlayerStats' config-settings. */
public final class ConfigHandler {
private static Main plugin;
private static int configVersion;
private File configFile;
private FileConfiguration config;
public ConfigHandler(Main plugin) {
ConfigHandler.plugin = plugin;
configVersion = 6;
saveDefaultConfig();
config = YamlConfiguration.loadConfiguration(configFile);
checkConfigVersion();
MyLogger.setDebugLevel(getDebugLevel());
}
/**
* Checks the number that "config-version" returns to see if the
* config needs updating, and if so, send it to the {@link ConfigUpdateHandler}.
* <br>
* <br>PlayerStats 1.1: "config-version" doesn't exist.
* <br>PlayerStats 1.2: "config-version" is 2.
* <br>PlayerStats 1.3: "config-version" is 3.
* <br>PlayerStats 1.4: "config-version" is 4.
* <br>PlayerStats 1.5: "config-version" is 5.
* <br>PlayerStats 1.6 and up: "config-version" is 6.
*/
private void checkConfigVersion() {
if (!config.contains("config-version") || config.getInt("config-version") != configVersion) {
new ConfigUpdateHandler(plugin, configFile, configVersion);
reloadConfig();
}
}
/**
* 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");
}
/**
* 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;
}
}
/** Returns the desired debugging level.
*
* <br> 1 = low (only show unexpected errors)
* <br> 2 = medium (detail all encountered exceptions, log main tasks and show time taken)
* <br> 3 = high (log all tasks and time taken)
*
* @return the DebugLevel (default: 1)
*/
public int getDebugLevel() {
return config.getInt("debug-level", 1);
}
/**
* Whether command-senders should be limited to one stat-request at a time.
* @return the config setting (default: true)
*/
public boolean limitStatRequests() {
return config.getBoolean("only-allow-one-lookup-at-a-time-per-player", true);
}
/**
* Whether stat-sharing is allowed.
* @return the config setting (default: true)
*/
public boolean allowStatSharing() {
return config.getBoolean("enable-stat-sharing", true);
}
/**
* The number of minutes a player has to wait before being able to
* share another stat-result.
* @return the number (default: 0)
*/
public int getStatShareWaitingTime() {
return config.getInt("waiting-time-before-sharing-again", 0);
}
/**
* Whether to limit stat-calculations to whitelisted players only.
* @return the config setting (default: true)
*/
public boolean whitelistOnly() {
return config.getBoolean("include-whitelist-only", false);
}
/**
* Whether to exclude banned players from stat-calculations.
* @return the config setting for exclude-banned-players (default: false)
*/
public boolean excludeBanned() {
return config.getBoolean("exclude-banned-players", false);
}
/**
* The number of maximum days since a player has last been online.
* @return the number (default: 0 - which signals not to use this limit)
*/
public int getLastPlayedLimit() {
return config.getInt("number-of-days-since-last-joined", 0);
}
/**
* Whether to use TranslatableComponents wherever possible.
*
* @return the config setting (default: true)
* @implNote Currently supported: statistic, block, item and entity names.
*/
public boolean useTranslatableComponents() {
return config.getBoolean("translate-to-client-language", true);
}
/**
* Whether to use HoverComponents for additional information
* @return the config setting (default: true)
*/
public boolean useHoverText() {
return config.getBoolean("enable-hover-text", true);
}
/**
* Whether to use festive formatting, such as pride colors
* @return the config setting (default: true)
*/
public boolean useFestiveFormatting() {
return config.getBoolean("enable-festive-formatting", true);
}
/**
* Whether to use rainbow colors for the [PlayerStats] prefix rather than the
* default gold/purple
* @return the config setting (default: false)
*/
public boolean useRainbowMode() {
return config.getBoolean("rainbow-mode", false);
}
/**
* Whether to use enters before the statistic output in chat
*
* @param selection the Target (Player, Server or Top)
* @return the config setting (default: true for non-shared top
* statistics, false for everything else)
*/
public boolean useEnters(Target selection, boolean getSharedSetting) {
ConfigurationSection section = config.getConfigurationSection("use-enters");
boolean def = selection == Target.TOP && !getSharedSetting;
if (section != null) {
String path = switch (selection) {
case TOP -> getSharedSetting ? "top-stats-shared" : "top-stats";
case PLAYER -> getSharedSetting ? "player-stats-shared" : "player-stats";
case SERVER -> getSharedSetting ? "server-stats-shared" : "server-stats";
};
return section.getBoolean(path, def);
}
MyLogger.logWarning("Config settings for use-enters could not be retrieved! " +
"Please check your file if you want to use custom settings. " +
"Using default values...");
return def;
}
/**
* Whether dots should be used to align the numbers in a top-stat-result.
* @return the config setting (default: true)
*/
public boolean useDots() {
return config.getBoolean("use-dots", true);
}
/**
* The maximum size for the top-stat-list.
* @return the config setting (default: 10)
*/
public int getTopListMaxSize() {
return config.getInt("top-list-max-size", 10);
}
/**
* The title that a top-statistic should start with.
* @return a String that represents the title for a top statistic
* (default: "Top")
*/
public String getTopStatsTitle() {
return config.getString("top-list-title", "Top");
}
/**
* The title that a server statistic should start with.
* @return the title (default: "Total on")
*/
public String getServerTitle() {
return config.getString("total-server-stat-title", "Total on");
}
/**
* The specified server name for a server stat title.
* @return the title (default: "this server")
*/
public String getServerName() {
return config.getString("your-server-name", "this server");
}
/**
* The unit that should be used for distance-related statistics.
*
* @param isUnitForHoverText whether the number formatted with this
* Unit is inside a HoverComponent
* @return the Unit (default: Blocks for plain text, km for hover-text)
*/
public String getDistanceUnit(boolean isUnitForHoverText) {
return getUnitString(isUnitForHoverText, "blocks", "km", "distance-unit");
}
/**
* The unit that should be used for damage-based statistics.
*
* @param isUnitForHoverText whether the number formatted with this
* Unit is inside a HoverComponent
* @return the Unit (default: Hearts for plain text, HP for hover-text)
*/
public String getDamageUnit(boolean isUnitForHoverText) {
return getUnitString(isUnitForHoverText, "hearts", "hp", "damage-unit");
}
/**
* Whether PlayerStats should automatically detect the most suitable
* unit to use for time-based statistics
*
* @param isUnitForHoverText whether the number formatted with this
* Unit is inside a HoverComponent
* @return the config setting (default: true)
*/
public boolean autoDetectTimeUnit(boolean isUnitForHoverText) {
String path = "auto-detect-biggest-time-unit";
if (isUnitForHoverText) {
path = path + "-for-hover-text";
}
boolean defaultValue = !isUnitForHoverText;
return config.getBoolean(path, defaultValue);
}
/**
* How many additional units should be displayed next to the most
* suitable largest unit for time-based statistics
*
* @param isUnitForHoverText whether the number formatted with this
* Unit is inside a HoverComponent
* @return the config setting (default: 1 for plain text,
* 0 for hover-text)
*/
public int getNumberOfExtraTimeUnits(boolean isUnitForHoverText) {
String path = "number-of-extra-units";
if (isUnitForHoverText) {
path = path + "-for-hover-text";
}
int defaultValue = isUnitForHoverText ? 0 : 1;
return config.getInt(path, defaultValue);
}
/**
* The largest unit that should be used for time-based statistics.
*
* @param isUnitForHoverText whether the number formatted with this
* Unit is inside a HoverComponent
* @return a String representation of the largest time-unit
* (default: days for plain text, hours for hover-text)
*/
public String getTimeUnit(boolean isUnitForHoverText) {
return getTimeUnit(isUnitForHoverText, false);
}
/**
* The unit that should be used for time-based statistics.
* If the optional smallUnit flag is true, this will return
* the smallest unit (and otherwise the biggest).
*
* @param isUnitForHoverText whether the number formatted with this
* Unit is inside a HoverComponent
* @param smallUnit if this is true, get the minimum time-unit
* @return the Unit (default: hours for plain text, seconds for hover-text)
*/
public String getTimeUnit(boolean isUnitForHoverText, boolean smallUnit) {
if (smallUnit) {
return getUnitString(isUnitForHoverText, "hours", "seconds", "smallest-time-unit");
}
return getUnitString(isUnitForHoverText, "days", "hours", "biggest-time-unit");
}
/**
* Returns an integer between 0 and 100 that represents how much lighter
* a hoverColor should be.
* @return an {@code int} that represents a percentage (default: 20)
*/
public int getHoverTextAmountLighter() {
return config.getInt("hover-text-amount-lighter", 20);
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or a Style.
*
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "italic"
* <br>Color: "gray"
*/
public String getSharedByTextDecoration(boolean getStyleSetting) {
String def = getStyleSetting ? "italic" : "gray";
return getDecorationString(null, getStyleSetting, def, "shared-by");
}
/**
* Gets a String that represents either a Chat Color, hex color code, or a Style.
*
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "#845EC2"
*/
public String getSharerNameDecoration(boolean getStyleSetting) {
return getDecorationString(null, getStyleSetting, "#845EC2", "player-name");
}
/**
* Gets a String that represents either a Chat Color, hex color code, or a Style.
*
* @param selection the Target (Player, Server or Top)
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color Top: "green"
* <br>Color Individual/Server: "gold"
*/
public String getPlayerNameDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "green";
}
else {
def = "gold";
}
return getDecorationString(selection, getStyleSetting, def, "player-names");
}
/**
* Whether the playerNames Style is "bold" for a top-stat.
* @return the config setting (default: false)
*/
public boolean playerNameIsBold() {
ConfigurationSection style = getRelevantSection(Target.TOP);
if (style != null) {
String styleString = style.getString("player-names");
return styleString != null && styleString.equalsIgnoreCase("bold");
}
return false;
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or a Style.
*
* @param selection the Target (Player, Server or Top)
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "yellow"
*/
public String getStatNameDecoration(Target selection, boolean getStyleSetting) {
return getDecorationString(selection, getStyleSetting, "yellow", "stat-names");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or a Style.
*
* @param selection the Target (Player, Server or Top)
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "#FFD52B"
*/
public String getSubStatNameDecoration(Target selection, boolean getStyleSetting) {
return getDecorationString(selection, getStyleSetting, "#FFD52B", "sub-stat-names");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or Style.
*
* @param selection the Target (Player, Server or Top)
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color Top: "#55AAFF"
* <br>Color Individual/Server: "#ADE7FF"
*/
public String getStatNumberDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "#55AAFF";
}
else {
def = "#ADE7FF";
}
return getDecorationString(selection, getStyleSetting, def,"stat-numbers");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or Style.
*
* @param selection the Target (Player, Server or Top)
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color Top: "yellow"
* <br>Color Server: "gold"
*/
public String getTitleDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "yellow";
}
else {
def = "gold";
}
return getDecorationString(selection, getStyleSetting, def, "title");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or Style.
*
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "gold"
*/
public String getTitleNumberDecoration(boolean getStyleSetting) {
return getDecorationString(Target.TOP, getStyleSetting, "gold", "title-number");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or Style.
*
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "#FFB80E"
*/
public String getServerNameDecoration(boolean getStyleSetting) {
return getDecorationString(Target.SERVER, getStyleSetting, "#FFB80E", "server-name");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or Style.
*
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "gold"
*/
public String getRankNumberDecoration(boolean getStyleSetting) {
return getDecorationString(Target.TOP, getStyleSetting, "gold", "rank-numbers");
}
/**
* Gets a String that represents either a Chat Color, hex color code,
* or Style.
*
* @param getStyleSetting if true, returns a Style instead of a Color
* @return the config setting. Default:
* <br>Style: "none"
* <br>Color: "dark_gray"
*/
public String getDotsDecoration(boolean getStyleSetting) {
return getDecorationString(Target.TOP, getStyleSetting, "dark_gray", "dots");
}
/**
* Gets a String representing a {@link Unit}.
*
* @return a String representing the {@link Unit} that should be used for a
* certain {@link Unit.Type}. If no String can be retrieved from the config,
* the supplied defaultValue will be returned. If the defaultValue is different
* for hoverText, an optional String defaultHoverValue can be supplied.
* @param isHoverText if true, the unit for hovering text is returned,
* otherwise the unit for plain text
* @param defaultValue the default unit for plain text
* @param defaultHoverValue the default unit for hovering text
* @param pathName the config path to retrieve the value from
*/
private String getUnitString(boolean isHoverText, String defaultValue, String defaultHoverValue, String pathName) {
String path = isHoverText ? pathName + "-for-hover-text" : pathName;
String def = defaultValue;
if (isHoverText && defaultHoverValue != null) {
def = defaultHoverValue;
}
return config.getString(path, def);
}
/**
* @return the config value for a color or style option in string-format,
* the supplied default value, or null if no configSection was found.
* @param selection the Target this decoration is meant for (Player, Server or Top)
* @param getStyleSetting if true, the result will be a style String,
* otherwise a color String
* @param defaultColor the default color to return if the config value cannot be found
* (for style, the default is always "none")
* @param pathName the config path to retrieve the value from
*/
private @Nullable String getDecorationString(Target selection, boolean getStyleSetting, String defaultColor, String pathName){
String path = getStyleSetting ? pathName + "-style" : pathName;
String defaultValue = getStyleSetting ? "none" : defaultColor;
ConfigurationSection section = getRelevantSection(selection);
return section != null ? section.getString(path, defaultValue) : null;
}
/**
* @return the config section that contains the relevant color or style option.
*/
private @Nullable ConfigurationSection getRelevantSection(Target selection) {
if (selection == null) { //rather than rework the whole Target enum, I have added shared-stats as the null-option for now
return config.getConfigurationSection("shared-stats");
}
switch (selection) {
case TOP -> {
return config.getConfigurationSection("top-list");
}
case PLAYER -> {
return config.getConfigurationSection("individual-statistics");
}
case SERVER -> {
return config.getConfigurationSection("total-server");
}
default -> {
return null;
}
}
}
}

View File

@ -1,7 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.config;
package com.artemis.the.gr8.playerstats.config;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
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;
@ -11,7 +11,10 @@ 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> */
/**
* 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);
@ -20,14 +23,17 @@ public final class ConfigUpdateHandler {
try {
configuration.save(configFile);
ConfigUpdater.update(plugin, configFile.getName(), configFile);
MyLogger.logMsg("Your config has been updated to version " + configVersion +
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.*/
/**
* 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]")) {
@ -35,7 +41,10 @@ public final class ConfigUpdateHandler {
}
}
/** Adjusts some of the default colors to migrate from versions 2 or 3 to version 4 and above.*/
/**
* 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");

View File

@ -0,0 +1,12 @@
package com.artemis.the.gr8.playerstats.enums;
/**
* Represents the debugging level that PlayerStats can use.
* <br>
* <br>1 = low (only show unexpected errors)
* <br>2 = medium (detail all encountered exceptions, log main tasks and show time taken)
* <br>3 = high (log all tasks and time taken)
*/
public enum DebugLevel {
LOW, MEDIUM, HIGH
}

View File

@ -0,0 +1,155 @@
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,7 +1,10 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
package com.artemis.the.gr8.playerstats.enums;
/** All standard messages PlayerStats can send as feedback.
These are all the messages that can be sent without needing additional parameters.*/
/**
* All standard messages PlayerStats can send as feedback.
* These are all the messages that can be sent without needing
* additional parameters.
*/
public enum StandardMessage {
RELOADED_CONFIG,
STILL_RELOADING,

View File

@ -0,0 +1,9 @@
package com.artemis.the.gr8.playerstats.enums;
/**
* This enum represents the targets PlayerStats accepts
* for a stat-lookup (Player, Server and Top).
*/
public enum Target {
PLAYER, SERVER, TOP
}

View File

@ -1,9 +1,12 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
package com.artemis.the.gr8.playerstats.enums;
import org.bukkit.Statistic;
import org.jetbrains.annotations.NotNull;
/** All the units PlayerStats can display statistics in, separated by Type.*/
/**
* All the units PlayerStats can display statistics in, separated
* by {@link Unit.Type}.
*/
public enum Unit {
NUMBER (Type.UNTYPED, "Times"),
KM (Type.DISTANCE, "km"),
@ -26,21 +29,33 @@ public enum Unit {
this.label = label;
}
/** Returns a pretty name belonging to this enum constant. If the Unit is
NUMBER, it will return null. */
/**
* Gets the pretty name belonging to this enum constant.
*
* @return the label
*/
public String getLabel() {
return this.label;
}
/** Returns the Type this enum constant belongs to.*/
/**
* Gets the Type this enum constant belongs to.
*
* @return the Type
*/
public Type getType() {
return this.type;
}
/** For Type Time, Damage and Distance, this will return a smaller Unit than the current one
(if there is a smaller Unit, that is, otherwise it will return itself).
So for DAY, for example, it can return HOUR, MINUTE or SECOND.
@param stepsSmaller how many steps smaller the returned Unit should be*/
/**
* For Type Time, Damage and Distance, this will return a smaller Unit
* than the current one (if there is a smaller Unit, that is, otherwise
* it will return itself). So for DAY, for example, it can return HOUR,
* MINUTE or SECOND.
*
* @param stepsSmaller how many steps smaller the returned Unit should be
* @return the smaller Unit
*/
public Unit getSmallerUnit(int stepsSmaller) {
switch (this) {
case DAY -> {
@ -99,7 +114,12 @@ public enum Unit {
}
}
/** Converts the current Unit into seconds (and returns -1 if the current Unit is not of Type TIME)*/
/**
* Converts the current Unit into seconds (and returns
* -1 if the current Unit is not of {@link Unit.Type} TIME).
*
* @return this Unit in seconds
*/
public double getSeconds() {
return switch (this) {
case DAY -> 86400;
@ -111,6 +131,15 @@ public enum Unit {
};
}
/**
* Gets the Unit corresponding to the given String. This String
* does not need to match exactly (it can be "day" or "days",
* for example), and is case-insensitive.
*
* @param unitName the name belonging to the desired Unit,
* case-insensitive
* @return the Unit
*/
/** Converts the current Unit into a short label (and returns a '?' if the current Unit is not of Type TIME)*/
public char getShortLabel(){
return switch (this) {
@ -118,7 +147,6 @@ public enum Unit {
case HOUR -> 'h';
case MINUTE -> 'm';
case SECOND -> 's';
case TICK -> 't';
default -> '?';
};
}
@ -143,8 +171,13 @@ public enum Unit {
};
}
/** Returns the Unit.Type of this Statistic, which can be Untyped, Distance, Damage, or Time.
@param statistic the Statistic enum constant*/
/**
* Gets the Unit.Type of this Statistic, which can be Untyped,
* Distance, Damage, or Time.
*
* @param statistic the Statistic enum constant
* @return the Type of this Unit
*/
public static @NotNull Type getTypeFromStatistic(Statistic statistic) {
String name = statistic.toString().toLowerCase();
if (name.contains("one_cm")) {
@ -158,9 +191,13 @@ public enum Unit {
}
}
/** Returns the most suitable timeUnit for this number.
@param type the Unit.Type of the statistic this number belongs to
@param number the statistic number as returned by Player.getStatistic()*/
/**
* Gets the most suitable Unit for this number.
*
* @param type the Unit.Type of the statistic this number belongs to
* @param number the statistic number as returned by Player.getStatistic()
* @return the Unit
*/
public static Unit getMostSuitableUnit(Unit.Type type, long number) {
switch (type) {
case TIME -> {

View File

@ -1,11 +1,16 @@
package com.gmail.artemis.the.gr8.playerstats.listeners;
package com.artemis.the.gr8.playerstats.listeners;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.artemis.the.gr8.playerstats.ThreadManager;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.jetbrains.annotations.ApiStatus;
/** Listens for new Players that join, and reloads PlayerStats if someone joins that hasn't joined before.*/
/**
* Listens for new Players that join, and reloads PlayerStats
* if someone joins that hasn't joined before.
*/
@ApiStatus.Internal
public class JoinListener implements Listener {
private static ThreadManager threadManager;

View File

@ -0,0 +1,36 @@
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

@ -0,0 +1,736 @@
package com.artemis.the.gr8.playerstats.msg;
import com.artemis.the.gr8.playerstats.Main;
import com.artemis.the.gr8.playerstats.api.ApiFormatter;
import com.artemis.the.gr8.playerstats.msg.components.ComponentFactory;
import com.artemis.the.gr8.playerstats.msg.components.ExampleMessage;
import com.artemis.the.gr8.playerstats.msg.components.HelpMessage;
import com.artemis.the.gr8.playerstats.msg.components.BukkitConsoleComponentFactory;
import com.artemis.the.gr8.playerstats.msg.components.PrideComponentFactory;
import com.artemis.the.gr8.playerstats.msg.msgutils.*;
import com.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.artemis.the.gr8.playerstats.utils.MyLogger;
import com.artemis.the.gr8.playerstats.enums.Target;
import com.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.artemis.the.gr8.playerstats.enums.Unit;
import com.artemis.the.gr8.playerstats.statistic.request.RequestSettings;
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.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.BiFunction;
import static net.kyori.adventure.text.Component.*;
/**
* Composes messages to send to a Player or Console. This class is responsible
* for constructing a final {@link TextComponent} with the text content of the
* desired message. The component parts (with appropriate formatting) are supplied
* by a {@link ComponentFactory}. By default, this class works with the standard
* ComponentFactory, but you can give it a different ComponentFactory upon creation.
*
* @see PrideComponentFactory
* @see BukkitConsoleComponentFactory
*/
public final class MessageBuilder implements ApiFormatter {
private static ConfigHandler config;
private boolean useHoverText;
private boolean isConsoleBuilder;
private final ComponentFactory componentFactory;
private final LanguageKeyHandler languageKeyHandler;
private final NumberFormatter formatter;
private MessageBuilder(ConfigHandler config) {
this (config, new ComponentFactory(config));
}
private MessageBuilder(ConfigHandler configHandler, ComponentFactory factory) {
config = configHandler;
useHoverText = config.useHoverText();
componentFactory = factory;
formatter = new NumberFormatter();
languageKeyHandler = Main.getLanguageKeyHandler();
}
public static MessageBuilder defaultBuilder(ConfigHandler config) {
return new MessageBuilder(config);
}
public static MessageBuilder fromComponentFactory(ConfigHandler config, ComponentFactory factory) {
return new MessageBuilder(config, 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 TextComponent getPluginPrefix() {
return componentFactory.pluginPrefix();
}
@Override
public TextComponent getRainbowPluginPrefix() {
PrideComponentFactory pride = new PrideComponentFactory(config);
return pride.rainbowPrefix();
}
@Override
public TextComponent getPluginPrefixAsTitle() {
return componentFactory.pluginPrefixAsTitle();
}
@Override
public TextComponent getRainbowPluginPrefixAsTitle() {
PrideComponentFactory pride = new PrideComponentFactory(config);
return pride.pluginPrefixAsTitle();
}
public TextComponent reloadedConfig() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content("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 TextComponent waitAMoment(boolean longWait) {
String msg = longWait ? "Calculating statistics, this may take a minute..." :
"Calculating statistics, this may take a few moments...";
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(msg));
}
public TextComponent missingStatName() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please provide a valid statistic name!"));
}
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 TextComponent missingPlayerName() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please specify a valid player-name!"));
}
public TextComponent wrongSubStatType(Statistic.Type 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) + "!"));
}
public TextComponent requestAlreadyRunning() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please wait for your previous lookup to finish!"));
}
public TextComponent stillOnShareCoolDown() {
int waitTime = config.getStatShareWaitingTime();
String minutes = waitTime == 1 ? " minute" : " minutes";
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content("You need to wait")
.append(space())
.append(componentFactory.messageAccent()
.content(waitTime + minutes))
.append(space())
.append(text("between sharing!")));
}
public TextComponent resultsAlreadyShared() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content("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 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 TextComponent usageExamples() {
return ExampleMessage.construct(componentFactory);
}
public TextComponent helpMsg() {
int listSize = config.getTopListMaxSize();
if (!isConsoleBuilder && useHoverText) {
return HelpMessage.constructHoverMsg(componentFactory, listSize);
} else {
return HelpMessage.constructPlainMsg(componentFactory, listSize);
}
}
@Override
public TextComponent getStatTitle(Statistic statistic, @Nullable String subStatName) {
return getTopStatTitleComponent(0, statistic, subStatName, null);
}
@Override
public TextComponent getTopStatTitle(int topListSize, Statistic statistic, @Nullable String subStatName) {
return getTopStatTitleComponent(topListSize, statistic, subStatName, null);
}
@Override
public 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) {
TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.TOP, unit);
return getTopStatLineComponent(positionInTopList, playerName, statNumberComponent);
}
/**
* Time-number does not hover
*/
@Override
public 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) {
TextComponent statNumberComponent = getStatNumberComponent(statNumber, Target.SERVER, statistic);
return getServerStatComponent(statNumberComponent, statistic, null, null);
}
@Override
public 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) {
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) {
TextComponent statNumberComponent = getBasicTimeNumberComponent(statNumber, Target.SERVER, bigUnit, smallUnit);
return getServerStatComponent(statNumberComponent, statistic, null, null);
}
@Override
public 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) {
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) {
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) {
TextComponent statNumberComponent = getBasicTimeNumberComponent(statNumber, Target.PLAYER, bigUnit, smallUnit);
return getPlayerStatComponent(playerName, statNumberComponent, statistic, null, null);
}
/**
* Returns a BiFunction for a player statistic. This BiFunction will return
* a formattedComponent, the shape of which is determined by the 2 parameters
* the BiFunction gets.
* <p>- Integer shareCode: if a shareCode is provided, a clickable "share"
* button will be added.
* <br>- CommandSender sender: if a sender is provided, a signature with
* "shared by sender-name" will be added.
* <br>- If both parameters are null, the formattedComponent will be returned
* as is.
*/
public BiFunction<Integer, CommandSender, TextComponent> formattedPlayerStatFunction(int stat, @NotNull RequestSettings request) {
TextComponent playerStat = formatPlayerStat(request.getPlayerName(), stat, request.getStatistic(), request.getSubStatEntryName());
return getFormattingFunction(playerStat, Target.PLAYER);
}
/**
* Returns a BiFunction for a server statistic. This BiFunction will return
* a formattedComponent, the shape of which is determined by the 2 parameters
* the BiFunction gets.
* <p>- Integer shareCode: if a shareCode is provided, a clickable "share"
* button will be added.
* <br>- CommandSender sender: if a sender is provided, a signature with
* "shared by sender-name" will be added.
* <br>- If both parameters are null, the formattedComponent will be returned
* as is.
*/
public BiFunction<Integer, CommandSender, TextComponent> formattedServerStatFunction(long stat, @NotNull RequestSettings request) {
TextComponent serverStat = formatServerStat(stat, request.getStatistic(), request.getSubStatEntryName());
return getFormattingFunction(serverStat, Target.SERVER);
}
/**
* Returns a BiFunction for a top statistic. This BiFunction will return
* a formattedComponent, the shape of which is determined by the 2 parameters
* the BiFunction gets.
* <p>- Integer shareCode: if a shareCode is provided, a clickable "share"
* button will be added.
* <br>- CommandSender sender: if a sender is provided, a signature with
* "shared by sender-name" will be added.
* <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) {
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) -> {
TextComponent.Builder topBuilder = text();
//if we're adding a share-button
if (shareCode != null) {
if (useEnters) {
topBuilder.append(newline());
}
topBuilder.append(componentFactory.pluginPrefix())
.append(space())
.append(title)
.append(space())
.append(componentFactory.shareButton(shareCode))
.append(list);
}
//if we're adding a "shared by" component
else if (sender != null) {
if (useEntersForShared) {
topBuilder.append(newline());
}
topBuilder.append(title)
.append(space())
.append(componentFactory.statResultInHoverText(text()
.append(componentFactory.pluginPrefix())
.append(space())
.append(title)
.append(list)
.build()))
.append(newline())
.append(componentFactory.sharedByMessage(
getSharerNameComponent(sender)));
}
//if we're not adding a share-button or a "shared by" component
else {
if (useEnters) {
topBuilder.append(newline());
}
topBuilder.append(componentFactory.pluginPrefix())
.append(space())
.append(title)
.append(list);
}
return topBuilder.build();
};
}
private 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);
return Component.text()
.append(componentFactory.playerName(playerName, Target.PLAYER)
.append(text(":"))
.append(space()))
.append(statNumberComponent)
.append(space())
.append(getStatAndSubStatNameComponent(statistic, subStatName, Target.PLAYER))
.append(statUnit) //space is provided by statUnitComponent
.build();
}
private TextComponent getServerStatComponent(TextComponent statNumber, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) {
String serverTitle = config.getServerTitle();
String serverName = config.getServerName();
TextComponent statUnit = (unit == null) ?
getStatUnitComponent(statistic, Target.SERVER) :
getStatUnitComponent(unit, Target.SERVER);
return Component.text()
.append(componentFactory.title(serverTitle, Target.SERVER))
.append(space())
.append(componentFactory.serverName(serverName))
.append(space())
.append(statNumber)
.append(space())
.append(getStatAndSubStatNameComponent(statistic, subStatName, Target.SERVER))
.append(statUnit) //space is provided by statUnit
.build();
}
private TextComponent getTopStatTitleComponent(int topListSize, Statistic statistic, @Nullable String subStatName, @Nullable Unit unit) {
TextComponent statUnit = (unit == null) ?
getStatUnitComponent(statistic, Target.TOP) :
getStatUnitComponent(unit, Target.TOP);
if (topListSize == 0) {
return Component.text()
.append(getStatAndSubStatNameComponent(statistic, subStatName, Target.TOP))
.append(statUnit) //space is provided by statUnitComponent
.build();
} else {
return Component.text()
.append(componentFactory.title(config.getTopStatsTitle(), Target.TOP))
.append(space())
.append(componentFactory.titleNumber(topListSize))
.append(space())
.append(getStatAndSubStatNameComponent(statistic, subStatName, Target.TOP))
.append(statUnit) //space is provided by statUnitComponent
.build();
}
}
private TextComponent getTopStatListComponent(LinkedHashMap<String, Integer> topStats, Statistic statistic) {
TextComponent.Builder topList = Component.text();
Set<String> playerNames = topStats.keySet();
boolean useDots = config.useDots();
int count = 0;
for (String playerName : playerNames) {
topList.append(newline());
if (useDots) {
topList.append(getTopStatLineComponent(
++count, playerName, getStatNumberComponent(topStats.get(playerName), Target.TOP, statistic)));
} else {
topList.append(space())
.append(componentFactory.rankNumber(++count))
.append(space())
.append(componentFactory.playerName(playerName + ":", Target.TOP))
.append(space()).append(getStatNumberComponent(topStats.get(playerName), Target.TOP, statistic));
}
}
return topList.build();
}
private TextComponent getTopStatLineComponent(int positionInTopList, String playerName, TextComponent statNumberComponent) {
TextComponent.Builder topStatLineBuilder = Component.text()
.append(space())
.append(componentFactory.rankNumber(positionInTopList))
.append(space())
.append(componentFactory.playerName(playerName, Target.TOP))
.append(space());
int dots = getNumberOfDotsToAlign(positionInTopList + ". " + playerName);
if (dots >= 1) {
topStatLineBuilder.append(componentFactory.dots(".".repeat(dots)));
}
return topStatLineBuilder
.append(space())
.append(statNumberComponent)
.build();
}
private TextComponent getStatAndSubStatNameComponent(Statistic statistic, @Nullable String subStatName, Target target) {
if (config.useTranslatableComponents()) {
String statKey = languageKeyHandler.getStatKey(statistic);
String subStatKey = switch (statistic.getType()) {
case UNTYPED -> null;
case ENTITY -> languageKeyHandler.getEntityKey(EnumHandler.getEntityEnum(subStatName));
case BLOCK -> languageKeyHandler.getBlockKey(EnumHandler.getBlockEnum(subStatName));
case ITEM -> languageKeyHandler.getItemKey(EnumHandler.getItemEnum(subStatName));
};
if (subStatKey == null) {
subStatKey = StringUtils.prettify(subStatName);
}
return componentFactory.statAndSubStatNameTranslatable(statKey, subStatKey, target);
}
String prettyStatName = StringUtils.prettify(statistic.toString());
String prettySubStatName = StringUtils.prettify(subStatName);
return componentFactory.statAndSubStatName(prettyStatName, prettySubStatName, target);
}
private TextComponent getStatNumberComponent(long statNumber, Target target, Unit unit) {
return switch (unit.getType()) {
case TIME -> getBasicTimeNumberComponent(statNumber, target, unit, null);
case DAMAGE -> getDamageNumberComponent(statNumber, target, unit);
case DISTANCE -> getDistanceNumberComponent(statNumber, target, unit);
default -> getDefaultNumberComponent(statNumber, target);
};
}
private TextComponent getStatNumberComponent(long statNumber, Target target, Statistic statistic) {
Unit.Type unitType = Unit.getTypeFromStatistic(statistic);
return switch (unitType) {
case DISTANCE -> getDistanceNumberComponent(statNumber, target);
case DAMAGE -> getDamageNumberComponent(statNumber, target);
case TIME -> getTimeNumberComponent(statNumber, target);
default -> getDefaultNumberComponent(statNumber, target);
};
}
private TextComponent getDistanceNumberComponent(long statNumber, Target target) {
Unit statUnit = Unit.fromString(config.getDistanceUnit(false));
return getDistanceNumberComponent(statNumber, target, statUnit);
}
private TextComponent getDistanceNumberComponent(long statNumber, Target target, Unit unit) {
String prettyNumber = formatter.formatDistanceNumber(statNumber, unit);
if (!useHoverText) {
return componentFactory.distanceNumber(prettyNumber, target);
}
Unit hoverUnit = Unit.fromString(config.getDistanceUnit(true));
String hoverNumber = formatter.formatDistanceNumber(statNumber, hoverUnit);
if (config.useTranslatableComponents()) {
String unitKey = languageKeyHandler.getUnitKey(hoverUnit);
if (unitKey != null) {
return componentFactory.distanceNumberWithTranslatableHoverText(prettyNumber, hoverNumber, unitKey, target);
}
}
return componentFactory.distanceNumberWithHoverText(prettyNumber, hoverNumber, hoverUnit.getLabel(), target);
}
private TextComponent getDamageNumberComponent(long statNumber, Target target) {
Unit statUnit = Unit.fromString(config.getDamageUnit(false));
return getDamageNumberComponent(statNumber, target, statUnit);
}
private TextComponent getDamageNumberComponent(long statNumber, Target target, Unit unit) {
String prettyNumber = formatter.formatDamageNumber(statNumber, unit);
if (!useHoverText) {
return componentFactory.damageNumber(prettyNumber, target);
}
Unit hoverUnit = Unit.fromString(config.getDamageUnit(true));
String prettyHoverNumber = formatter.formatDamageNumber(statNumber, hoverUnit);
if (hoverUnit == Unit.HEART) {
return componentFactory.damageNumberWithHeartUnitInHoverText(prettyNumber, prettyHoverNumber, target);
}
return componentFactory.damageNumberWithHoverText(prettyNumber, prettyHoverNumber, hoverUnit.getLabel(), target);
}
private TextComponent getTimeNumberComponent(long statNumber, Target target) {
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);
}
else {
String mainNumber = formatter.formatTimeNumber(statNumber, unitRange.get(0), unitRange.get(1));
if (!useHoverText) {
return componentFactory.timeNumber(mainNumber, target);
} else {
String hoverNumber = formatter.formatTimeNumber(statNumber, unitRange.get(2), unitRange.get(3));
MyLogger.logHighLevelMsg("mainNumber: " + mainNumber + ", hoverNumber: " + hoverNumber);
return componentFactory.timeNumberWithHoverText(mainNumber, hoverNumber, target);
}
}
}
private TextComponent getBasicTimeNumberComponent(long statNumber, Target target, Unit bigUnit, @Nullable Unit smallUnit) {
if (smallUnit == null) {
smallUnit = bigUnit.getSmallerUnit(1);
}
return componentFactory.timeNumber(formatter.formatTimeNumber(statNumber, bigUnit, smallUnit), target);
}
private TextComponent getDefaultNumberComponent(long statNumber, Target target) {
return componentFactory.statNumber(formatter.formatNumber(statNumber), target);
}
/**
* Provides its own space in front of it!
*/
private TextComponent getStatUnitComponent(Statistic statistic, Target target) {
Unit unit = switch (Unit.getTypeFromStatistic(statistic)) {
case DAMAGE -> Unit.fromString(config.getDamageUnit(false));
case DISTANCE -> Unit.fromString(config.getDistanceUnit(false));
default -> Unit.NUMBER;
};
return getStatUnitComponent(unit, target);
}
private TextComponent getStatUnitComponent(Unit unit, Target target) {
return switch (unit.getType()) {
case DAMAGE -> getDamageUnitComponent(unit, target);
case DISTANCE -> getDistanceUnitComponent(unit, target);
default -> Component.empty();
};
}
/**
* Provides its own space in front of it!
*/
private TextComponent getDistanceUnitComponent(Unit unit, Target target) {
if (config.useTranslatableComponents()) {
String unitKey = languageKeyHandler.getUnitKey(unit);
if (unitKey != null) {
return Component.space()
.append(componentFactory.statUnitTranslatable(unitKey, target));
}
}
return Component.space()
.append(componentFactory.statUnit(unit.getLabel(), target));
}
/**
* Provides its own space in front of it!
*/
private 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);
}
return Component.space()
.append(componentFactory.statUnit(unit.getLabel(), target));
}
private Component getSharerNameComponent(CommandSender sender) {
if (sender instanceof Player player) {
Component senderName = EasterEggProvider.getPlayerName(player);
if (senderName != null) {
return senderName;
}
}
return componentFactory.sharerName(sender.getName());
}
private BiFunction<Integer, CommandSender, TextComponent> getFormattingFunction(@NotNull TextComponent statResult, Target target) {
boolean useEnters = config.useEnters(target, false);
boolean useEntersForShared = config.useEnters(target, true);
return (shareCode, sender) -> {
TextComponent.Builder statBuilder = text();
//if we're adding a share-button
if (shareCode != null) {
if (useEnters) {
statBuilder.append(newline());
}
statBuilder.append(statResult)
.append(space())
.append(componentFactory.shareButton(shareCode));
}
//if we're adding a "shared by" component
else if (sender != null) {
if (useEntersForShared) {
statBuilder.append(newline());
}
statBuilder.append(statResult)
.append(newline())
.append(componentFactory.sharedByMessage(
getSharerNameComponent(sender)));
}
//if we're not adding a share-button or a "shared by" component
else {
if (useEnters) {
statBuilder.append(newline());
}
statBuilder.append(statResult);
}
return statBuilder.build();
};
}
private int getNumberOfDotsToAlign(String displayText) {
if (isConsoleBuilder) {
return FontUtils.getNumberOfDotsToAlignForConsole(displayText);
} else if (config.playerNameIsBold()) {
return FontUtils.getNumberOfDotsToAlignForBoldText(displayText);
} else {
return FontUtils.getNumberOfDotsToAlign(displayText);
}
}
/**
* Get an ArrayList consisting of 2 or 4 timeUnits. The order of items is:
* <p>0. maxUnit</p>
* <p>1. minUnit</p>
* <p>2. maxHoverUnit</p>
* <p>3. minHoverUnit</p>
*/
private ArrayList<Unit> getTimeUnitRange(long statNumber) {
ArrayList<Unit> unitRange = new ArrayList<>();
if (!config.autoDetectTimeUnit(false)) {
unitRange.add(Unit.fromString(config.getTimeUnit(false)));
unitRange.add(Unit.fromString(config.getTimeUnit(false, true)));
}
else {
Unit bigUnit = Unit.getMostSuitableUnit(Unit.Type.TIME, statNumber);
unitRange.add(bigUnit);
unitRange.add(bigUnit.getSmallerUnit(config.getNumberOfExtraTimeUnits(false)));
}
if (useHoverText) {
if (!config.autoDetectTimeUnit(true)) {
unitRange.add(Unit.fromString(config.getTimeUnit(true)));
unitRange.add(Unit.fromString(config.getTimeUnit(true, true)));
}
else {
Unit bigHoverUnit = Unit.getMostSuitableUnit(Unit.Type.TIME, statNumber);
unitRange.add(bigHoverUnit);
unitRange.add(bigHoverUnit.getSmallerUnit(config.getNumberOfExtraTimeUnits(true)));
}
}
return unitRange;
}
}

View File

@ -1,13 +1,11 @@
package com.gmail.artemis.the.gr8.playerstats.msg;
package com.artemis.the.gr8.playerstats.msg;
import com.gmail.artemis.the.gr8.playerstats.ShareManager;
import com.gmail.artemis.the.gr8.playerstats.api.StatFormatter;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentFactory;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.msg.components.BukkitConsoleComponentFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.components.PrideComponentFactory;
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;
@ -24,12 +22,16 @@ import java.util.LinkedHashMap;
import java.util.function.BiFunction;
import java.util.function.Function;
import static com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage.*;
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 the messages from a {@link MessageBuilder}, which is different for a Console as 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 StatFormatter {
/**
* 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;
@ -53,56 +55,27 @@ public final class OutputManager implements StatFormatter {
}
@Override
public TextComponent getPluginPrefix() {
ComponentFactory factory = new ComponentFactory(config);
return factory.pluginPrefix();
}
@Override
public TextComponent getRainbowPluginPrefix() {
ComponentFactory prideFactory = new PrideComponentFactory(config);
return prideFactory.pluginPrefix();
}
@Override
public TextComponent getPluginPrefixAsTitle() {
ComponentFactory factory = new ComponentFactory(config);
return factory.pluginPrefixAsTitle();
}
@Override
public TextComponent getRainbowPluginPrefixAsTitle() {
ComponentFactory prideFactory = new PrideComponentFactory(config);
return prideFactory.pluginPrefixAsTitle();
}
@Override
public TextComponent formatPlayerStat(@NotNull StatRequest statRequest, int playerStat) {
public TextComponent formatAndSavePlayerStat(@NotNull RequestSettings requestSettings, int playerStat) {
BiFunction<Integer, CommandSender, TextComponent> playerStatFunction =
getMessageBuilder(statRequest).formattedPlayerStatFunction(playerStat, statRequest);
getMessageBuilder(requestSettings).formattedPlayerStatFunction(playerStat, requestSettings);
return processFunction(statRequest.getCommandSender(), playerStatFunction);
return processFunction(requestSettings.getCommandSender(), playerStatFunction);
}
@Override
public TextComponent formatServerStat(@NotNull StatRequest statRequest, long serverStat) {
public TextComponent formatAndSaveServerStat(@NotNull RequestSettings requestSettings, long serverStat) {
BiFunction<Integer, CommandSender, TextComponent> serverStatFunction =
getMessageBuilder(statRequest).formattedServerStatFunction(serverStat, statRequest);
getMessageBuilder(requestSettings).formattedServerStatFunction(serverStat, requestSettings);
return processFunction(statRequest.getCommandSender(), serverStatFunction);
return processFunction(requestSettings.getCommandSender(), serverStatFunction);
}
@Override
public TextComponent formatSingleTopStatLine(int positionInTopList, String playerName, long statNumber, Statistic statistic) {
return messageBuilder.singleTopStatLine(positionInTopList, playerName, statNumber, statistic);
}
@Override
public TextComponent formatTopStat(@NotNull StatRequest statRequest, @NotNull LinkedHashMap<String, Integer> topStats) {
public TextComponent formatAndSaveTopStat(@NotNull RequestSettings requestSettings, @NotNull LinkedHashMap<String, Integer> topStats) {
BiFunction<Integer, CommandSender, TextComponent> topStatFunction =
getMessageBuilder(statRequest).formattedTopStatFunction(topStats, statRequest);
getMessageBuilder(requestSettings).formattedTopStatFunction(topStats, requestSettings);
return processFunction(statRequest.getCommandSender(), topStatFunction);
return processFunction(requestSettings.getCommandSender(), topStatFunction);
}
public void sendFeedbackMsg(@NotNull CommandSender sender, StandardMessage message) {
@ -168,8 +141,8 @@ public final class OutputManager implements StatFormatter {
return sender instanceof ConsoleCommandSender ? consoleMessageBuilder : messageBuilder;
}
private MessageBuilder getMessageBuilder(StatRequest statRequest) {
if (statRequest.isAPIRequest() || !statRequest.isConsoleSender()) {
private MessageBuilder getMessageBuilder(RequestSettings requestSettings) {
if (!requestSettings.isConsoleSender()) {
return messageBuilder;
} else {
return consoleMessageBuilder;

View File

@ -1,7 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.msg.components;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import com.artemis.the.gr8.playerstats.enums.PluginColor;
import com.artemis.the.gr8.playerstats.config.ConfigHandler;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
@ -11,8 +11,11 @@ import org.jetbrains.annotations.Nullable;
import static net.kyori.adventure.text.Component.text;
/** The {@link ComponentFactory} that is used to build messages for a Bukkit Console.
Bukkit consoles don't support hex colors, unlike Paper consoles.*/
/**
* The {@link ComponentFactory} that is used to build messages for
* a Bukkit Console. Bukkit consoles don't support hex colors,
* unlike Paper consoles.
*/
public class BukkitConsoleComponentFactory extends ComponentFactory {
public BukkitConsoleComponentFactory(ConfigHandler config) {

View File

@ -1,11 +1,11 @@
package com.gmail.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.msg.components;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageBuilder;
import com.gmail.artemis.the.gr8.playerstats.msg.msgutils.LanguageKeyHandler;
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 net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
@ -22,9 +22,14 @@ import org.jetbrains.annotations.Nullable;
import static net.kyori.adventure.text.Component.*;
/** Creates Components with the desired formatting for the {@link MessageBuilder} to build messages with.
This class can put Strings into formatted Components with TextColor
and TextDecoration, or return empty Components with the desired formatting.*/
/** Creates Components with the desired formatting for the
* {@link MessageBuilder} to build messages with. This class
* can put Strings into formatted Components with TextColor
* and TextDecoration, or return empty Components with the
* desired formatting (as specified by the {@link ConfigHandler}).
*
* @see PluginColor
*/
public class ComponentFactory {
private static ConfigHandler config;
@ -76,8 +81,9 @@ public class ComponentFactory {
return getColorFromString(config.getSharerNameDecoration(false));
}
/** Returns [PlayerStats]. */
/**
* Returns [PlayerStats].
*/
public TextComponent pluginPrefix() {
return text("[")
.color(BRACKETS)
@ -85,7 +91,9 @@ public class ComponentFactory {
.append(text("]"));
}
/** Returns [PlayerStats] surrounded by underscores on both sides. */
/**
* Returns [PlayerStats] surrounded by underscores on both sides.
*/
public TextComponent pluginPrefixAsTitle() {
//12 underscores for both console and in-game
return text("____________").color(UNDERSCORE)
@ -95,12 +103,18 @@ public class ComponentFactory {
.append(text("____________"));
}
/** Returns a TextComponent with the input String as content, with color Gray and decoration Italic.*/
/**
* Returns a TextComponent with the input String as content,
* with color Gray and decoration Italic.
*/
public TextComponent subTitle(String content) {
return text(content).color(BRACKETS).decorate(TextDecoration.ITALIC);
}
/** Returns a TextComponents in the style of a default plugin message, with color Medium_Blue. */
/**
* Returns a TextComponents in the style of a default plugin message,
* with color Medium_Blue.
*/
public TextComponent message() {
return text().color(MSG_MAIN).build();
}
@ -127,8 +141,8 @@ public class ComponentFactory {
getStyleFromString(config.getRankNumberDecoration(true)));
}
public TextComponent dots() {
return getComponent(null,
public TextComponent dots(String dots) {
return getComponent(dots,
getColorFromString(config.getDotsDecoration(false)),
getStyleFromString(config.getDotsDecoration(true)));
}
@ -182,8 +196,12 @@ public class ComponentFactory {
.build());
}
/** @param prettyStatName a statName with underscores removed and each word capitalized
@param prettySubStatName if present, a subStatName with underscores removed and each word capitalized*/
/**
* @param prettyStatName a statName with underscores removed and each
* word capitalized
* @param prettySubStatName if present, a subStatName with underscores
* removed and each word capitalized
*/
public TextComponent statAndSubStatName(String prettyStatName, @Nullable String prettySubStatName, Target target) {
TextComponent.Builder totalStatNameBuilder = getComponentBuilder(prettyStatName,
getColorFromString(config.getStatNameDecoration(target, false)),
@ -198,8 +216,10 @@ public class ComponentFactory {
return totalStatNameBuilder.build();
}
/** Returns a TextComponent with TranslatableComponent as a child.*/
public TextComponent statAndSubStatNameTranslatable(String statKey, String subStatKey, Target target) {
/**
* Returns a TextComponent with TranslatableComponent as a child.
* */
public TextComponent statAndSubStatNameTranslatable(String statKey, @Nullable String subStatKey, Target target) {
TextComponent.Builder totalStatNameBuilder = getComponentBuilder(null,
getColorFromString(config.getStatNameDecoration(target, false)),
getStyleFromString(config.getStatNameDecoration(target, true)));
@ -228,13 +248,18 @@ public class ComponentFactory {
getStyleFromString(config.getStatNumberDecoration(target, true)));
}
public TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber, @Nullable String hoverUnitName, @Nullable String hoverUnitKey, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, hoverUnitName, hoverUnitKey, null, target);
public TextComponent timeNumber(String prettyNumber, Target target) {
return statNumber(prettyNumber, target);
}
public TextComponent timeNumberWithHoverText(String mainNumber, String hoverNumber, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, null, null, null, target);
}
public TextComponent damageNumber(String prettyNumber, Target target) {
return statNumber(prettyNumber, target);
}
public TextComponent damageNumberWithHoverText(String mainNumber, String hoverNumber, String hoverUnitName, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, hoverUnitName, null, null, target);
}
@ -248,11 +273,11 @@ public class ComponentFactory {
}
public TextComponent distanceNumberWithHoverText(String mainNumber, String hoverNumber, String hoverUnitName, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, hoverUnitName, null, target);
return statNumberWithHoverText(mainNumber, hoverNumber, hoverUnitName, null, null, target);
}
public TextComponent distanceNumberWithTranslatableHoverText(String mainNumber, String hoverNumber, String hoverUnitKey, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, null, hoverUnitKey, target);
return statNumberWithHoverText(mainNumber, hoverNumber, null, hoverUnitKey, null, target);
}
public TextComponent statUnit(String unitName, Target target) {
@ -303,7 +328,9 @@ public class ComponentFactory {
.build();
}
/** Returns a TextComponent for the subStatName, or an empty component.*/
/**
* Returns a TextComponent for the subStatName, or an empty component.
*/
private TextComponent subStatName(@Nullable String prettySubStatName, Target target) {
if (prettySubStatName == null) {
return Component.empty();
@ -318,8 +345,10 @@ public class ComponentFactory {
}
}
/** Returns a TranslatableComponent for the subStatName, or an empty component.*/
private TextComponent subStatNameTranslatable(String subStatKey, Target target) {
/**
* Returns a TranslatableComponent for the subStatName, or an empty component.
*/
private TextComponent subStatNameTranslatable(@Nullable String subStatKey, Target target) {
if (subStatKey != null) {
return getComponentBuilder(null,
getColorFromString(config.getSubStatNameDecoration(target, false)),
@ -333,18 +362,26 @@ public class ComponentFactory {
return Component.empty();
}
/** Construct a custom translation for kill_entity with the language key for commands.kill.success.single ("Killed %s").
@return a TranslatableComponent Builder with the subStat Component as args.*/
/**
* Construct a custom translation for kill_entity with the language key
* for commands.kill.success.single ("Killed %s").
*
* @return a TranslatableComponent Builder with the subStat Component as args.
*/
private TranslatableComponent.Builder killEntityBuilder(@NotNull TextComponent subStat) {
return translatable()
.key(LanguageKeyHandler.getAlternativeKeyForKillEntity()) //"Killed %s"
.args(subStat);
}
/** Construct a custom translation for entity_killed_by with the language keys for stat.minecraft.deaths
("Number of Deaths") and book.byAuthor ("by %s").
@return a TranslatableComponent Builder with stat.minecraft.deaths as key, with a ChildComponent
with book.byAuthor as key and the subStat Component as args.*/
/**
* Construct a custom translation for entity_killed_by with the language
* keys for stat.minecraft.deaths ("Number of Deaths") and book.byAuthor
* ("by %s").
*
* @return a TranslatableComponent Builder with stat.minecraft.deaths as key,
* with a ChildComponent with book.byAuthor as key and the subStat Component as args.
*/
private TranslatableComponent.Builder entityKilledByBuilder(@NotNull TextComponent subStat) {
return translatable()
.key(LanguageKeyHandler.getAlternativeKeyForEntityKilledBy()) //"Number of Deaths"
@ -354,7 +391,11 @@ public class ComponentFactory {
.args(subStat));
}
private TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber, @Nullable String hoverUnitName, @Nullable String hoverUnitKey, @Nullable TextComponent heartComponent, Target target) {
private TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber,
@Nullable String hoverUnitName,
@Nullable String hoverUnitKey,
@Nullable TextComponent heartComponent, Target target) {
TextColor baseColor = getColorFromString(config.getStatNumberDecoration(target, false));
TextDecoration style = getStyleFromString(config.getStatNumberDecoration(target, true));

View File

@ -1,19 +1,26 @@
package com.gmail.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.msg.components;
import com.gmail.artemis.the.gr8.playerstats.msg.msgutils.LanguageKeyHandler;
import com.gmail.artemis.the.gr8.playerstats.msg.msgutils.StringUtils;
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;
/** A utility class for handling Adventure's Components.
Its main function is currently to help serialize Components into String.*/
/**
* A small utility class for turning PlayerStats' custom Components into String.
*/
public final class ComponentUtils {
/** Returns a LegacyComponentSerializer that is capable of serializing TranslatableComponents,
and capable of dealing with the custom language-keys I am using to improve the entity-related
statistic names. This serializer will create a String with hex colors and styles, and it will
turn language keys into prettified, readable English. */
/**
* Returns a LegacyComponentSerializer that is capable of serializing
* TranslatableComponents, and capable of dealing with the custom
* language-keys I am using to improve the entity-related statistic
* names. This serializer will create a String with hex colors and styles,
* and it will turn language keys into prettified, readable English.
*
* @return the Serializer
* @see LanguageKeyHandler
*/
public static LegacyComponentSerializer getTranslatableComponentSerializer() {
LegacyComponentSerializer serializer = getTextComponentSerializer();
@ -29,6 +36,7 @@ public final class ComponentUtils {
TextComponent.Builder temp = Component.text();
trans.iterator(ComponentIteratorType.DEPTH_FIRST, ComponentIteratorFlag.INCLUDE_TRANSLATABLE_COMPONENT_ARGUMENTS)
.forEachRemaining(component -> {
//copy the style to the temp builder, because the translatable component that follows it has no style itself
if (component instanceof TextComponent text) {
if (!text.children().isEmpty()) {
text.iterator(ComponentIteratorType.DEPTH_FIRST).forEachRemaining(component1 -> {
@ -37,27 +45,31 @@ public final class ComponentUtils {
}
});
}
} else if (component instanceof TranslatableComponent translatable) {
if (translatable.key().contains("entity")) {
}
//isolate the translatable component with the entity inside
else if (component instanceof TranslatableComponent translatable) {
if (translatable.key().contains("entity.")) {
temp.append(Component.space())
.append(Component.text("(")
.append(Component.text(StringUtils.prettify(LanguageKeyHandler.convertToName(translatable.key()))))
.append(Component.text(
StringUtils.prettify(LanguageKeyHandler.convertToName(translatable.key()))))
.append(Component.text(")")));
totalPrettyName.append(
serializer.serialize(temp.build()));
} else if (!LanguageKeyHandler.isKeyForEntityKilledByArg(translatable.key())) {
}
else if (!LanguageKeyHandler.isKeyForEntityKilledByArg(translatable.key())) {
totalPrettyName.append(
StringUtils.prettify(
LanguageKeyHandler.convertToName(
translatable.key())));
LanguageKeyHandler.getStatKeyTranslation(
translatable.key()));
}
}
});
}
else if (trans.key().startsWith("stat")) {
return LanguageKeyHandler.getStatKeyTranslation(trans.key());
}
else {
return StringUtils.prettify(
LanguageKeyHandler.convertToName(
trans.key()));
return StringUtils.prettify(LanguageKeyHandler.convertToName(trans.key()));
}
return totalPrettyName.toString();
})

View File

@ -1,4 +1,4 @@
package com.gmail.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.msg.components;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
@ -11,7 +11,9 @@ import java.util.List;
import static net.kyori.adventure.text.Component.text;
/** A fully constructed message with examples on how to use PlayerStats.*/
/**
* A fully constructed message with examples on how to use PlayerStats.
*/
public final class ExampleMessage implements TextComponent {
private final TextComponent exampleMessage;

View File

@ -1,4 +1,4 @@
package com.gmail.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.msg.components;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
@ -13,7 +13,9 @@ import java.util.List;
import static net.kyori.adventure.text.Component.text;
/** The help message that explains how to use PlayerStats.*/
/**
* The help message that explains how to use PlayerStats.
*/
public final class HelpMessage implements TextComponent {
private final TextComponent helpMessage;

View File

@ -1,8 +1,8 @@
package com.gmail.artemis.the.gr8.playerstats.msg.components;
package com.artemis.the.gr8.playerstats.msg.components;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
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;
@ -11,7 +11,9 @@ import java.util.Random;
import static net.kyori.adventure.text.Component.*;
/** A festive version of the {@link ComponentFactory}*/
/**
* A festive version of the {@link ComponentFactory}
*/
public class PrideComponentFactory extends ComponentFactory {
public PrideComponentFactory(ConfigHandler c) {
@ -61,6 +63,10 @@ public class PrideComponentFactory extends ComponentFactory {
if (randomizer.nextBoolean()) {
return backwardsPluginPrefixComponent();
}
return rainbowPrefix();
}
public TextComponent rainbowPrefix() {
return text()
.append(MiniMessage.miniMessage()
.deserialize("<#f74040>[</#f74040>" +
@ -79,7 +85,7 @@ public class PrideComponentFactory extends ComponentFactory {
.build();
}
public TextComponent backwardsPluginPrefixComponent() {
private TextComponent backwardsPluginPrefixComponent() {
return text()
.append(MiniMessage.miniMessage()
.deserialize("<#631ae6>[</#631ae6>" +

View File

@ -1,4 +1,4 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
package com.artemis.the.gr8.playerstats.msg.msgutils;
import me.clip.placeholderapi.PlaceholderAPI;
import net.kyori.adventure.text.Component;
@ -12,8 +12,11 @@ import org.jetbrains.annotations.NotNull;
import java.util.Random;
/**This class is just for fun and adds some silly names for players on my server.
It does not impact the rest of the plugin, and will only be used for the players mentioned in here.*/
/**
* This class is just for fun and adds some silly names for
* players on my server. It does not impact the rest of the plugin,
* and will only be used for the players mentioned in here.
*/
public final class EasterEggProvider {
private static boolean isEnabled;
@ -58,7 +61,7 @@ public final class EasterEggProvider {
}
}
case "0dc5336b-acd2-4dc3-a5e9-0aa9b8f113f7" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
if (sillyNumberIsBetween(sillyNumber, 0, 100)) {
playerName = "<gradient:#f73bdb:#fc8bec:#f73bdb>an UwU sister</gradient>";
}
}

View File

@ -0,0 +1,25 @@
package com.artemis.the.gr8.playerstats.msg.msgutils;
import org.bukkit.map.MinecraftFont;
/**
* A small utility class that helps calculate how many dots
* to use to get the numbers of a top-statistic aligned.
*/
public final class FontUtils {
private FontUtils() {
}
public static int getNumberOfDotsToAlign(String displayText) {
return (int) Math.round((130.0 - MinecraftFont.Font.getWidth(displayText))/2);
}
public static int getNumberOfDotsToAlignForConsole(String displayText) {
return (int) Math.round((130.0 - MinecraftFont.Font.getWidth(displayText))/6) + 7;
}
public static int getNumberOfDotsToAlignForBoldText(String displayText) {
return (int) Math.round((130.0 - (MinecraftFont.Font.getWidth(displayText) * 1.5))/2);
}
}

View File

@ -1,65 +1,133 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
package com.artemis.the.gr8.playerstats.msg.msgutils;
import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
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 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.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.ApiStatus.Internal;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
/** A utility class that provides language keys to be put in a TranslatableComponent.*/
/**
*
* A utility class that provides language keys to be
* put in a TranslatableComponent.
*/
public final class LanguageKeyHandler {
private static Main plugin;
private static HashMap<Statistic, String> statNameKeys;
private static File languageKeyFile;
private static FileConfiguration languageKeys;
public LanguageKeyHandler() {
/**
* 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();
}
/** Checks if a given Key is the language key "stat_type.minecraft.killed"
or "commands.kill.success.single" (which results in "Killed %s").*/
private static void loadFile() {
languageKeyFile = new File(plugin.getDataFolder(), "language.yml");
if (!languageKeyFile.exists()) {
plugin.saveResource("language.yml", false);
}
languageKeys = YamlConfiguration.loadConfiguration(languageKeyFile);
}
@Internal
public static void reloadFile() {
if (!languageKeyFile.exists()) {
loadFile();
} else {
languageKeys = YamlConfiguration.loadConfiguration(languageKeyFile);
MyLogger.logLowLevelMsg("Language file reloaded!");
}
}
/**
* Checks if a given Key is the language key "stat_type.minecraft.killed"
* or "commands.kill.success.single" (which results in "Killed %s").
*
* @param statKey the Key to check
* @return true if this Key is key for kill-entity
*/
public static boolean isKeyForKillEntity(String statKey) {
return statKey.equalsIgnoreCase("stat_type.minecraft.killed") ||
statKey.equalsIgnoreCase("commands.kill.success.single");
}
/** Returns a language key to replace the default Statistic.Kill_Entity key.
@return the key "commands.kill.success.single", which results in "Killed %s" */
/**
* Returns a language key to replace the default Statistic.Kill_Entity key.
*
* @return the key "commands.kill.success.single", which results in "Killed %s"
*/
public static String getAlternativeKeyForKillEntity() {
return "commands.kill.success.single";
}
/** Checks if a given Key is the language key "stat_type.minecraft.killed_by"
or "stat.minecraft.deaths" (which results in "Number of Deaths").*/
/**
* Checks if a given Key is the language key "stat_type.minecraft.killed_by"
* or "stat.minecraft.deaths" (which results in "Number of Deaths").
*
* @param statKey the Key to check
* @return true if this Key is a key for entity-killed-by
*/
public static boolean isKeyForEntityKilledBy(String statKey) {
return statKey.equalsIgnoreCase("stat_type.minecraft.killed_by") ||
statKey.equalsIgnoreCase("stat.minecraft.deaths");
}
/** Returns a language key to replace the default Statistic.Entity_Killed_By key.
@return the key "stat.minecraft.deaths", which results in "Number of Deaths"
(meant to be followed by {@link #getAlternativeKeyForEntityKilledByArg()})*/
/**
* Returns a language key to replace the default Statistic.Entity_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";
}
/** Checks if a given Key is the language key "book.byAuthor"
(which results in "by %s"). */
/**
* Checks if a given Key is the language key "book.byAuthor"
* (which results in "by %s").
*
* @param statKey the Key to Check
* @return true if this Key is the key for book.byAuthor
*/
public static boolean isKeyForEntityKilledByArg(String statKey) {
return statKey.equalsIgnoreCase("book.byAuthor");
}
/** 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 #getAlternativeKeyForEntityKilledBy()}, you will get "Number of Deaths" "by %s"*/
/**
* 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 #getAlternativeKeyForEntityKilledBy()}, you will get "Number of Deaths" "by %s"
*/
public static String getAlternativeKeyForEntityKilledByArg() {
return "book.byAuthor";
}
/**
* @param key the String to turn into a normal name
* @return a pretty name
*/
public static String convertToName(String key) {
if (key.equalsIgnoreCase("soundCategory.block")) {
return Unit.BLOCK.getLabel();
@ -88,6 +156,31 @@ public final class LanguageKeyHandler {
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;
}
}
public static String getStatKeyTranslation(String statKey) {
String realKey = convertToNormalStatKey(statKey);
if (realKey == null) {
return "";
}
return languageKeys.getString(realKey);
}
/**
* @param statistic the Statistic to get the Key for
* @return the official Key from the NameSpacedKey for this Statistic,
* or return null if no enum constant can be retrieved.
*/
public String getStatKey(@NotNull Statistic statistic) {
if (statistic.getType() == Statistic.Type.UNTYPED) {
return "stat.minecraft." + statNameKeys.get(statistic);
@ -97,8 +190,11 @@ public final class LanguageKeyHandler {
}
}
/** Get the official Key from the NameSpacedKey for this entityType,
or return null if no enum constant can be retrieved or entityType is UNKNOWN.*/
/**
* @param entity the EntityType to get the Key for
* @return the official Key from the NameSpacedKey for this EntityType,
* or return null if no enum constant can be retrieved or EntityType is UNKNOWN.
*/
public @Nullable String getEntityKey(EntityType entity) {
if (entity == null || entity == EntityType.UNKNOWN) return null;
else {
@ -106,8 +202,11 @@ public final class LanguageKeyHandler {
}
}
/** Get the official Key from the NameSpacedKey for this item Material,
or return null if no enum constant can be retrieved.*/
/**
* @param item the Material to get the Key for
* @return the official Key from the NameSpacedKey for this item Material,
* or return null if no enum constant can be retrieved.
*/
public @Nullable String getItemKey(Material item) {
if (item == null) return null;
else if (item.isBlock()) {
@ -118,8 +217,11 @@ public final class LanguageKeyHandler {
}
}
/** Returns the official Key from the NameSpacedKey for the block Material provided,
or return null if no enum constant can be retrieved.*/
/**
* @param block the Material to get the Key for
* @return the official Key from the NameSpacedKey for the block Material provided,
* or return null if no enum constant can be retrieved.
*/
public @Nullable String getBlockKey(Material block) {
if (block == null) return null;
else if (block.toString().toLowerCase().contains("wall_banner")) { //replace wall_banner with regular banner, since there is no key for wall banners
@ -132,6 +234,10 @@ public final class LanguageKeyHandler {
}
}
/**
* @param unit the Unit to get the Key for
* @return "soundCategory.block" for Unit.Block, null otherwise
*/
public @Nullable String getUnitKey(Unit unit) {
if (unit == Unit.BLOCK) {
return "soundCategory.block";

View File

@ -1,12 +1,15 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
package com.artemis.the.gr8.playerstats.msg.msgutils;
import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import com.artemis.the.gr8.playerstats.enums.Unit;
import java.text.DecimalFormat;
/** A utility class that formats statistic numbers into something more readable.
It transforms {@link Unit} Type TIME, DAMAGE, and DISTANCE into a more Minecraft-appropriate Unit,
and adds commas to break up large numbers.*/
/**
* A utility class that formats statistic numbers into something more readable.
* It transforms numbers of {@link Unit.Type} Time, Damage, and Distance into numbers
* that are easier to understand (for example: from ticks to hours) and adds commas
* to break up large numbers.
*/
public final class NumberFormatter {
private final DecimalFormat format;
@ -17,15 +20,19 @@ public final class NumberFormatter {
format.setGroupingSize(3);
}
/** 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.*/
/**
* 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.
*/
public String formatNumber(long number) {
return format.format(number);
}
/** The unit of damage-based statistics is half a heart by default.
This method turns the number into hearts. */
/**
* 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
if (statUnit == Unit.HEART) {
return format.format(Math.round(number / 2.0));
@ -34,8 +41,12 @@ public final class NumberFormatter {
}
}
/** The unit of distance-based statistics is cm by default. This method turns it into blocks by default,
and turns it into km or leaves it as cm otherwise, depending on the config settings. */
/**
* The unit of distance-based statistics is cm by default.
* This method turns it into blocks by default,
* 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
switch (statUnit) {
case CM -> {
@ -53,7 +64,11 @@ public final class NumberFormatter {
}
}
/** The unit of time-based statistics is ticks by default.*/
/** The unit of time-based statistics is ticks by default.
*
* @return a String with the form "1D 2H 3M 4S"
* (depending on the Unit range selected)
*/
public String formatTimeNumber(long number, Unit biggestUnit, Unit smallestUnit) { //5 statistics
if (number <= 0) {
return "-";

View File

@ -1,21 +1,27 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
package com.artemis.the.gr8.playerstats.msg.msgutils;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.artemis.the.gr8.playerstats.utils.MyLogger;
/** A small utility class that helps make enum constant names prettier for output in stat-messages.*/
/**
* A small utility class that helps make enum constant
* names prettier for output in stat-messages.
*/
public final class StringUtils {
private StringUtils() {
}
/** Replace "_" with " " and capitalize each first letter of the input.
@param input String to prettify, case-insensitive*/
/**
* Replace "_" with " " and capitalize each first letter of the input.
*
* @param input String to prettify, case-insensitive
*/
public static String prettify(String input) {
if (input == null) return null;
StringBuilder capitals = new StringBuilder(input.toLowerCase());
capitals.setCharAt(0, Character.toUpperCase(capitals.charAt(0)));
while (capitals.indexOf("_") != -1) {
MyLogger.replacingUnderscores();
MyLogger.logHighLevelMsg("Replacing underscores and capitalizing names...");
int index = capitals.indexOf("_");
capitals.setCharAt(index + 1, Character.toUpperCase(capitals.charAt(index + 1)));

View File

@ -1,15 +1,18 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
package com.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.UnixTimeHandler;
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 org.bukkit.OfflinePlayer;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
/** The action that is executed when a reload-command is triggered. */
/**
* The action that is executed when a reload-command is triggered.
*/
final class ReloadAction extends RecursiveAction {
private static int threshold;
@ -21,10 +24,14 @@ final class ReloadAction extends RecursiveAction {
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.
/**
* 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 offlinePlayerUUIDs the ConcurrentHashMap to put resulting playerNames and UUIDs on
* @param offlinePlayerUUIDs the ConcurrentHashMap to put playerNames and UUIDs in
* @see OfflinePlayerHandler
*/
public ReloadAction(OfflinePlayer[] players,
int lastPlayedLimit, ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
@ -68,7 +75,7 @@ final class ReloadAction extends RecursiveAction {
for (int i = start; i < end; i++) {
OfflinePlayer player = players[i];
String playerName = player.getName();
MyLogger.actionRunning(Thread.currentThread().getName(), playerName, 1);
MyLogger.actionRunning(Thread.currentThread().getName());
if (playerName != null &&
(lastPlayedLimit == 0 || UnixTimeHandler.hasPlayedSince(lastPlayedLimit, player.getLastPlayed()))) {
offlinePlayerUUIDs.put(playerName, player.getUniqueId());

View File

@ -1,15 +1,16 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
package com.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.ShareManager;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRetriever;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
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;
@ -41,22 +42,26 @@ public final class ReloadThread extends Thread {
sender = se;
this.setName("ReloadThread-" + reloadThreadID);
MyLogger.threadCreated(this.getName());
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 StatRetriever},
and update the MessageBuilders in the {@link OutputManager}.*/
/**
* 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.threadStart(this.getName());
MyLogger.logHighLevelMsg(this.getName() + " started!");
if (statThread != null && statThread.isAlive()) {
try {
MyLogger.waitingForOtherThread(this.getName(), statThread.getName());
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());
@ -65,7 +70,7 @@ public final class ReloadThread extends Thread {
}
if (reloadThreadID != 1 && config.reloadConfig()) { //during a reload
MyLogger.logMsg("Reloading!", false);
MyLogger.logLowLevelMsg("Reloading!");
reloadEverything();
if (sender != null) {
@ -81,6 +86,7 @@ public final class ReloadThread extends Thread {
private void reloadEverything() {
MyLogger.setDebugLevel(config.getDebugLevel());
LanguageKeyHandler.reloadFile();
OutputManager.updateMessageBuilders();
OfflinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
ShareManager.updateSettings(config);
@ -92,8 +98,8 @@ public final class ReloadThread extends Thread {
OfflinePlayer[] offlinePlayers;
if (config.whitelistOnly()) {
offlinePlayers = Bukkit.getWhitelistedPlayers().toArray(OfflinePlayer[]::new);
MyLogger.logTimeTaken("ReloadThread",
"retrieved whitelist", time, DebugLevel.MEDIUM);
MyLogger.logMediumLevelTask("ReloadThread",
"retrieved whitelist", time);
}
else if (config.excludeBanned()) {
if (Bukkit.getPluginManager().getPlugin("LiteBans") != null) {
@ -107,13 +113,13 @@ public final class ReloadThread extends Thread {
.parallel()
.filter(offlinePlayer -> !bannedPlayers.contains(offlinePlayer)).toArray(OfflinePlayer[]::new);
}
MyLogger.logTimeTaken("ReloadThread",
"retrieved banlist", time, DebugLevel.MEDIUM);
MyLogger.logMediumLevelTask("ReloadThread",
"retrieved banlist", time);
}
else {
offlinePlayers = Bukkit.getOfflinePlayers();
MyLogger.logTimeTaken("ReloadThread",
"retrieved list of Offline Players", time, DebugLevel.MEDIUM);
MyLogger.logMediumLevelTask("ReloadThread",
"retrieved list of Offline Players", time);
}
int size = offlinePlayers != null ? offlinePlayers.length : 16;
@ -122,10 +128,10 @@ public final class ReloadThread extends Thread {
ReloadAction task = new ReloadAction(offlinePlayers, config.getLastPlayedLimit(), playerMap);
MyLogger.actionCreated((offlinePlayers != null) ? offlinePlayers.length : 0);
ForkJoinPool.commonPool().invoke(task);
MyLogger.actionFinished(1);
MyLogger.actionFinished();
MyLogger.logTimeTaken("ReloadThread",
("loaded " + playerMap.size() + " offline players"), time, DebugLevel.LOW);
MyLogger.logLowLevelTask("ReloadThread",
("loaded " + playerMap.size() + " offline players"), time);
return playerMap;
}
}

View File

@ -1,9 +1,9 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
package com.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
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;
@ -11,30 +11,35 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveTask;
/** The action that is executed when a stat-command is triggered. */
/**
* The action that is executed when a stat-command is triggered.
*/
final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>> {
private static int threshold;
private final OfflinePlayerHandler offlinePlayerHandler;
private final ImmutableList<String> playerNames;
private final StatRequest statRequest;
private final RequestSettings requestSettings;
private final ConcurrentHashMap<String, Integer> allStats;
/**
* Gets the statistic numbers for all players whose name is on the list, puts them in a ConcurrentHashMap
* using the default ForkJoinPool, and returns the ConcurrentHashMap when everything is done
* Gets the statistic numbers for all players whose name is on
* the list, puts them in a ConcurrentHashMap using the default
* ForkJoinPool, and returns the ConcurrentHashMap when
* everything is done.
*
* @param offlinePlayerHandler the OfflinePlayerHandler to convert playerNames into Players
* @param playerNames ImmutableList of playerNames for players that should be included in stat calculations
* @param statRequest a validated statRequest
* @param requestSettings a validated requestSettings object
* @param allStats the ConcurrentHashMap to put the results on
*/
public StatAction(OfflinePlayerHandler offlinePlayerHandler, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> allStats) {
public StatAction(OfflinePlayerHandler offlinePlayerHandler, ImmutableList<String> playerNames, RequestSettings requestSettings, ConcurrentHashMap<String, Integer> allStats) {
threshold = ThreadManager.getTaskThreshold();
this.offlinePlayerHandler = offlinePlayerHandler;
this.playerNames = playerNames;
this.statRequest = statRequest;
this.requestSettings = requestSettings;
this.allStats = allStats;
MyLogger.subActionCreated(Thread.currentThread().getName());
@ -46,8 +51,8 @@ final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>>
return getStatsDirectly();
}
else {
final StatAction subTask1 = new StatAction(offlinePlayerHandler, playerNames.subList(0, playerNames.size()/2), statRequest, allStats);
final StatAction subTask2 = new StatAction(offlinePlayerHandler, playerNames.subList(playerNames.size()/2, playerNames.size()), statRequest, allStats);
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);
//queue and compute all subtasks in the right order
subTask1.fork();
@ -61,19 +66,17 @@ final class StatAction extends RecursiveTask<ConcurrentHashMap<String, Integer>>
if (iterator.hasNext()) {
do {
String playerName = iterator.next();
MyLogger.actionRunning(Thread.currentThread().getName(), playerName, 2);
MyLogger.actionRunning(Thread.currentThread().getName());
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
int statistic = 0;
switch (statRequest.getStatistic().getType()) {
case UNTYPED -> statistic = player.getStatistic(statRequest.getStatistic());
case ENTITY -> statistic = player.getStatistic(statRequest.getStatistic(), statRequest.getEntity());
case BLOCK -> statistic = player.getStatistic(statRequest.getStatistic(), statRequest.getBlock());
case ITEM -> statistic = player.getStatistic(statRequest.getStatistic(), statRequest.getItem());
}
if (statistic > 0) {
allStats.put(playerName, statistic);
}
int statistic = 0;
switch (requestSettings.getStatistic().getType()) {
case UNTYPED -> statistic = player.getStatistic(requestSettings.getStatistic());
case ENTITY -> statistic = player.getStatistic(requestSettings.getStatistic(), requestSettings.getEntity());
case BLOCK -> statistic = player.getStatistic(requestSettings.getStatistic(), requestSettings.getBlock());
case ITEM -> statistic = player.getStatistic(requestSettings.getStatistic(), requestSettings.getItem());
}
if (statistic > 0) {
allStats.put(playerName, statistic);
}
} while (iterator.hasNext());
}

View File

@ -0,0 +1,85 @@
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

@ -0,0 +1,76 @@
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

@ -0,0 +1,60 @@
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

@ -0,0 +1,178 @@
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,7 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.request;
package com.artemis.the.gr8.playerstats.statistic.request;
import com.gmail.artemis.the.gr8.playerstats.api.RequestGenerator;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
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;
@ -10,26 +10,33 @@ import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
/** A StatRequest is the object PlayerStats uses to calculate and format the requested statistic.
This object can be generated 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 StatRequest to be valid, it needs the following values:
<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> (automatically set for all API-requests)
<li> if the <code>target</code> is Target.Player, a <code>playerName</code> needs to be added
</ul>*/
public final class StatRequest {
/**
* 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 boolean isAPIRequest;
private Statistic statistic;
private String playerName;
private Target target;
@ -41,37 +48,28 @@ public final class StatRequest {
private Material item;
private boolean playerFlag;
/** Create a new {@link StatRequest} with default values:
<br>- CommandSender sender (provided)
<br>- Target target = {@link Target#TOP}
<br>- int topListSize = 10
<br>- boolean playerFlag = false
<br>- boolean isAPIRequest
@param sender the CommandSender who prompted this RequestGenerator
@param isAPIRequest whether this RequestGenerator is coming through the API or the onCommand
/**
* 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
* <br>- boolean isAPIRequest
*
* @param sender the CommandSender who prompted this RequestGenerator
*/
private StatRequest(@NotNull CommandSender sender, boolean isAPIRequest) {
private RequestSettings(@NotNull CommandSender sender) {
this.sender = sender;
this.isAPIRequest = isAPIRequest;
target = Target.TOP;
playerFlag = false;
}
public static StatRequest getBasicRequest(CommandSender sender) {
return new StatRequest(sender, false);
public static RequestSettings getBasicRequest(CommandSender sender) {
return new RequestSettings(sender);
}
public static StatRequest getBasicAPIRequest() {
return new StatRequest(Bukkit.getConsoleSender(), true);
}
public void setAPIRequest() {
this.isAPIRequest = true;
}
public boolean isAPIRequest() {
return isAPIRequest;
public static RequestSettings getBasicAPIRequest() {
return new RequestSettings(Bukkit.getConsoleSender());
}
public @NotNull CommandSender getCommandSender() {

View File

@ -0,0 +1,60 @@
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

@ -0,0 +1,87 @@
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

@ -0,0 +1,62 @@
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

@ -0,0 +1,37 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,78 @@
package com.artemis.the.gr8.playerstats.statistic.result;
import com.artemis.the.gr8.playerstats.api.ApiFormatter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.TextComponent;
/**
* Holds the result of a completed stat-lookup. The <code>Type</code> parameter
* <code>T</code> of this StatResult represents the data type of the stored number:
* <ul>
* <li> <code>Integer</code> for playerStat
* <li> <code>Long</code> for serverStat
* <li> <code>LinkedHashMap(String, Integer)</code> for topStat
* </ul>
* You can get these raw numbers with {@link #getNumericalValue()}. Additionally,
* you can get a formatted message that contains the following information:
* <ul>
* <li> for playerStat:
* <br> [player-name]: [formatted-number] [stat-name] [sub-stat-name]
* <li> for serverStat:
* <br> [Total on] [server-name]: [formatted-number] [stat-name] [sub-stat-name]
* <li> for topStat:
* <br> [PlayerStats] [Top x] [stat-name] [sub-stat-name]
* <br> [1.] [player-name] [.....] [formatted-number]
* <br> [2.] [player-name] [.....] [formatted-number]
* <br> [3.] etc...
* </ul>
* 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
* <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
* 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}.
*/
public interface StatResult<T> {
/**
* 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
*/
T getNumericalValue();
/**
* 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();
/**
* 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();
}

View File

@ -0,0 +1,23 @@
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,4 +1,4 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
package com.artemis.the.gr8.playerstats.utils;
import org.bukkit.Material;
import org.bukkit.Statistic;
@ -12,11 +12,13 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** This class deals with Bukkit Enumerators. It holds private lists of all
block-, item-, entity- and statistic-names, and has one big list of all
possible sub-statistic-entries (block/item/entity). It can give the names
of all aforementioned enums, check if something is a valid enum constant,
and turn a name into its corresponding enum constant. */
/**
* This class deals with Bukkit Enumerators. It holds private lists of all
* block-, item-, entity- and statistic-names, and has one big list of all
* possible sub-statistic-entries (block/item/entity). It can give the names
* of all aforementioned enums, check if something is a valid enum constant,
* and turn a name into its corresponding enum constant.
*/
public final class EnumHandler {
private static List<String> blockNames;
@ -28,24 +30,40 @@ public final class EnumHandler {
prepareLists();
}
/** Returns all block-names in lowercase */
/**
* Returns all block-names in lowercase.
*
* @return the List
*/
public List<String> getBlockNames() {
return blockNames;
}
/** Returns all item-names in lowercase*/
/**
* Returns all item-names in lowercase.
*
* @return the List
*/
public List<String> getItemNames() {
return itemNames;
}
/** Returns all statistic-names in lowercase */
/**
* Returns all statistic-names in lowercase.
*
* @return the List
*/
public List<String> getStatNames() {
return statNames;
}
/** Returns the corresponding Material enum constant for an itemName
@param itemName String, case-insensitive
@return Material enum constant, uppercase */
/**
* Returns the corresponding Material enum constant for an itemName.
*
* @param itemName String (case-insensitive)
* @return Material enum constant (uppercase), or null if none
* can be found
*/
public static @Nullable Material getItemEnum(String itemName) {
if (itemName == null) return null;
@ -53,9 +71,13 @@ public final class EnumHandler {
return (item != null && item.isItem()) ? item : null;
}
/** Returns the corresponding EntityType enum constant for an entityName
@param entityName String, case-insensitive
@return EntityType enum constant, uppercase */
/**
* Returns the corresponding EntityType enum constant for an entityName.
*
* @param entityName String (case-insensitive)
* @return EntityType enum constant (uppercase), or null if none
* can be found
*/
public static @Nullable EntityType getEntityEnum(String entityName) {
try {
return EntityType.valueOf(entityName.toUpperCase());
@ -65,9 +87,13 @@ public final class EnumHandler {
}
}
/** Returns the corresponding Material enum constant for a materialName
@param materialName String, case-insensitive
@return Material enum constant, uppercase */
/**
* Returns the corresponding Material enum constant for a materialName.
*
* @param materialName String (case-insensitive)
* @return Material enum constant (uppercase), or null if none
* can be found
*/
public static @Nullable Material getBlockEnum(String materialName) {
if (materialName == null) return null;
@ -75,8 +101,12 @@ public final class EnumHandler {
return (block != null && block.isBlock()) ? block : null;
}
/** Returns the statistic enum constant, or null if that failed.
@param statName String, case-insensitive */
/**
* Returns the statistic enum constant, or null if that failed.
*
* @param statName String (case-insensitive)
* @return the Statistic enum constant, or null
*/
public static @Nullable Statistic getStatEnum(@NotNull String statName) {
try {
return Statistic.valueOf(statName.toUpperCase());
@ -86,24 +116,58 @@ public final class EnumHandler {
}
}
/** Checks if string is a valid statistic
@param statName String, case-insensitive */
/**
* Checks if string is a valid {@link Statistic}.
*
* @param statName the String to check (case-insensitive)
* @return true if this String is a valid Statistic
*/
public boolean isStatistic(@NotNull String statName) {
return statNames.contains(statName.toLowerCase());
}
/** Checks whether the given String equals the name of an entity-type statistic. */
/**
* Checks whether the given String equals the name of a
* {@link Statistic} of Type.Entity.
*
* @param statName the String to check (case-insensitive)
* @return true if this String is a Statistic of Type.Entity
*/
public boolean isEntityStatistic(String statName) {
return statName.equalsIgnoreCase(Statistic.ENTITY_KILLED_BY.toString()) ||
statName.equalsIgnoreCase(Statistic.KILL_ENTITY.toString());
}
/** Checks if this statistic is a subStatEntry, meaning it is a block, item or entity
@param statName String, case-insensitive*/
/**
* Checks if this statistic is a subStatEntry, meaning it is a block,
* item or entity.
*
* @param statName the String to check (case-insensitive)
* @return true if this String is a Statistic that is not
* of Type.Untyped
*/
public boolean isSubStatEntry(@NotNull String statName) {
return subStatNames.contains(statName.toLowerCase());
}
/**
* Gets the name of the given Statistic.Type
*
* @param statType the Type of the Statistic to check
* @return "block", "entity", "item", or "sub-statistic" if the
* provided Type is null.
*/
public static String getSubStatTypeName(Statistic.Type statType) {
String subStat = "sub-statistic";
if (statType == null) return subStat;
switch (statType) {
case BLOCK -> subStat = "block";
case ENTITY -> subStat = "entity";
case ITEM -> subStat = "item";
}
return subStat;
}
private void prepareLists() {
List<String> entityNames = Arrays.stream(EntityType.values())
.map(EntityType::toString)

View File

@ -0,0 +1,169 @@
package com.artemis.the.gr8.playerstats.utils;
import com.artemis.the.gr8.playerstats.enums.DebugLevel;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
/**
* The PlayerStats Logger
*/
public final class MyLogger {
private static final Logger logger;
private static DebugLevel debugLevel;
private static ConcurrentHashMap<String, Integer> threadNames;
static {
Plugin plugin = Bukkit.getPluginManager().getPlugin("PlayerStats");
logger = (plugin != null) ? plugin.getLogger() : Bukkit.getLogger();
debugLevel = DebugLevel.LOW;
threadNames = new ConcurrentHashMap<>();
}
private MyLogger() {
}
/**
* Sets the desired debugging level.
* <br>1 = low (only show unexpected errors)
* <br>2 = medium (detail all encountered exceptions, log main tasks and show time taken)
* <br>3 = high (log all tasks and time taken)
* <br>Default: 1
*/
public static void setDebugLevel(int level) {
if (level == 2) {
debugLevel = DebugLevel.MEDIUM;
}
else if (level == 3) {
debugLevel = DebugLevel.HIGH;
}
else {
debugLevel = DebugLevel.LOW;
}
}
public static void logLowLevelMsg(String content) {
logger.info(content);
}
public static void logMediumLevelMsg(String content) {
if (debugLevel != DebugLevel.LOW) {
logger.info(content);
}
}
public static void logHighLevelMsg(String content) {
if (debugLevel == DebugLevel.HIGH) {
logger.info(content);
}
}
public static void logWarning(String content) {
logger.warning(content);
}
/**
* Log the encountered exception as a warning to console,
* with some information about which class/method caught it
* and with a printStackTrace if DebugLevel is HIGH.
*
* @param exception The encountered exception
* @param caughtBy The name of the class that caught the exception
* @param additionalInfo e.g. the method-name or line where the
* exception is caught
*/
public static void logException(@NotNull Exception exception, String caughtBy, @Nullable String additionalInfo) {
String extraInfo = (additionalInfo != null) ? " [" + additionalInfo + "]" : "";
String info = " (" + caughtBy + extraInfo + ")";
logger.warning(exception + info);
if (debugLevel == DebugLevel.HIGH) {
exception.printStackTrace();
}
}
/**
* If DebugLevel is MEDIUM or HIGH, output to console that an
* action has started.
*
* @param taskLength Length of the action (in terms of
* units-to-process)
*/
public static void actionCreated(int taskLength) {
if (debugLevel != DebugLevel.LOW) {
threadNames = new ConcurrentHashMap<>();
logger.info("Initial Action created for " + taskLength + " Players. Processing...");
}
}
/**
* Internally save the name of the executing thread for later
* logging of this action. The list of names is reset upon the
* start of every new action.
*
* @param threadName Name of the executing thread
*/
public static void subActionCreated(String threadName) {
if (debugLevel == DebugLevel.HIGH) {
if (!threadNames.containsKey(threadName)) {
threadNames.put(threadName, threadNames.size());
}
}
}
/**
* Internally save the name of the executing thread for logging.
*
* @param threadName Name of the executing thread
*/
public static void actionRunning(String threadName) {
if (debugLevel != DebugLevel.LOW) {
if (!threadNames.containsKey(threadName)) {
threadNames.put(threadName, threadNames.size());
}
}
}
/**
* Output to console that an action has finished if DebugLevel is
* MEDIUM or higher. If DebugLevel is HIGH, also output the names
* of the threads that were used.
*/
public static void actionFinished() {
if (debugLevel != DebugLevel.LOW) {
logger.info("Finished Recursive Action! In total " +
threadNames.size() + " Threads were used");
}
if (debugLevel == DebugLevel.HIGH) {
logger.info(Collections.list(threadNames.keys()).toString());
}
}
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 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");
}
}

View File

@ -0,0 +1,87 @@
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

@ -0,0 +1,26 @@
package com.artemis.the.gr8.playerstats.utils;
/**
* A small utility class that calculates with unix time.
*/
public final class UnixTimeHandler {
private UnixTimeHandler() {
}
/**
* Calculates whether a player has played recently enough
* to fall within the lastPlayedLimit. If lastPlayedLimit == 0,
* this always returns true (since there is no limit).
*
* @param lastPlayed a long that represents the amount of
* milliseconds between the unix start point
* and the time this player last joined
* @param lastPlayedLimit a long that represents the maximum-
* number-of-days-since-last-joined
*/
public static boolean hasPlayedSince(long lastPlayedLimit, long lastPlayed) {
long maxLastPlayed = System.currentTimeMillis() - lastPlayedLimit * 24 * 60 * 60 * 1000;
return lastPlayedLimit == 0 || lastPlayed >= maxLastPlayed;
}
}

View File

@ -1,120 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.api.PlayerStats;
import com.gmail.artemis.the.gr8.playerstats.api.PlayerStatsAPI;
import com.gmail.artemis.the.gr8.playerstats.commands.ReloadCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.ShareCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.StatCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.TabCompleter;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.listeners.JoinListener;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRetriever;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.NotNull;
public final class Main extends JavaPlugin {
private static BukkitAudiences adventure;
private static ConfigHandler config;
private static OfflinePlayerHandler offlinePlayerHandler;
private static EnumHandler enumHandler;
private static OutputManager outputManager;
private static ShareManager shareManager;
private static ThreadManager threadManager;
private static PlayerStats playerStatsAPI;
@Override
public void onEnable() {
new Metrics(this, 15923);
//initialize all the Managers, singletons, ConfigHandler and the API
initializeMainClasses();
//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!");
}
public static @NotNull BukkitAudiences getAdventure() throws IllegalStateException {
if (adventure == null) {
throw new IllegalStateException("Tried to access Adventure without PlayerStats being enabled!");
}
return adventure;
}
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 EnumHandler getEnumHandler() {
if (enumHandler == null) {
enumHandler = new EnumHandler();
}
return enumHandler;
}
public static @NotNull OfflinePlayerHandler getOfflinePlayerHandler() throws IllegalStateException {
if (offlinePlayerHandler == null) {
throw new IllegalStateException("PlayerStats does not seem to be fully loaded!");
}
return offlinePlayerHandler;
}
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() {
adventure = BukkitAudiences.create(this);
config = new ConfigHandler(this);
enumHandler = new EnumHandler();
offlinePlayerHandler = new OfflinePlayerHandler();
shareManager = new ShareManager(config);
outputManager = new OutputManager(getAdventure(), config, shareManager);
StatRetriever statRetriever = new StatRetriever(offlinePlayerHandler);
threadManager = new ThreadManager(config, statRetriever, outputManager);
playerStatsAPI = new PlayerStatsAPI(statRetriever, outputManager, offlinePlayerHandler);
}
}

View File

@ -1,97 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRetriever;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.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 StatRetriever statRetriever;
private ReloadThread lastActiveReloadThread;
private StatThread lastActiveStatThread;
private final HashMap<String, Thread> statThreads;
private static long lastRecordedCalcTime;
public ThreadManager(ConfigHandler config, StatRetriever statRetriever, OutputManager outputManager) {
ThreadManager.config = config;
ThreadManager.outputManager = outputManager;
ThreadManager.statRetriever = statRetriever;
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.threadAlreadyRunning(lastActiveReloadThread.getName());
}
}
public void startStatThread(StatRequest statRequest) {
statThreadID += 1;
String cmdSender = statRequest.getCommandSender().getName();
if (config.limitStatRequests() && statThreads.containsKey(cmdSender)) {
Thread runningThread = statThreads.get(cmdSender);
if (runningThread.isAlive()) {
outputManager.sendFeedbackMsg(statRequest.getCommandSender(), StandardMessage.REQUEST_ALREADY_RUNNING);
} else {
startNewStatThread(statRequest);
}
} else {
startNewStatThread(statRequest);
}
}
/** 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 statRequest) {
lastActiveStatThread = new StatThread(outputManager, statRetriever, statThreadID, statRequest, lastActiveReloadThread);
statThreads.put(statRequest.getCommandSender().getName(), lastActiveStatThread);
lastActiveStatThread.start();
}
}

View File

@ -1,33 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentUtils;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.Statistic;
public interface Formatter {
/** Turns a TextComponent into its String representation. This method is equipped
to turn all PlayerStats' formatted statResults into String.
@return a String representation of this TextComponent, without hover/click events,
but with color, style and formatting. TranslatableComponents will be turned into
plain English.*/
default String TextComponentToString(TextComponent component) {
return ComponentUtils.getTranslatableComponentSerializer()
.serialize(component);
}
/** @return [PlayerStats]*/
TextComponent getPluginPrefix();
TextComponent getRainbowPluginPrefix();
/** @return ________ [PlayerStats] ________*/
TextComponent getPluginPrefixAsTitle();
TextComponent getRainbowPluginPrefixAsTitle();
/** @return a single line from a top-x statistic:
* <br> x. Player-name ......... number */
TextComponent formatSingleTopStatLine(int positionInTopList, String playerName, long statNumber, Statistic statistic);
}

View File

@ -1,30 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.Main;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
/** The outgoing API that you can use to access the core functionality of PlayerStats!
To work with the API, you need to call PlayerStats.{@link #getAPI()} to get an instance of
{@link PlayerStatsAPI}. You can then use this object to access any of the further methods.
<br>
<br>Since calculating a top or server statistics can take some time, I strongly
encourage you to call all the serverStat() and topStat() methods asynchronously.
Otherwise, the main Thread will have to wait until all calculations are done,
and this can severely impact server performance.
*/
public interface PlayerStats {
/** Gets an instance of the {@link PlayerStatsAPI}.
@return the PlayerStats API
@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();
}
StatManager getStatManager();
Formatter getFormatter();
}

View File

@ -1,65 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRetriever;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.*;
import com.gmail.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 StatRetriever statRetriever;
private static StatFormatter statFormatter;
@Internal
public PlayerStatsAPI(StatRetriever stat, StatFormatter format, OfflinePlayerHandler offlinePlayers) {
statRetriever = stat;
statFormatter = format;
offlinePlayerHandler = offlinePlayers;
}
static StatRetriever statCalculator() {
return statRetriever;
}
static StatFormatter statFormatter() {
return statFormatter;
}
@Override
public Formatter getFormatter() {
return statFormatter;
}
@Override
public StatManager getStatManager() {
return this;
}
@Override
public RequestGenerator<Integer> getPlayerStat(String playerName) {
StatRequest request = StatRequestHandler.getBasicPlayerStatRequest(playerName);
return new PlayerStatRequest(request);
}
@Override
public ServerStatRequest calculateServerStat() {
StatRequest request = StatRequestHandler.getBasicServerStatRequest();
return new ServerStatRequest(request);
}
@Override
public TopStatRequest calculateTopStat(int topListSize) {
StatRequest request = StatRequestHandler.getBasicTopStatRequest(topListSize);
return new TopStatRequest(request);
}
@Override
public TopStatRequest calculateTotalTopStatList() {
int playerCount = offlinePlayerHandler.getOfflinePlayerCount();
return calculateTopStat(playerCount);
}
}

View File

@ -1,20 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRetriever;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.StatResult;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
/** Completes a basic {@link StatRequest} provided by the {@link PlayerStatsAPI}
and performs a statistic lookup with the information that is stored inside this StatRequest.*/
public interface RequestExecutor<T> {
static StatRetriever getStatCalculator() {
return PlayerStatsAPI.statCalculator();
}
static StatFormatter getStatFormatter() {
return PlayerStatsAPI.statFormatter();
}
StatResult<T> execute();
}

View File

@ -1,40 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRetriever;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
/** Turns user input into a completed {@link StatRequest}. This StatRequest should hold all
the information PlayerStats needs to work with, and is used by the {@link StatRetriever}
to get the desired statistic data.*/
public interface RequestGenerator<T> {
/** Gets a StatRequest for a Statistic of Statistic.Type {@code Untyped}.
@param statistic a Statistic of Type.Untyped
@return a {@link StatRequest}
@throws IllegalArgumentException if <code>statistic</code> is not of Type.Untyped*/
RequestExecutor<T> untyped(@NotNull Statistic statistic) throws IllegalArgumentException;
/** Gets a StatRequest for a Statistic of Statistic.Type Block or Item.
@param statistic a Statistic of Type.Block or Type.Item
@param material a block if the <code>statistic</code> is of Type.Block,
and an item if the <code>statistic</code> is of Type.Item
@return a {@link StatRequest}
@throws IllegalArgumentException if <code>statistic</code> is not of Type.Block
(with a block as <code>material</code>), or <code>statistic</code> is not of Type.Item
(with an item as <code>material</code>) */
RequestExecutor<T> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException;
/** Gets a StatRequest for a Statistic of Statistic.Type Entity.
@param statistic a Statistic of Type.Entity
@param entityType an EntityType
@return a {@link StatRequest}
@throws IllegalArgumentException if <code>statistic</code> is not of Type.Entity*/
RequestExecutor<T> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException;
}

View File

@ -1,43 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.*;
import org.jetbrains.annotations.ApiStatus.Internal;
import java.util.LinkedHashMap;
/** The {@link StatFormatter} formats raw numbers into pretty messages.
This Formatter takes a {@link StatRequest} and combines it with the raw number(s)
returned by the {@link StatCalculator}, and transforms those into a pretty message
(by default a TextComponent) with all the relevant information in it.
<br>
<br>The output is ready to be sent to a Minecraft client or console with the Adventure library.
To send a Component, you need to get a {@link BukkitAudiences} object. Normally you would
have to add the library as a dependency, but since the library is included in PlayerStats, you can
access it directly. 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>.
<br>
<br>Alternatively, you can also turn your TextComponent into a plain String with
{@link #TextComponentToString(TextComponent)}. Don't use Adventure's method .content()
on your formattedValue to do this - because of the way the TextComponent is built by PlayerStats,
you won't be able to get the full content that way.*/
@Internal
public
interface StatFormatter extends Formatter {
/** @return a TextComponent with the following parts:
<br>[player-name]: [number] [stat-name] {sub-stat-name}*/
TextComponent formatPlayerStat(StatRequest statRequest, int playerStat);
/** @return a TextComponent with the following parts:
<br>[Total on] [server-name]: [number] [stat-name] [sub-stat-name]*/
TextComponent formatServerStat(StatRequest statRequest, 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 formatTopStat(StatRequest statRequest, LinkedHashMap<String, Integer> topStats);
}

View File

@ -1,35 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import java.util.LinkedHashMap;
public interface StatManager {
/** Gets a StatRequest object that can be used to look up a player-statistic.
This StatRequest will have all default settings already configured,
and will be processed as soon as you call one of its methods.
@return a PlayerStatRequest that can be used to look up a statistic for the
Player whose name is provided*/
RequestGenerator<Integer> getPlayerStat(String playerName);
/** Gets a StatRequest object that can be used to look up a server-statistic.
This StatRequest will have all default settings already configured,
and will be processed as soon as you call one of its methods.
<br>
<br> Don't call this from the main Thread! (see class description)
@return a ServerStatRequest that can be used to look up a server total*/
RequestGenerator<Long> calculateServerStat();
/** Gets a StatRequest object that can be used to look up a top-x-statistic.
This StatRequest will have all default settings already configured, and will be
processed as soon as you call one of its methods.
<br>
<br> Don't call this from the main Thread! (see class description)
@param topListSize how big the top-x should be (10 by default)
@return a TopStatRequest that can be used to look up a top statistic*/
RequestGenerator<LinkedHashMap<String, Integer>> calculateTopStat(int topListSize);
RequestGenerator<LinkedHashMap<String, Integer>> calculateTotalTopStatList();
}

View File

@ -1,414 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.config;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import com.gmail.artemis.the.gr8.playerstats.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;
public final class ConfigHandler {
private static Main plugin;
private static int configVersion;
private File configFile;
private FileConfiguration config;
public ConfigHandler(Main plugin) {
ConfigHandler.plugin = plugin;
configVersion = 6;
saveDefaultConfig();
config = YamlConfiguration.loadConfiguration(configFile);
checkConfigVersion();
MyLogger.setDebugLevel(getDebugLevel());
}
/** Checks the number that "config-version" returns to see if the config needs updating, and if so, send it to the {@link ConfigUpdateHandler}.
<br></br>
<br>PlayerStats 1.1: "config-version" doesn't exist.</br>
<br>PlayerStats 1.2: "config-version" is 2.</br>
<br>PlayerStats 1.3: "config-version" is 3. </br>
<br>PlayerStats 1.4: "config-version" is 4.</br>*/
private void checkConfigVersion() {
if (!config.contains("config-version") || config.getInt("config-version") != configVersion) {
new ConfigUpdateHandler(plugin, configFile, configVersion);
reloadConfig();
}
}
/** 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");
}
/** 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}. */
public boolean reloadConfig() {
if (!configFile.exists()) {
saveDefaultConfig();
}
try {
config = YamlConfiguration.loadConfiguration(configFile);
return true;
}
catch (IllegalArgumentException e) {
MyLogger.logException(e, "ConfigHandler", "reloadConfig");
return false;
}
}
/** Returns the desired debugging level.
<br></br>
<br>1 = low (only show unexpected errors)</br>
<br>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</br>
<br>3 = high (log all tasks and time taken)</br>
<br></br>
<br>Default: 1</br>*/
public int getDebugLevel() {
return config.getInt("debug-level", 1);
}
/** Returns true if command-senders should be limited to one stat-request at a time.
<br>Default: true</br>*/
public boolean limitStatRequests() {
return config.getBoolean("only-allow-one-lookup-at-a-time-per-player", true);
}
/** Returns true if stat-sharing is allowed.
<br>Default: true</br>*/
public boolean allowStatSharing() {
return config.getBoolean("enable-stat-sharing", true);
}
/** Returns the number of minutes a player has to wait before being able to
share another stat-result.
<br>Default: 0</br>*/
public int getStatShareWaitingTime() {
return config.getInt("waiting-time-before-sharing-again", 0);
}
/** Returns the config setting for include-whitelist-only.
<br>Default: false</br>*/
public boolean whitelistOnly() {
return config.getBoolean("include-whitelist-only", false);
}
/** Returns the config setting for exclude-banned-players.
<br>Default: false</br>*/
public boolean excludeBanned() {
return config.getBoolean("exclude-banned-players", false);
}
/** Returns the number of maximum days since a player has last been online.
<br>Default: 0 (which signals not to use this limit)</br>*/
public int getLastPlayedLimit() {
return config.getInt("number-of-days-since-last-joined", 0);
}
/** Whether to use TranslatableComponents wherever possible.
Currently supported: statistic, block, item and entity names.
<br>Default: true</br>*/
public boolean useTranslatableComponents() {
return config.getBoolean("translate-to-client-language", true);
}
/** Whether to use HoverComponents for additional information.
<br>Default: true</br>*/
public boolean useHoverText() {
return config.getBoolean("enable-hover-text", true);
}
/** Whether to use festive formatting, such as pride colors.
<br>Default: true</br> */
public boolean useFestiveFormatting() {
return config.getBoolean("enable-festive-formatting", true);
}
/** Whether to use rainbow colors for the [PlayerStats] prefix rather than the default gold/purple.
<br>Default: false</br> */
public boolean useRainbowMode() {
return config.getBoolean("rainbow-mode", false);
}
/** Whether to use enters before the statistic output in chat.
Enters create some separation between the previous things that have been said in chat and the stat-result.
<br>Default: true for non-shared top statistics, false for everything else</br>*/
public boolean useEnters(Target selection, boolean getSharedSetting) {
ConfigurationSection section = config.getConfigurationSection("use-enters");
boolean def = selection == Target.TOP && !getSharedSetting;
if (section != null) {
String path = switch (selection) {
case TOP -> getSharedSetting ? "top-stats-shared" : "top-stats";
case PLAYER -> getSharedSetting ? "player-stats-shared" : "player-stats";
case SERVER -> getSharedSetting ? "server-stats-shared" : "server-stats";
};
return section.getBoolean(path, def);
}
MyLogger.logMsg("Config settings for use-enters could not be retrieved! " +
"Please check your file if you want to use custom settings. " +
"Using default values...", true);
return def;
}
/** Returns the config setting for use-dots.
<br>Default: true</br>*/
public boolean useDots() {
return config.getBoolean("use-dots", true);
}
/** Returns the config setting for top-list-max-size.
<br>Default: 10</br> */
public int getTopListMaxSize() {
return config.getInt("top-list-max-size", 10);
}
/** Returns a String that represents the title for a top statistic.
<br>Default: "Top"</br>*/
public String getTopStatsTitle() {
return config.getString("top-list-title", "Top");
}
/** Returns a String that represents the title for a server stat.
<br>Default: "Total on"</br> */
public String getServerTitle() {
return config.getString("total-server-stat-title", "Total on");
}
/** Returns the specified server name for a server stat title.
<br>Default: "this server"</br>*/
public String getServerName() {
return config.getString("your-server-name", "this server");
}
/** Returns the unit that should be used for distance-related statistics.
<br>Default: Blocks for plain text, km for hover-text</br>*/
public String getDistanceUnit(boolean isUnitForHoverText) {
return getUnitString(isUnitForHoverText, "blocks", "km", "distance-unit");
}
/** Returns the unit that should be used for damage-based statistics.
<br>Default: Hearts for plain text, HP for hover-text.</br>*/
public String getDamageUnit(boolean isUnitForHoverText) {
return getUnitString(isUnitForHoverText, "hearts", "hp", "damage-unit");
}
/** Whether PlayerStats should automatically detect the most suitable unit to use for time-based statistics.
<br>Default: true</br>*/
public boolean autoDetectTimeUnit(boolean isUnitForHoverText) {
String path = "auto-detect-biggest-time-unit";
if (isUnitForHoverText) {
path = path + "-for-hover-text";
}
boolean defaultValue = !isUnitForHoverText;
return config.getBoolean(path, defaultValue);
}
/** How many additional units should be displayed next to the most suitable largest unit for time-based statistics.
<br>Default: 1 for plain text, 0 for hover-text</br>*/
public int getNumberOfExtraTimeUnits(boolean isUnitForHoverText) {
String path = "number-of-extra-units";
if (isUnitForHoverText) {
path = path + "-for-hover-text";
}
int defaultValue = isUnitForHoverText ? 0 : 1;
return config.getInt(path, defaultValue);
}
/** Returns the unit that should be used for time-based statistics.
(this will return the largest unit that should be used).
<br>Default: days for plain text, hours for hover-text</br>*/
public String getTimeUnit(boolean isUnitForHoverText) {
return getTimeUnit(isUnitForHoverText, false);
}
/** Returns the unit that should be used for time-based statistics. If the optional smallUnit flag is true,
this will return the smallest unit (and otherwise the largest).
<br>Default: hours for plain text, seconds for hover-text</br>*/
public String getTimeUnit(boolean isUnitForHoverText, boolean smallUnit) {
if (smallUnit) {
return getUnitString(isUnitForHoverText, "hours", "seconds", "smallest-time-unit");
}
return getUnitString(isUnitForHoverText, "days", "hours", "biggest-time-unit");
}
/** Returns an integer between 0 and 100 that represents how much lighter a hoverColor should be.
So 20 would mean 20% lighter.
<br>Default: 20</br>*/
public int getHoverTextAmountLighter() {
return config.getInt("hover-text-amount-lighter", 20);
}
/** Returns a String that represents either a Chat Color, hex color code, or a Style. Default values are:
* <br>Style: "italic"</br>
* <br>Color: "gray"</br>*/
public String getSharedByTextDecoration(boolean getStyleSetting) {
String def = getStyleSetting ? "italic" : "gray";
return getDecorationString(null, getStyleSetting, def, "shared-by");
}
/** Returns a String that represents either a Chat Color, hex color code, or a Style. Default values are:
* <br>Style: "none"</br>
* <br>Color: "#845EC2"</br>*/
public String getSharerNameDecoration(boolean getStyleSetting) {
return getDecorationString(null, getStyleSetting, "#845EC2", "player-name");
}
/** Returns a String that represents either a Chat Color, hex color code, or a Style. Default values are:
<br>Style: "none"</br>
<br>Color Top: "green"</br>
<br>Color Individual/Server: "gold"</br>*/
public String getPlayerNameDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "green";
}
else {
def = "gold";
}
return getDecorationString(selection, getStyleSetting, def, "player-names");
}
/** Returns true if playerNames Style is "bold" for a top-stat, false if it is not.
<br>Default: false</br>*/
public boolean playerNameIsBold() {
ConfigurationSection style = getRelevantSection(Target.TOP);
if (style != null) {
String styleString = style.getString("player-names");
return styleString != null && styleString.equalsIgnoreCase("bold");
}
return false;
}
/** Returns a String that represents either a Chat Color, hex color code, or a Style. Default values are:
<br>Style: "none"</br>
<br>Color: "yellow"</br>*/
public String getStatNameDecoration(Target selection, boolean getStyleSetting) {
return getDecorationString(selection, getStyleSetting, "yellow", "stat-names");
}
/** Returns a String that represents either a Chat Color, hex color code, or a Style. Default values are:
<br>Style: "none"</br>
<br>Color: "#FFD52B"</br>*/
public String getSubStatNameDecoration(Target selection, boolean getStyleSetting) {
return getDecorationString(selection, getStyleSetting, "#FFD52B", "sub-stat-names");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<br>Style: "none"</br>
<br>Color Top: "#55AAFF"</br>
<br>Color Individual/Server: "#ADE7FF"</br> */
public String getStatNumberDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "#55AAFF";
}
else {
def = "#ADE7FF";
}
return getDecorationString(selection, getStyleSetting, def,"stat-numbers");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<br>Style: "none"</br>
<br>Color Top: "yellow"</br>
<br>Color Server: "gold"</br>*/
public String getTitleDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "yellow";
}
else {
def = "gold";
}
return getDecorationString(selection, getStyleSetting, def, "title");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<br>Style: "none"</br>
<br>Color: "gold"</br>*/
public String getTitleNumberDecoration(boolean getStyleSetting) {
return getDecorationString(Target.TOP, getStyleSetting, "gold", "title-number");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<br>Style: "none"</br>
<br>Color: "#FFB80E"</br>*/
public String getServerNameDecoration(boolean getStyleSetting) {
return getDecorationString(Target.SERVER, getStyleSetting, "#FFB80E", "server-name");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<br>Style: "none"</br>
<br>Color: "gold"</br>*/
public String getRankNumberDecoration(boolean getStyleSetting) {
return getDecorationString(Target.TOP, getStyleSetting, "gold", "rank-numbers");
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<br>Style: "none"</br>
<br>Color: "dark_gray"</br> */
public String getDotsDecoration(boolean getStyleSetting) {
return getDecorationString(Target.TOP, getStyleSetting, "dark_gray", "dots");
}
/** Returns a String representing the {@link Unit} that should be used for a certain {@link Unit.Type}.
If no String can be retrieved from the config, the supplied defaultValue will be returned.
If the defaultValue is different for hoverText, an optional String defaultHoverValue can be supplied.
@param isHoverText if true, the unit for hovering text is returned, otherwise the unit for plain text
@param defaultValue the default unit for plain text
@param defaultHoverValue the default unit for hovering text
@param pathName the config path to retrieve the value from*/
private String getUnitString(boolean isHoverText, String defaultValue, String defaultHoverValue, String pathName) {
String path = isHoverText ? pathName + "-for-hover-text" : pathName;
String def = defaultValue;
if (isHoverText && defaultHoverValue != null) {
def = defaultHoverValue;
}
return config.getString(path, def);
}
/** Returns the config value for a color or style option in string-format, the supplied default value,
or null if no configSection was found.
@param selection the Target this decoration is meant for (Player, Server or Top)
@param getStyleSetting if true, the result will be a style String, otherwise a color String
@param defaultColor the default color to return if the config value cannot be found (for style, the default is always "none")
@param pathName the config path to retrieve the value from*/
private @Nullable String getDecorationString(Target selection, boolean getStyleSetting, String defaultColor, String pathName){
String path = getStyleSetting ? pathName + "-style" : pathName;
String defaultValue = getStyleSetting ? "none" : defaultColor;
ConfigurationSection section = getRelevantSection(selection);
return section != null ? section.getString(path, defaultValue) : null;
}
/** Returns the config section that contains the relevant color or style option. */
private @Nullable ConfigurationSection getRelevantSection(Target selection) {
if (selection == null) { //rather than rework the whole Target enum, I have added shared-stats as the null-option for now
return config.getConfigurationSection("shared-stats");
}
switch (selection) {
case TOP -> {
return config.getConfigurationSection("top-list");
}
case PLAYER -> {
return config.getConfigurationSection("individual-statistics");
}
case SERVER -> {
return config.getConfigurationSection("total-server");
}
default -> {
return null;
}
}
}
}

View File

@ -1,10 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
/** Represents the debugging level that PlayerStats can use.
<br></br>
<br>1 = low (only show unexpected errors)</br>
<br>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</br>
<br>3 = high (log all tasks and time taken)</br>*/
public enum DebugLevel {
LOW, MEDIUM, HIGH
}

View File

@ -1,108 +0,0 @@
package com.gmail.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,6 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
/** This enum represents the targets PlayerStats accepts for a stat-lookup (Player, Server and Top).*/
public enum Target {
PLAYER, SERVER, TOP
}

View File

@ -1,567 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.msg;
import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ExampleMessage;
import com.gmail.artemis.the.gr8.playerstats.msg.components.HelpMessage;
import com.gmail.artemis.the.gr8.playerstats.msg.msgutils.*;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
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.NotNull;
import java.util.*;
import java.util.function.BiFunction;
import static net.kyori.adventure.text.Component.*;
/** Composes messages to send to a Player or Console. This class is responsible
for constructing a final Component with the text content of the desired message.
The component parts (with appropriate formatting) are supplied by a {@link ComponentFactory}.
By default, this class works with the default ComponentFactory, but you can
give it a different ComponentFactory upon creation.*/
public final class MessageBuilder {
private static ConfigHandler config;
private boolean useHoverText;
private boolean isConsoleBuilder;
private final ComponentFactory componentFactory;
private final LanguageKeyHandler languageKeyHandler;
private final NumberFormatter formatter;
private MessageBuilder(ConfigHandler config) {
this (config, new ComponentFactory(config));
}
private MessageBuilder(ConfigHandler configHandler, ComponentFactory factory) {
config = configHandler;
useHoverText = config.useHoverText();
componentFactory = factory;
formatter = new NumberFormatter();
languageKeyHandler = new LanguageKeyHandler();
MyLogger.logMsg("MessageBuilder created with factory: " + componentFactory.getClass().getSimpleName(), DebugLevel.MEDIUM);
}
public static MessageBuilder defaultBuilder(ConfigHandler config) {
return new MessageBuilder(config);
}
public static MessageBuilder fromComponentFactory(ConfigHandler config, ComponentFactory factory) {
return new MessageBuilder(config, 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;
}
public TextComponent reloadedConfig() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content("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 TextComponent waitAMoment(boolean longWait) {
String msg = longWait ? "Calculating statistics, this may take a minute..." :
"Calculating statistics, this may take a few moments...";
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(msg));
}
public TextComponent missingStatName() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please provide a valid statistic name!"));
}
public TextComponent missingSubStatName(Statistic.Type statType) {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please add a valid " + getSubStatTypeName(statType) + " to look up this statistic!"));
}
public TextComponent missingPlayerName() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please specify a valid player-name!"));
}
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatName) {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.messageAccent().content("\"" + subStatName + "\""))
.append(space())
.append(componentFactory.message().content(
"is not a valid " + getSubStatTypeName(statType) + "!"));
}
public TextComponent requestAlreadyRunning() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please wait for your previous lookup to finish!"));
}
//TODO Make this say amount of time left
public TextComponent stillOnShareCoolDown() {
int waitTime = config.getStatShareWaitingTime();
String minutes = waitTime == 1 ? " minute" : " minutes";
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content("You need to wait")
.append(space())
.append(componentFactory.messageAccent()
.content(waitTime + minutes))
.append(space())
.append(text("between sharing!")));
}
public TextComponent resultsAlreadyShared() {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content("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 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 TextComponent usageExamples() {
return ExampleMessage.construct(componentFactory);
}
public TextComponent helpMsg() {
int listSize = config.getTopListMaxSize();
if (!isConsoleBuilder && useHoverText) {
return HelpMessage.constructHoverMsg(componentFactory, listSize);
} else {
return HelpMessage.constructPlainMsg(componentFactory, listSize);
}
}
/** Returns a BiFunction for a player statistic. This BiFunction will return a formattedValue,
the shape of which is determined by the 2 parameters the BiFunction gets.
<p>- Integer shareCode: if a shareCode is provided, a clickable "share" button will be added.
<br>- CommandSender sender: if a sender is provided, a signature with "shared by sender-name" will be added.</br>
<br>- If both parameters are null, the formattedValue will be returned as is.</br>*/
public BiFunction<Integer, CommandSender, TextComponent> formattedPlayerStatFunction(int stat, @NotNull StatRequest statRequest) {
TextComponent playerStat = Component.text()
.append(componentFactory.playerName(statRequest.getPlayerName(), Target.PLAYER)
.append(text(":"))
.append(space()))
.append(getStatNumberComponent(statRequest, stat))
.append(space())
.append(getStatNameComponent(statRequest))
.append(getStatUnitComponent(statRequest.getStatistic(), statRequest.getTarget())) //space is provided by statUnitComponent
.build();
return getFormattingFunction(playerStat, Target.PLAYER);
}
/** Returns a BiFunction for a server statistic. This BiFunction will return a formattedValue,
the shape of which is determined by the 2 parameters the BiFunction gets.
<p>- Integer shareCode: if a shareCode is provided, a clickable "share" button will be added.
<br>- CommandSender sender: if a sender is provided, a signature with "shared by sender-name" will be added.</br>
<br>- If both parameters are null, the formattedValue will be returned as is.</br>*/
public BiFunction<Integer, CommandSender, TextComponent> formattedServerStatFunction(long stat, @NotNull StatRequest statRequest) {
TextComponent serverStat = text()
.append(componentFactory.title(config.getServerTitle(), Target.SERVER))
.append(space())
.append(componentFactory.serverName(config.getServerName()))
.append(space())
.append(getStatNumberComponent(statRequest, stat))
.append(space())
.append(getStatNameComponent(statRequest))
.append(getStatUnitComponent(statRequest.getStatistic(), statRequest.getTarget())) //space is provided by statUnit
.build();
return getFormattingFunction(serverStat, Target.SERVER);
}
/** Returns a BiFunction for a top statistic. This BiFunction will return a formattedValue,
the shape of which is determined by the 2 parameters the BiFunction gets.
<p>- Integer shareCode: if a shareCode is provided, a clickable "share" button will be added.
<br>- CommandSender sender: if a sender is provided, a signature with "shared by sender-name" will be added.</br>
<br>- If both parameters are null, the formattedValue will be returned as is.</br>*/
public BiFunction<Integer, CommandSender, TextComponent> formattedTopStatFunction(@NotNull LinkedHashMap<String, Integer> topStats, @NotNull StatRequest statRequest) {
final TextComponent title = getTopStatsTitleComponent(statRequest, topStats.size());
final TextComponent shortTitle = getTopStatDescription(statRequest, topStats.size());
final TextComponent list = getTopStatListComponent(topStats, statRequest);
final boolean useEnters = config.useEnters(Target.TOP, false);
final boolean useEntersForShared = config.useEnters(Target.TOP, true);
return (shareCode, sender) -> {
TextComponent.Builder topBuilder = text();
//if we're adding a share-button
if (shareCode != null) {
if (useEnters) {
topBuilder.append(newline());
}
topBuilder.append(title)
.append(space())
.append(componentFactory.shareButton(shareCode))
.append(list);
}
//if we're adding a "shared by" component
else if (sender != null) {
if (useEntersForShared) {
topBuilder.append(newline());
}
topBuilder.append(shortTitle)
.append(space())
.append(componentFactory.statResultInHoverText(text()
.append(title)
.append(list)
.build()))
.append(newline())
.append(componentFactory.sharedByMessage(
getSharerNameComponent(sender)));
}
//if we're not adding a share-button or a "shared by" component
else {
if (useEnters) {
topBuilder.append(newline());
}
topBuilder.append(title)
.append(list);
}
return topBuilder.build();
};
}
public TextComponent singleTopStatLine(int positionInTopList, String playerName, long statNumber, Statistic statistic) {
TextComponent.Builder topStatLineBuilder = Component.text()
.append(space())
.append(componentFactory.rankNumber(positionInTopList))
.append(space());
if (config.useDots()) {
topStatLineBuilder.append(getPlayerNameWithDotsComponent(positionInTopList, playerName));
} else {
topStatLineBuilder.append(componentFactory.playerName(playerName + ":", Target.TOP));
}
return topStatLineBuilder
.append(space())
.append(getStatNumberComponent(statistic, Target.TOP, statNumber))
.build();
}
private Component getSharerNameComponent(CommandSender sender) {
if (sender instanceof Player player) {
Component senderName = EasterEggProvider.getPlayerName(player);
if (senderName != null) {
return senderName;
}
}
return componentFactory.sharerName(sender.getName());
}
private TextComponent getTopStatsTitleComponent(StatRequest statRequest, int statListSize) {
return Component.text()
.append(componentFactory.pluginPrefix()).append(space())
.append(componentFactory.title(config.getTopStatsTitle(), Target.TOP)).append(space())
.append(componentFactory.titleNumber(statListSize)).append(space())
.append(getStatNameComponent(statRequest)) //space is provided by statUnitComponent
.append(getStatUnitComponent(statRequest.getStatistic(), statRequest.getTarget()))
.build();
}
private TextComponent getTopStatDescription(StatRequest statRequest, int statListSize) {
return Component.text()
.append(componentFactory.title(config.getTopStatsTitle(), Target.TOP)).append(space())
.append(componentFactory.titleNumber(statListSize)).append(space())
.append(getStatNameComponent(statRequest)) //space is provided by statUnitComponent
.build();
}
private TextComponent getTopStatListComponent(LinkedHashMap<String, Integer> topStats, StatRequest statRequest) {
TextComponent.Builder topList = Component.text();
Set<String> playerNames = topStats.keySet();
boolean useDots = config.useDots();
int count = 0;
for (String playerName : playerNames) {
topList.append(newline())
.append(space())
.append(componentFactory.rankNumber(++count))
.append(space());
if (useDots) {
topList.append(getPlayerNameWithDotsComponent(count, playerName));
}
else {
topList.append(componentFactory.playerName(playerName + ":", Target.TOP));
}
topList.append(space()).append(getStatNumberComponent(statRequest, topStats.get(playerName)));
}
return topList.build();
}
private TextComponent getPlayerNameWithDotsComponent(int positionInTopList, String playerName) {
int dots = FontUtils.getNumberOfDotsToAlign(positionInTopList + ". " + playerName, isConsoleBuilder, config.playerNameIsBold());
TextComponent.Builder nameWithDots = Component.text()
.append(componentFactory.playerName(playerName, Target.TOP))
.append(space());
if (dots >= 1) {
nameWithDots.append(componentFactory.dots().append(text(".".repeat(dots))));
}
return nameWithDots.build();
}
/** Depending on the config settings, return either a TranslatableComponent representing
the statName (and potential subStatName), or a TextComponent with capitalized English names.*/
private TextComponent getStatNameComponent(StatRequest statRequest) {
if (config.useTranslatableComponents()) {
String statKey = languageKeyHandler.getStatKey(statRequest.getStatistic());
String subStatKey = statRequest.getSubStatEntryName();
if (subStatKey != null) {
switch (statRequest.getStatistic().getType()) {
case BLOCK -> subStatKey = languageKeyHandler.getBlockKey(statRequest.getBlock());
case ENTITY -> subStatKey = languageKeyHandler.getEntityKey(statRequest.getEntity());
case ITEM -> subStatKey = languageKeyHandler.getItemKey(statRequest.getItem());
default -> {
}
}
}
return componentFactory.statAndSubStatNameTranslatable(statKey, subStatKey, statRequest.getTarget());
}
else {
return componentFactory.statAndSubStatName(
StringUtils.prettify(statRequest.getStatistic().toString()),
StringUtils.prettify(statRequest.getSubStatEntryName()),
statRequest.getTarget());
}
}
private TextComponent getStatNumberComponent(StatRequest request, long statNumber) {
return getStatNumberComponent(request.getStatistic(), request.getTarget(), statNumber);
}
private TextComponent getStatNumberComponent(Statistic statistic, Target target, long statNumber) {
Unit.Type statUnitType = Unit.getTypeFromStatistic(statistic);
return switch (statUnitType) {
case DISTANCE -> getDistanceNumberComponent(statNumber, target);
case DAMAGE -> getDamageNumberComponent(statNumber, target);
case TIME -> getTimeNumberComponent(statNumber, target);
default -> getDefaultNumberComponent(statNumber, target);
};
}
private TextComponent getDistanceNumberComponent(long statNumber, Target target) {
Unit statUnit = Unit.fromString(config.getDistanceUnit(false));
String prettyNumber = formatter.formatDistanceNumber(statNumber, statUnit);
if (!useHoverText) {
return componentFactory.distanceNumber(prettyNumber, target);
}
Unit hoverUnit = Unit.fromString(config.getDistanceUnit(true));
String hoverNumber = formatter.formatDistanceNumber(statNumber, hoverUnit);
if (config.useTranslatableComponents()) {
String unitKey = languageKeyHandler.getUnitKey(hoverUnit);
if (unitKey != null) {
return componentFactory.distanceNumberWithTranslatableHoverText(prettyNumber, hoverNumber, unitKey, target);
}
}
return componentFactory.distanceNumberWithHoverText(prettyNumber, hoverNumber, hoverUnit.getLabel(), target);
}
private TextComponent getDamageNumberComponent(long statNumber, Target target) {
Unit statUnit = Unit.fromString(config.getDamageUnit(false));
String prettyNumber = formatter.formatDamageNumber(statNumber, statUnit);
if (!useHoverText) {
return componentFactory.damageNumber(prettyNumber, target);
}
Unit hoverUnit = Unit.fromString(config.getDamageUnit(true));
String prettyHoverNumber = formatter.formatDamageNumber(statNumber, hoverUnit);
if (hoverUnit == Unit.HEART) {
return componentFactory.damageNumberWithHeartUnitInHoverText(prettyNumber, prettyHoverNumber, target);
}
return componentFactory.damageNumberWithHoverText(prettyNumber, prettyHoverNumber, hoverUnit.getLabel(), target);
}
private TextComponent getTimeNumberComponent(long statNumber, Target target) {
ArrayList<Unit> unitRange = getTimeUnitRange(statNumber);
if (unitRange.size() <= 1 || (useHoverText && unitRange.size() <= 3)) {
MyLogger.logMsg(
"There is something wrong with the time-units you specified, please check your config!",
true);
return componentFactory.statNumber("-", target);
}
else {
String mainNumber = formatter.formatTimeNumber(statNumber, unitRange.get(0), unitRange.get(1));
if (!useHoverText) {
return componentFactory.statNumber(mainNumber, target);
} else {
String hoverNumber = formatter.formatTimeNumber(statNumber, unitRange.get(2), unitRange.get(3));
MyLogger.logMsg("mainNumber: " + mainNumber + ", hoverNumber: " + hoverNumber, DebugLevel.HIGH);
return componentFactory.statNumberWithHoverText(mainNumber, hoverNumber,
null, null, target);
}
}
}
private TextComponent getDefaultNumberComponent(long statNumber, Target target) {
return componentFactory.statNumber(formatter.formatNumber(statNumber), target);
}
private TextComponent getStatUnitComponent(Statistic statistic, Target target) {
return switch (Unit.getTypeFromStatistic(statistic)) {
case DAMAGE -> getDamageUnit(target);
case DISTANCE -> getDistanceUnit(target);
default -> Component.empty();
};
}
private TextComponent getDistanceUnit(Target target) {
Unit statUnit = Unit.fromString(config.getDistanceUnit(false));
if (config.useTranslatableComponents()) {
String unitKey = languageKeyHandler.getUnitKey(statUnit);
if (unitKey != null) {
return Component.space()
.append(componentFactory.statUnitTranslatable(unitKey, target));
}
}
return Component.space()
.append(componentFactory.statUnit(statUnit.getLabel(), target));
}
private TextComponent getDamageUnit(Target target) {
Unit statUnit = Unit.fromString(config.getDamageUnit(false));
if (statUnit == 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);
}
return Component.space()
.append(componentFactory.statUnit(statUnit.getLabel(), target));
}
private BiFunction<Integer, CommandSender, TextComponent> getFormattingFunction(@NotNull TextComponent statResult, Target target) {
boolean useEnters = config.useEnters(target, false);
boolean useEntersForShared = config.useEnters(target, true);
return (shareCode, sender) -> {
TextComponent.Builder statBuilder = text();
//if we're adding a share-button
if (shareCode != null) {
if (useEnters) {
statBuilder.append(newline());
}
statBuilder.append(statResult)
.append(space())
.append(componentFactory.shareButton(shareCode));
}
//if we're adding a "shared by" component
else if (sender != null) {
if (useEntersForShared) {
statBuilder.append(newline());
}
statBuilder.append(statResult)
.append(newline())
.append(componentFactory.sharedByMessage(
getSharerNameComponent(sender)));
}
//if we're not adding a share-button or a "shared by" component
else {
if (useEnters) {
statBuilder.append(newline());
}
statBuilder.append(statResult);
}
return statBuilder.build();
};
}
/** Get an ArrayList consisting of 2 or 4 timeUnits. The order of items is:
<p>0. maxUnit</p>
<p>1. minUnit</p>
<p>2. maxHoverUnit</p>
<p>3. minHoverUnit</p>*/
private ArrayList<Unit> getTimeUnitRange(long statNumber) {
ArrayList<Unit> unitRange = new ArrayList<>();
if (!config.autoDetectTimeUnit(false)) {
unitRange.add(Unit.fromString(config.getTimeUnit(false)));
unitRange.add(Unit.fromString(config.getTimeUnit(false, true)));
}
else {
Unit bigUnit = Unit.getMostSuitableUnit(Unit.Type.TIME, statNumber);
unitRange.add(bigUnit);
unitRange.add(bigUnit.getSmallerUnit(config.getNumberOfExtraTimeUnits(false)));
}
if (useHoverText) {
if (!config.autoDetectTimeUnit(true)) {
unitRange.add(Unit.fromString(config.getTimeUnit(true)));
unitRange.add(Unit.fromString(config.getTimeUnit(true, true)));
}
else {
Unit bigHoverUnit = Unit.getMostSuitableUnit(Unit.Type.TIME, statNumber);
unitRange.add(bigHoverUnit);
unitRange.add(bigHoverUnit.getSmallerUnit(config.getNumberOfExtraTimeUnits(true)));
}
}
MyLogger.logMsg("total selected unitRange for this statistic: " + unitRange, DebugLevel.MEDIUM);
return unitRange;
}
/** Returns "block", "entity", "item", or "sub-statistic" if the provided Type is null. */
public static String getSubStatTypeName(Statistic.Type statType) {
String subStat = "sub-statistic";
if (statType == null) return subStat;
switch (statType) {
case BLOCK -> subStat = "block";
case ENTITY -> subStat = "entity";
case ITEM -> subStat = "item";
}
return subStat;
}
}

View File

@ -1,20 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
import org.bukkit.map.MinecraftFont;
/** A small utility class that helps calculate how many dots to use to get the numbers of a top-statistic aligned. */
public final class FontUtils {
private FontUtils() {
}
public static int getNumberOfDotsToAlign(String displayText, boolean isConsoleSender, boolean fontIsBold) {
if (isConsoleSender) {
return (int) Math.round((130.0 - MinecraftFont.Font.getWidth(displayText))/6) + 7;
} else if (!fontIsBold) {
return (int) Math.round((130.0 - MinecraftFont.Font.getWidth(displayText))/2);
} else {
return (int) Math.round((130.0 - (MinecraftFont.Font.getWidth(displayText) * 1.5))/2);
}
}
}

View File

@ -1,88 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
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 StatRetriever {
private final OfflinePlayerHandler offlinePlayerHandler;
public StatRetriever(OfflinePlayerHandler offlinePlayerHandler) {
this.offlinePlayerHandler = offlinePlayerHandler;
}
public int getPlayerStat(StatRequest statRequest) {
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(statRequest.getPlayerName());
if (player != null) {
return switch (statRequest.getStatistic().getType()) {
case UNTYPED -> player.getStatistic(statRequest.getStatistic());
case ENTITY -> player.getStatistic(statRequest.getStatistic(), statRequest.getEntity());
case BLOCK -> player.getStatistic(statRequest.getStatistic(), statRequest.getBlock());
case ITEM -> player.getStatistic(statRequest.getStatistic(), statRequest.getItem());
};
}
return 0;
}
public LinkedHashMap<String, Integer> getTopStats(StatRequest statRequest) {
return getAllStatsAsync(statRequest).entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(statRequest.getTopListSize())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}
public long getServerStat(StatRequest statRequest) {
List<Integer> numbers = getAllStatsAsync(statRequest)
.values()
.parallelStream()
.toList();
return numbers.parallelStream().mapToLong(Integer::longValue).sum();
}
/** Invokes a bunch of worker pool threads to divide and conquer (get the statistics for all players
that are stored in the {@link OfflinePlayerHandler}) */
private @NotNull ConcurrentHashMap<String, Integer> getAllStatsAsync(StatRequest statRequest) {
long time = System.currentTimeMillis();
ForkJoinPool commonPool = ForkJoinPool.commonPool();
ConcurrentHashMap<String, Integer> allStats;
try {
allStats = commonPool.invoke(getStatTask(statRequest));
} catch (ConcurrentModificationException e) {
MyLogger.logMsg("The statRequest 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!", true);
throw new ConcurrentModificationException(e.toString());
}
MyLogger.actionFinished(2);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
MyLogger.logTimeTaken("StatThread", "calculated all stats", time, DebugLevel.MEDIUM);
return allStats;
}
private StatAction getStatTask(StatRequest statRequest) {
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, statRequest, allStats);
MyLogger.actionCreated(playerNames.size());
return task;
}
}

View File

@ -1,82 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentUtils;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
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 StatRetriever statRetriever;
private final ReloadThread reloadThread;
private final StatRequest statRequest;
public StatThread(OutputManager m, StatRetriever t, int ID, StatRequest s, @Nullable ReloadThread r) {
outputManager = m;
statRetriever = t;
reloadThread = r;
statRequest = s;
this.setName("StatThread-" + statRequest.getCommandSender().getName() + "-" + ID);
MyLogger.threadCreated(this.getName());
}
@Override
public void run() throws IllegalStateException, NullPointerException {
MyLogger.threadStart(this.getName());
if (statRequest == null) {
throw new NullPointerException("No statistic statRequest was found!");
}
if (reloadThread != null && reloadThread.isAlive()) {
try {
MyLogger.waitingForOtherThread(this.getName(), reloadThread.getName());
outputManager.sendFeedbackMsg(statRequest.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(statRequest.getCommandSender(), lastCalc > 20000);
}
Target selection = statRequest.getTarget();
try {
TextComponent statResult = switch (selection) {
case PLAYER -> outputManager.formatPlayerStat(statRequest, statRetriever.getPlayerStat(statRequest));
case TOP -> outputManager.formatTopStat(statRequest, statRetriever.getTopStats(statRequest));
case SERVER -> outputManager.formatServerStat(statRequest, statRetriever.getServerStat(statRequest));
};
if (statRequest.isAPIRequest()) {
String msg = ComponentUtils.getTranslatableComponentSerializer()
.serialize(statResult);
statRequest.getCommandSender().sendMessage(msg);
}
else {
outputManager.sendToCommandSender(statRequest.getCommandSender(), statResult);
}
}
catch (ConcurrentModificationException e) {
if (!statRequest.isConsoleSender()) {
outputManager.sendFeedbackMsg(statRequest.getCommandSender(), StandardMessage.UNKNOWN_ERROR);
}
}
}
}

View File

@ -1,54 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.request;
import com.gmail.artemis.the.gr8.playerstats.api.RequestGenerator;
import com.gmail.artemis.the.gr8.playerstats.api.RequestExecutor;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.PlayerStatResult;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.StatResult;
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 implements RequestGenerator<Integer>, RequestExecutor<Integer> {
private final StatRequest statRequest;
private final StatRequestHandler statRequestHandler;
public PlayerStatRequest(StatRequest request) {
statRequest = request;
statRequestHandler = new StatRequestHandler(request);
}
@Override
public RequestExecutor<Integer> untyped(@NotNull Statistic statistic) {
StatRequest completedRequest = statRequestHandler.untyped(statistic);
return new PlayerStatRequest(completedRequest);
}
@Override
public RequestExecutor<Integer> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
StatRequest completedRequest = statRequestHandler.blockOrItemType(statistic, material);
return new PlayerStatRequest(completedRequest);
}
@Override
public RequestExecutor<Integer> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
StatRequest completedRequest = statRequestHandler.entityType(statistic, entityType);
return new PlayerStatRequest(completedRequest);
}
@Override
public StatResult<Integer> execute() {
return getStatResult(statRequest);
}
private PlayerStatResult getStatResult(StatRequest completedRequest) {
int stat = RequestExecutor.getStatCalculator()
.getPlayerStat(completedRequest);
TextComponent prettyStat = RequestExecutor.getStatFormatter()
.formatPlayerStat(completedRequest, stat);
return new PlayerStatResult(stat, prettyStat);
}
}

View File

@ -1,54 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.request;
import com.gmail.artemis.the.gr8.playerstats.api.RequestExecutor;
import com.gmail.artemis.the.gr8.playerstats.api.RequestGenerator;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.ServerStatResult;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.StatResult;
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 implements RequestGenerator<Long>, RequestExecutor<Long> {
private final StatRequest statRequest;
private final StatRequestHandler statRequestHandler;
public ServerStatRequest(StatRequest request) {
statRequest = request;
statRequestHandler = new StatRequestHandler(statRequest);
}
@Override
public RequestExecutor<Long> untyped(@NotNull Statistic statistic) {
StatRequest completedRequest = statRequestHandler.untyped(statistic);
return new ServerStatRequest(completedRequest);
}
@Override
public RequestExecutor<Long> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
StatRequest completedRequest = statRequestHandler.blockOrItemType(statistic, material);
return new ServerStatRequest(completedRequest);
}
@Override
public RequestExecutor<Long> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
StatRequest completedRequest = statRequestHandler.entityType(statistic, entityType);
return new ServerStatRequest(completedRequest);
}
@Override
public StatResult<Long> execute() {
return getStatResult(statRequest);
}
private ServerStatResult getStatResult(StatRequest completedRequest) {
long stat = RequestExecutor.getStatCalculator()
.getServerStat(completedRequest);
TextComponent prettyStat = RequestExecutor.getStatFormatter()
.formatServerStat(completedRequest, stat);
return new ServerStatResult(stat, prettyStat);
}
}

View File

@ -1,174 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.request;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.api.RequestGenerator;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.Material;
import org.bukkit.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 StatRequestHandler {
private final StatRequest statRequest;
public StatRequestHandler(StatRequest request) {
statRequest = request;
}
public static StatRequest getBasicPlayerStatRequest(String playerName) {
StatRequest request = StatRequest.getBasicAPIRequest();
request.setTarget(Target.PLAYER);
request.setPlayerName(playerName);
return request;
}
public static StatRequest getBasicServerStatRequest() {
StatRequest request = StatRequest.getBasicAPIRequest();
request.setTarget(Target.SERVER);
return request;
}
public static StatRequest getBasicTopStatRequest(int topListSize) {
StatRequest request = StatRequest.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 StatRequest getBasicInternalStatRequest(CommandSender sender) {
StatRequest request = StatRequest.getBasicRequest(sender);
request.setTopListSize(Main.getConfigHandler().getTopListMaxSize());
return request;
}
public StatRequest untyped(@NotNull Statistic statistic) throws IllegalArgumentException {
if (statistic.getType() == Statistic.Type.UNTYPED) {
statRequest.setStatistic(statistic);
return statRequest;
}
throw new IllegalArgumentException("This statistic is not of Type.Untyped");
}
public StatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException {
Statistic.Type type = statistic.getType();
if (type == Statistic.Type.BLOCK && material.isBlock()) {
statRequest.setBlock(material);
}
else if (type == Statistic.Type.ITEM && material.isItem()){
statRequest.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");
}
statRequest.setSubStatEntryName(material.toString());
return statRequest;
}
public StatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException {
if (statistic.getType() == Statistic.Type.ENTITY) {
statRequest.setSubStatEntryName(entityType.toString());
statRequest.setEntity(entityType);
return statRequest;
}
throw new IllegalArgumentException("This statistic is not of Type.Entity");
}
/**
This will create a {@link StatRequest} from the provided args, with the requesting Player (or Console)
as CommandSender. This CommandSender will receive feedback messages if the StatRequest could not be created.
@param args an Array of args such as a CommandSender would put in Minecraft chat:
<p>- a <code>statName</code> (example: "mine_block")</p>
<p>- if applicable, a <code>subStatEntryName</code> (example: diorite)(</p>
<p>- a <code>target</code> for this lookup: can be "top", "server", "player" (or "me" to indicate the current CommandSender)</p>
<p>- if "player" was chosen, include a <code>playerName</code></p>
@return the generated StatRequest
*/
public StatRequest getRequestFromArgs(String[] args) {
EnumHandler enumHandler = Main.getEnumHandler();
OfflinePlayerHandler offlinePlayerHandler = Main.getOfflinePlayerHandler();
CommandSender sender = statRequest.getCommandSender();
for (String arg : args) {
//check for statName
if (enumHandler.isStatistic(arg) && statRequest.getStatistic() == null) {
statRequest.setStatistic(EnumHandler.getStatEnum(arg));
}
//check for subStatEntry and playerFlag
else if (enumHandler.isSubStatEntry(arg)) {
if (arg.equalsIgnoreCase("player") && !statRequest.getPlayerFlag()) {
statRequest.setPlayerFlag(true);
} else {
if (statRequest.getSubStatEntryName() == null) statRequest.setSubStatEntryName(arg);
}
}
//check for selection
else if (arg.equalsIgnoreCase("top")) {
statRequest.setTarget(Target.TOP);
} else if (arg.equalsIgnoreCase("server")) {
statRequest.setTarget(Target.SERVER);
} else if (arg.equalsIgnoreCase("me")) {
if (sender instanceof Player) {
statRequest.setPlayerName(sender.getName());
statRequest.setTarget(Target.PLAYER);
} else if (sender instanceof ConsoleCommandSender) {
statRequest.setTarget(Target.SERVER);
}
} else if (offlinePlayerHandler.isRelevantPlayer(arg) && statRequest.getPlayerName() == null) {
statRequest.setPlayerName(arg);
statRequest.setTarget(Target.PLAYER);
} else if (arg.equalsIgnoreCase("api")) {
statRequest.setAPIRequest();
}
}
patchRequest(statRequest);
return statRequest;
}
/**
Adjust the StatRequest 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(StatRequest statRequest) {
if (statRequest.getStatistic() != null) {
Statistic.Type type = statRequest.getStatistic().getType();
if (statRequest.getPlayerFlag()) { //unpack the playerFlag
if (type == Statistic.Type.ENTITY && statRequest.getSubStatEntryName() == null) {
statRequest.setSubStatEntryName("player");
} else {
statRequest.setTarget(Target.PLAYER);
}
}
String subStatEntry = statRequest.getSubStatEntryName();
switch (type) { //attempt to convert relevant subStatEntries into their corresponding Enum Constant
case BLOCK -> {
Material block = EnumHandler.getBlockEnum(subStatEntry);
if (block != null) statRequest.setBlock(block);
}
case ENTITY -> {
EntityType entity = EnumHandler.getEntityEnum(subStatEntry);
if (entity != null) statRequest.setEntity(entity);
}
case ITEM -> {
Material item = EnumHandler.getItemEnum(subStatEntry);
if (item != null) statRequest.setItem(item);
}
case UNTYPED -> { //remove unnecessary subStatEntries
if (subStatEntry != null) statRequest.setSubStatEntryName(null);
}
}
}
}
}

View File

@ -1,56 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.request;
import com.gmail.artemis.the.gr8.playerstats.api.RequestExecutor;
import com.gmail.artemis.the.gr8.playerstats.api.RequestGenerator;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.StatResult;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.TopStatResult;
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 implements RequestGenerator<LinkedHashMap<String, Integer>>, RequestExecutor<LinkedHashMap<String, Integer>> {
private final StatRequest statRequest;
private final StatRequestHandler statRequestHandler;
public TopStatRequest(StatRequest request) {
statRequest = request;
statRequestHandler = new StatRequestHandler(request);
}
@Override
public TopStatRequest untyped(@NotNull Statistic statistic) {
StatRequest completedRequest = statRequestHandler.untyped(statistic);
return new TopStatRequest(completedRequest);
}
@Override
public TopStatRequest blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
StatRequest completedRequest = statRequestHandler.blockOrItemType(statistic, material);
return new TopStatRequest(completedRequest);
}
@Override
public TopStatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
StatRequest completedRequest = statRequestHandler.entityType(statistic, entityType);
return new TopStatRequest(completedRequest);
}
@Override
public StatResult<LinkedHashMap<String, Integer>> execute() {
return getStatResult(statRequest);
}
private TopStatResult getStatResult(StatRequest completedRequest) {
LinkedHashMap<String, Integer> stat = RequestExecutor.getStatCalculator()
.getTopStats(completedRequest);
TextComponent prettyStat = RequestExecutor.getStatFormatter()
.formatTopStat(completedRequest, stat);
return new TopStatResult(stat, prettyStat);
}
}

View File

@ -1,30 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.result;
import com.gmail.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 toString() {
return ComponentUtils.getTranslatableComponentSerializer()
.serialize(formattedValue);
}
}

View File

@ -1,23 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.result;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentUtils;
import net.kyori.adventure.text.TextComponent;
public record PlayerStatResult(int value, TextComponent formattedValue) implements StatResult<Integer> {
@Override
public Integer getNumericalValue() {
return value;
}
@Override
public TextComponent getFormattedTextComponent() {
return formattedValue;
}
@Override
public String toString() {
return ComponentUtils.getTranslatableComponentSerializer()
.serialize(formattedValue);
}
}

View File

@ -1,23 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.result;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentUtils;
import net.kyori.adventure.text.TextComponent;
public record ServerStatResult(long value, TextComponent formattedValue) implements StatResult<Long> {
@Override
public Long getNumericalValue() {
return value;
}
@Override
public TextComponent getFormattedTextComponent() {
return formattedValue;
}
@Override
public String toString() {
return ComponentUtils.getTranslatableComponentSerializer()
.serialize(formattedValue);
}
}

View File

@ -1,63 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.result;
import com.gmail.artemis.the.gr8.playerstats.api.Formatter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.TextComponent;
/** Holds the result of a completed stat-lookup. The <code>Type</code> parameter
<code>T</code> of this StatResult represents the data type of the stored number:
<ul>
<li> <code>Integer</code> for playerStat
<li> <code>Long</code> for serverStat
<li> <code>LinkedHashMap(String, Integer)</code> for topStat
</ul>
You can get these raw numbers with {@link #getNumericalValue()}. Additionally,
you can get a formatted message that contains the following information:
<ul>
<li> for playerStat:
<br> [player-name]: [formatted-number] [stat-name] [sub-stat-name]
<li> for serverStat:
<br> [Total on] [server-name]: [formatted-number] [stat-name] [sub-stat-name]
<li> for topStat:
<br> [PlayerStats] [Top x] [stat-name] [sub-stat-name]
<br> [1.] [player-name] [.....] [formatted-number]
<br> [2.] [player-name] [.....] [formatted-number]
<br> [3.] etc...
</ul>
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 directly.
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 #toString()} method to get the same information
in String-format. Don't use Adventure's toString methods on the Components
- those are for debugging purposes. And finally, if you want the results to be
formatted differently, you can get an instance of the {@link Formatter}.
*/
public interface StatResult<T> {
/** 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*/
T getNumericalValue();
/** Gets the formatted message for the completed stat-lookup this {@link 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. */
TextComponent getFormattedTextComponent();
/** Turns the formatted message for the completed stat-lookup into String.
@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.*/
String toString();
}

View File

@ -1,25 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.statistic.result;
import com.gmail.artemis.the.gr8.playerstats.msg.components.ComponentUtils;
import net.kyori.adventure.text.TextComponent;
import java.util.LinkedHashMap;
public record TopStatResult(LinkedHashMap<String, Integer> value, TextComponent formattedValue) implements StatResult<LinkedHashMap<String,Integer>> {
@Override
public LinkedHashMap<String, Integer> getNumericalValue() {
return value;
}
@Override
public TextComponent getFormattedTextComponent() {
return formattedValue;
}
@Override
public String toString() {
return ComponentUtils.getTranslatableComponentSerializer()
.serialize(formattedValue);
}
}

View File

@ -1,240 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
/** The PlayerStats Logger*/
public final class MyLogger {
private static final Logger logger;
private static DebugLevel debugLevel;
private static final String[] processedPlayers;
private static final AtomicInteger playersIndex;
private static ConcurrentHashMap<String, Integer> threadNames;
static {
Plugin plugin = Bukkit.getPluginManager().getPlugin("PlayerStats");
logger = (plugin != null) ? plugin.getLogger() : Bukkit.getLogger();
debugLevel = DebugLevel.LOW;
processedPlayers = new String[10];
playersIndex = new AtomicInteger(0);
threadNames = new ConcurrentHashMap<>();
}
private MyLogger() {
}
/** Sets the desired debugging level.
<br>1 = low (only show unexpected errors)</br>
<br>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</br>
<br>3 = high (log all tasks and time taken)</br>
<br>Default: 1</br>*/
public static void setDebugLevel(int level) {
if (level == 2) {
debugLevel = DebugLevel.MEDIUM;
}
else if (level == 3) {
debugLevel = DebugLevel.HIGH;
}
else {
debugLevel = DebugLevel.LOW;
}
}
public static void logMsg(String content) {
logMsg(content, DebugLevel.LOW, false);
}
public static void logMsg(String content, boolean logAsWarning) {
logMsg(content, DebugLevel.LOW, logAsWarning);
}
public static void logMsg(String content, DebugLevel logThreshold) {
logMsg(content, logThreshold, false);
}
public static void logMsg(String content, DebugLevel logThreshold, boolean logAsWarning) {
switch (logThreshold) {
case LOW -> log(content, logAsWarning);
case MEDIUM -> {
if (debugLevel != DebugLevel.LOW) {
log(content, logAsWarning);
}
}
case HIGH -> {
if (debugLevel == DebugLevel.HIGH) {
log(content, logAsWarning);
}
}
}
}
public static void logException(@NotNull Exception exception, String caughtBy) {
logException(exception, caughtBy, null);
}
/** Log the encountered exception as a warning to console,
with some information about which class/method caught it
and with a printStackTrace if DebugLevel is HIGH.
@param exception The encountered exception
@param caughtBy The name of the class that caught the exception
@param additionalInfo e.g. the method-name or line where the exception is caught */
public static void logException(@NotNull Exception exception, String caughtBy, @Nullable String additionalInfo) {
String extraInfo = (additionalInfo != null) ? " [" + additionalInfo + "]" : "";
String info = " (" + caughtBy + extraInfo + ")";
logger.warning(exception + info);
if (debugLevel == DebugLevel.HIGH) {
exception.printStackTrace();
}
}
/** If DebugLevel is MEDIUM or HIGH, logs when the while loop in MessageBuilder, getLanguageKey is being run. */
public static void replacingUnderscores() {
if (debugLevel != DebugLevel.LOW) {
logger.info("Replacing underscores and capitalizing names...");
}
}
/** Output to console that the given thread has been created (but not started yet).*/
public static void threadCreated(String threadName) {
if (debugLevel == DebugLevel.HIGH) {
logger.info(threadName + " created!");
}
}
/** Output to console that the given thread has been started. */
public static void threadStart(String threadName) {
if (debugLevel == DebugLevel.HIGH) {
logger.info(threadName + " started!");
}
}
/** Output to console that another reloadThread is already running. */
public static void threadAlreadyRunning(String threadName) {
logger.info("Another reloadThread is already running! (" + threadName + ")");
}
/** Output to console that the executingThread is waiting for otherThread to finish up. */
public static void waitingForOtherThread(String executingThread, String otherThread) {
logger.info(executingThread + ": Waiting for " + otherThread + " to finish up...");
}
/** If DebugLevel is MEDIUM or HIGH, output to console that an action has started.
@param taskLength Length of the action (in terms of units-to-process)*/
public static void actionCreated(int taskLength) {
if (debugLevel != DebugLevel.LOW) {
threadNames = new ConcurrentHashMap<>();
playersIndex.set(0);
logger.info("Initial Action created for " + taskLength + " Players. Processing...");
}
}
/** Internally save the name of the executing thread for later logging of this action.
The list of names is reset upon the start of every new action.
@param threadName Name of the executing thread*/
public static void subActionCreated(String threadName) {
if (debugLevel == DebugLevel.HIGH) {
if (!threadNames.containsKey(threadName)) {
threadNames.put(threadName, threadNames.size());
}
}
}
/** Internally save the name of the executing thread and processed player for logging,
and for the ReloadThread, if DebugLevel is HIGH, output the last 10 processed players once
there have been 10 names saved in MyLogger. This method is synchronized.
@param threadName Name of the executing thread
@param playerName Name of the player that was processed in this action
@param thread 1 for ReloadThread, 2 for StatThread */
public static synchronized void actionRunning(String threadName, String playerName, int thread) {
if (debugLevel != DebugLevel.LOW) {
if (!threadNames.containsKey(threadName)) {
threadNames.put(threadName, threadNames.size());
}
if (thread == 1 && debugLevel == DebugLevel.HIGH) {
if (incrementOfTen()) {
logger.info(Arrays.asList(processedPlayers).toString());
}
processedPlayers[nextPlayersIndex() % 10] = playerName;
}
else if (debugLevel == DebugLevel.MEDIUM || debugLevel == DebugLevel.HIGH && thread == 2) {
nextPlayersIndex();
}
}
}
/** Output to console that an action has finished.
<p>For the ReloadThread, if DebugLevel is HIGH, output the left-over processed players.
For both threads, if DebugLevel is MEDIUM or HIGH, output the names of the threads that were used.</p>
@param thread 1 for ReloadThread, 2 for StatThread */
public static void actionFinished(int thread) {
if (thread == 1 && debugLevel == DebugLevel.HIGH) {
ArrayList<String> leftOvers = new ArrayList<>(Arrays.asList(processedPlayers).subList(playersIndex.intValue() % 10, 10));
logger.info(leftOvers.toString());
}
if (debugLevel != DebugLevel.LOW) {
logger.info("Finished Recursive Action! In total " +
threadNames.size() + " Threads were used to process " +
playersIndex.get() + " Players.");
}
if (debugLevel == DebugLevel.HIGH) {
logger.info(Collections.list(threadNames.keys()).toString());
}
}
/** Output to console how long a certain task has taken if DebugLevel is equal to or higher than the specified threshold.
@param className Name of the class executing the task
@param methodName Name or description of the task
@param startTime Timestamp marking the beginning of the task
@param logThreshold the DebugLevel threshold */
public static void logTimeTaken(String className, String methodName, long startTime, DebugLevel logThreshold) {
switch (logThreshold) {
case LOW -> printTime(className, methodName, startTime);
case MEDIUM -> {
if (debugLevel != DebugLevel.LOW) {
printTime(className, methodName, startTime);
}
}
case HIGH -> {
if (debugLevel == DebugLevel.HIGH) {
printTime(className, methodName, startTime);
}
}
}
}
private static void log(String content, boolean logAsWarning) {
if (logAsWarning) {
logger.warning(content);
} else {
logger.info(content);
}
}
private static void printTime(String className, String methodName, long startTime) {
logger.info(className + " " + methodName + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
/** Accesses the playersIndex to up it by 1 and return its previous value. */
private static int nextPlayersIndex() {
return playersIndex.getAndIncrement();
}
/** Returns true if the playersIndex is 10, or any subsequent increment of 10. */
private static boolean incrementOfTen() {
return (playersIndex.get() == 10 || (playersIndex.get() > 10 && playersIndex.get() % 10 == 0));
}
}

View File

@ -1,63 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.Nullable;
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 */
public boolean isRelevantPlayer(String playerName) {
return offlinePlayerUUIDs.containsKey(playerName);
}
/** Returns the number of OfflinePlayers that are included in statistic calculations */
public int getOfflinePlayerCount() {
return offlinePlayerUUIDs.size();
}
/** Get an ArrayList of names from all OfflinePlayers that should be included in statistic calculations */
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
* @return OfflinePlayer (if this player is on the list, otherwise null)
*/
public @Nullable OfflinePlayer getOfflinePlayer(String playerName) {
if (offlinePlayerUUIDs.get(playerName) != null) {
return Bukkit.getOfflinePlayer(offlinePlayerUUIDs.get(playerName));
}
else {
MyLogger.logMsg("Cannot calculate statistics for player-name: " + playerName, true);
return null;
}
}
}

View File

@ -1,17 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
/** A small utility class that calculates with unix time.*/
public final class UnixTimeHandler {
private UnixTimeHandler() {
}
/** Calculates whether a player has played recently enough to fall within the lastPlayedLimit.
If lastPlayedLimit == 0, this always returns true (since there is no limit).
@param lastPlayed a long that represents the amount of milliseconds between the unix start point and the time this player last joined
@param lastPlayedLimit a long that represents the maximum-number-of-days-since-last-joined */
public static boolean hasPlayedSince(long lastPlayedLimit, long lastPlayed) {
long maxLastPlayed = System.currentTimeMillis() - lastPlayedLimit * 24 * 60 * 60 * 1000;
return lastPlayedLimit == 0 || lastPlayed >= maxLastPlayed;
}
}

View File

@ -0,0 +1,90 @@
# ------------------------------------------------------------------------------------------------------ #
# PlayerStats Language File #
# ------------------------------------------------------------------------------------------------------ #
stat_type.minecraft.mined: "Times Mined"
stat_type.minecraft.crafted: "Times Crafted"
stat_type.minecraft.used: "Times Used"
stat_type.minecraft.broken: "Times Broken"
stat_type.minecraft.picked_up: "Picked Up"
stat_type.minecraft.dropped: "Dropped"
stat_type.minecraft.killed: "Times Killed"
stat_type.minecraft.killed_by: "Number of Times Killed By"
stat.minecraft.animals_bred: "Animals Bred"
stat.minecraft.aviate_one_cm: "Distance by Elytra"
stat.minecraft.clean_armor: "Armor Pieces Cleaned"
stat.minecraft.clean_banner: "Banners Cleaned"
stat.minecraft.clean_shulker_box: "Shulker Boxes Cleaned"
stat.minecraft.climb_one_cm: "Distance Climbed"
stat.minecraft.bell_ring: "Bells Rung"
stat.minecraft.target_hit: "Targets Hit"
stat.minecraft.boat_one_cm: "Distance by Boat"
stat.minecraft.crouch_one_cm: "Distance Crouched"
stat.minecraft.damage_dealt: "Damage Dealt"
stat.minecraft.damage_dealt_absorbed: "Damage Dealt (Absorbed)"
stat.minecraft.damage_dealt_resisted: "Damage Dealt (Resisted)"
stat.minecraft.damage_taken: "Damage Taken"
stat.minecraft.damage_blocked_by_shield: "Damage Blocked by Shield"
stat.minecraft.damage_absorbed: "Damage Absorbed"
stat.minecraft.damage_resisted: "Damage Resisted"
stat.minecraft.deaths: "Number of Deaths"
stat.minecraft.walk_under_water_one_cm: "Distance Walked under Water"
stat.minecraft.drop: "Items Dropped"
stat.minecraft.eat_cake_slice: "Cake Slices Eaten"
stat.minecraft.enchant_item: "Items Enchanted"
stat.minecraft.fall_one_cm: "Distance Fallen"
stat.minecraft.fill_cauldron: "Cauldrons Filled"
stat.minecraft.fish_caught: "Fish Caught"
stat.minecraft.fly_one_cm: "Distance Flown"
stat.minecraft.horse_one_cm: "Distance by Horse"
stat.minecraft.inspect_dispenser: "Dispensers Searched"
stat.minecraft.inspect_dropper: "Droppers Searched"
stat.minecraft.inspect_hopper: "Hoppers Searched"
stat.minecraft.interact_with_anvil: "Interactions with Anvil"
stat.minecraft.interact_with_beacon: "Interactions with Beacon"
stat.minecraft.interact_with_brewingstand: "Interactions with Brewing Stand"
stat.minecraft.interact_with_campfire: "Interactions with Campfire"
stat.minecraft.interact_with_cartography_table: "Interactions with Cartography Table"
stat.minecraft.interact_with_crafting_table: "Interactions with Crafting Table"
stat.minecraft.interact_with_furnace: "Interactions with Furnace"
stat.minecraft.interact_with_grindstone: "Interactions with Grindstone"
stat.minecraft.interact_with_lectern: "Interactions with Lectern"
stat.minecraft.interact_with_loom: "Interactions with Loom"
stat.minecraft.interact_with_blast_furnace: "Interactions with Blast Furnace"
stat.minecraft.interact_with_smithing_table: "Interactions with Smithing Table"
stat.minecraft.interact_with_smoker: "Interactions with Smoker"
stat.minecraft.interact_with_stonecutter: "Interactions with Stonecutter"
stat.minecraft.jump: "Jumps"
stat.minecraft.junk_fished: "Junk Fished"
stat.minecraft.leave_game: "Games Quit"
stat.minecraft.minecart_one_cm: "Distance by Minecart"
stat.minecraft.mob_kills: "Mob Kills"
stat.minecraft.open_barrel: "Barrels Opened"
stat.minecraft.open_chest: "Chests Opened"
stat.minecraft.open_enderchest: "Ender Chests Opened"
stat.minecraft.open_shulker_box: "Shulker Boxes Opened"
stat.minecraft.pig_one_cm: "Distance by Pig"
stat.minecraft.strider_one_cm: "Distance by Strider"
stat.minecraft.player_kills: "Player Kills"
stat.minecraft.play_noteblock: "Note Blocks Played"
stat.minecraft.play_time: "Time Played"
stat.minecraft.play_record: "Music Discs Played"
stat.minecraft.pot_flower: "Plants Potted"
stat.minecraft.raid_trigger: "Raids Triggered"
stat.minecraft.raid_win: "Raids Won"
stat.minecraft.ring_bell: "Bells Rung"
stat.minecraft.sleep_in_bed: "Times Slept in a Bed"
stat.minecraft.sneak_time: "Sneak Time"
stat.minecraft.sprint_one_cm: "Distance Sprinted"
stat.minecraft.walk_on_water_one_cm: "Distance Walked on Water"
stat.minecraft.swim_one_cm: "Distance Swum"
stat.minecraft.talked_to_villager: "Talked to Villagers"
stat.minecraft.time_since_rest: "Time Since Last Rest"
stat.minecraft.time_since_death: "Time Since Last Death"
stat.minecraft.total_world_time: "Time with World Open"
stat.minecraft.traded_with_villager: "Traded with Villagers"
stat.minecraft.treasure_fished: "Treasure Fished"
stat.minecraft.trigger_trapped_chest: "Trapped Chests Triggered"
stat.minecraft.tune_noteblock: "Note Blocks Tuned"
stat.minecraft.use_cauldron: "Water Taken from Cauldron"
stat.minecraft.walk_one_cm: "Distance Walked"

View File

@ -1,6 +1,6 @@
main: com.gmail.artemis.the.gr8.playerstats.Main
main: com.github.artemis.the.gr8.playerstats.Main
name: PlayerStats
version: 1.6.1
version: 1.7
api-version: 1.13
description: adds commands to view player statistics in chat
author: Artemis_the_gr8