Merge pull request #104 from itHotL/placeholder-api

Placeholder api
This commit is contained in:
Elise 2022-08-04 15:16:35 +02:00 committed by GitHub
commit 84ee9a86db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
63 changed files with 2361 additions and 1056 deletions

View File

@ -5,6 +5,7 @@
<configuration>
<autoDetectTypes>
<platformType>SPIGOT</platformType>
<platformType>ADVENTURE</platformType>
</autoDetectTypes>
</configuration>
</facet>

View File

@ -26,6 +26,11 @@
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="placeholderapi" />
<option name="name" value="placeholderapi" />
<option name="url" value="https://repo.extendedclip.com/content/repositories/placeholderapi/" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />

View File

@ -6,6 +6,9 @@
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="JavadocGenerationManager">
<option name="OUTPUT_DIRECTORY" value="$PROJECT_DIR$/javadoc" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>

View File

@ -39,11 +39,15 @@
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.kyori</shadedPattern>
<shadedPattern>com.gmail.artemis.the.gr8.lib.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.tchristofferson</shadedPattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.tchristofferson</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
@ -67,9 +71,27 @@
<exclude>META-INF/**</exclude>
</excludes>
</filter>
<filter>
<artifact>org.bstats:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
@ -85,6 +107,10 @@
<id>maven-central</id>
<url>https://oss.sonatype.org/content/groups/public</url>
</repository>
<repository>
<id>placeholderapi</id>
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
</repositories>
<dependencies>
<dependency>
@ -93,6 +119,12 @@
<version>1.19-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>

48
pom.xml
View File

@ -30,6 +30,11 @@
<id>maven-central</id> <!-- Config-Updater -->
<url>https://oss.sonatype.org/content/groups/public</url>
</repository>
<repository>
<id>placeholderapi</id> <!-- Placeholder API -->
<url>https://repo.extendedclip.com/content/repositories/placeholderapi/</url>
</repository>
</repositories>
<dependencies>
@ -49,7 +54,7 @@
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-platform-bukkit</artifactId>
<version>4.1.1</version>
<version>4.1.2</version>
</dependency>
<dependency>
@ -58,12 +63,26 @@
<version>4.11.0</version>
</dependency>
<dependency>
<groupId>me.clip</groupId>
<artifactId>placeholderapi</artifactId>
<version>2.11.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.tchristofferson</groupId>
<artifactId>ConfigUpdater</artifactId>
<version>2.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.bstats</groupId>
<artifactId>bstats-bukkit</artifactId>
<version>3.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
@ -109,11 +128,15 @@
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.kyori</shadedPattern>
<shadedPattern>com.gmail.artemis.the.gr8.lib.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.tchristofferson</shadedPattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.tchristofferson</shadedPattern>
</relocation>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.util.bstats</shadedPattern>
</relocation>
</relocations>
<filters>
@ -137,9 +160,28 @@
<exclude>META-INF/**</exclude>
</excludes>
</filter>
<filter>
<artifact>org.bstats:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,5 +1,7 @@
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;
@ -7,42 +9,44 @@ 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.StatManager;
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 class Main extends JavaPlugin {
public final class Main extends JavaPlugin {
private static BukkitAudiences adventure;
public static @NotNull BukkitAudiences adventure() {
if (adventure == null) {
throw new IllegalStateException("Tried to access Adventure when the plugin was disabled!");
}
return 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() {
//initialize the Adventure library
adventure = BukkitAudiences.create(this);
//TODO fix (move these two into initializeMainClasses also, and remove all the Main.get... methods)
new Metrics(this, 15923);
//first get an instance of all the classes that need to be passed along to different classes
ConfigHandler config = new ConfigHandler(this);
OfflinePlayerHandler offlinePlayerHandler = new OfflinePlayerHandler();
OutputManager outputManager = OutputManager.getInstance(config);
ThreadManager threadManager = ThreadManager.getInstance(config, outputManager, offlinePlayerHandler);
ShareManager shareManager = ShareManager.getInstance(config);
//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, offlinePlayerHandler));
statcmd.setTabCompleter(new TabCompleter(offlinePlayerHandler));
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));
@ -51,7 +55,7 @@ public class Main extends JavaPlugin {
//register the listener
Bukkit.getPluginManager().registerEvents(new JoinListener(threadManager), this);
//finish up
this.getLogger().info("Enabled PlayerStats!");
}
@ -64,4 +68,54 @@ public class Main extends JavaPlugin {
}
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);
StatManager statManager = new StatManager(offlinePlayerHandler);
threadManager = new ThreadManager(config, statManager, outputManager);
playerStatsAPI = new PlayerStatsAPI(statManager, outputManager);
}
}

View File

@ -2,7 +2,7 @@ package com.gmail.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.models.StatResult;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.InternalStatResult;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
@ -13,43 +13,33 @@ import java.time.Instant;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
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.*/
public final class ShareManager {
private static volatile ShareManager instance;
private static boolean isEnabled;
private static int waitingTime;
private volatile AtomicInteger resultID;
private ConcurrentHashMap<UUID, StatResult> statResultQueue;
private ConcurrentHashMap<String, Instant> shareTimeStamp;
private ArrayBlockingQueue<UUID> sharedResults;
private static volatile AtomicInteger resultID;
private static ConcurrentHashMap<Integer, InternalStatResult> statResultQueue;
private static ConcurrentHashMap<String, Instant> shareTimeStamp;
private static ArrayBlockingQueue<Integer> sharedResults;
private ShareManager(ConfigHandler config) {
public ShareManager(ConfigHandler config) {
updateSettings(config);
}
public static ShareManager getInstance(ConfigHandler config) {
ShareManager shareManager = instance;
if (shareManager != null) {
return shareManager;
}
synchronized (ShareManager.class) {
if (instance == null) {
instance = new ShareManager(config);
}
return instance;
}
public static boolean isEnabled() {
return isEnabled;
}
public synchronized void updateSettings(ConfigHandler config) {
public static synchronized void updateSettings(ConfigHandler config) {
isEnabled = config.allowStatSharing() && config.useHoverText();
waitingTime = config.getStatShareWaitingTime();
@ -74,21 +64,18 @@ public final class ShareManager {
}
}
public boolean isEnabled() {
return isEnabled;
}
public boolean senderHasPermission(CommandSender sender) {
return !(sender instanceof ConsoleCommandSender) && sender.hasPermission("playerstats.share");
}
public UUID saveStatResult(String playerName, TextComponent statResult) {
public int saveStatResult(String playerName, TextComponent statResult) {
removeExcessResults(playerName);
int ID = getNextIDNumber();
UUID shareCode = UUID.randomUUID();
statResultQueue.put(shareCode, new StatResult(playerName, statResult, ID, shareCode));
//UUID shareCode = UUID.randomUUID();
InternalStatResult result = new InternalStatResult(playerName, statResult, ID);
int shareCode = result.hashCode();
statResultQueue.put(shareCode, result);
MyLogger.logMsg("Saving statResults with no. " + ID, DebugLevel.MEDIUM);
return shareCode;
}
@ -102,25 +89,25 @@ public final class ShareManager {
}
}
public boolean requestAlreadyShared(UUID shareCode) {
public boolean requestAlreadyShared(int shareCode) {
return sharedResults.contains(shareCode);
}
/** Takes a statResult from the internal ConcurrentHashmap,
/** Takes a formattedValue from the internal ConcurrentHashmap,
puts the current time in the shareTimeStamp (ConcurrentHashMap),
puts the shareCode (UUID) in the sharedResults (ArrayBlockingQueue),
and returns the statResult. If no statResult was found, returns null.*/
public @Nullable StatResult getStatResult(String playerName, UUID shareCode) {
puts the shareCode (int hashCode) in the sharedResults (ArrayBlockingQueue),
and returns the formattedValue. If no formattedValue 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);
ArrayBlockingQueue<UUID> newQueue = new ArrayBlockingQueue<>(500);
ArrayBlockingQueue<Integer> newQueue = new ArrayBlockingQueue<>(500);
synchronized (this) { //put the last 50 values in the new Queue
UUID[] lastValues = sharedResults.toArray(new UUID[0]);
Integer[] lastValues = sharedResults.toArray(new Integer[500]);
Arrays.stream(Arrays.copyOfRange(lastValues, 450, 500))
.parallel().iterator()
.forEachRemaining(newQueue::offer);
@ -139,18 +126,18 @@ public final class ShareManager {
/** 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<StatResult> alreadySavedResults = statResultQueue.values()
List<InternalStatResult> alreadySavedResults = statResultQueue.values()
.parallelStream()
.filter(result -> result.playerName().equalsIgnoreCase(playerName))
.filter(result -> result.executorName().equalsIgnoreCase(playerName))
.toList();
if (alreadySavedResults.size() > 25) {
UUID uuid = alreadySavedResults
int hashCode = alreadySavedResults
.parallelStream()
.min(Comparator.comparing(StatResult::ID))
.orElseThrow().uuid();
MyLogger.logMsg("Removing old stat no. " + statResultQueue.get(uuid).ID() + " for player " + playerName, DebugLevel.MEDIUM);
statResultQueue.remove(uuid);
.min(Comparator.comparing(InternalStatResult::ID))
.orElseThrow().hashCode();
MyLogger.logMsg("Removing old stat no. " + statResultQueue.get(hashCode).ID() + " for player " + playerName, DebugLevel.MEDIUM);
statResultQueue.remove(hashCode);
}
}

View File

@ -2,37 +2,40 @@ 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.models.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatManager;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
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 static volatile ThreadManager instance;
private final static int threshold = 10;
private int statThreadID;
private int reloadThreadID;
private static ConfigHandler config;
private static OutputManager messageSender;
private final OfflinePlayerHandler offlinePlayerHandler;
private static OutputManager outputManager;
private static StatManager statManager;
private ReloadThread lastActiveReloadThread;
private StatThread lastActiveStatThread;
private final HashMap<String, Thread> statThreads;
private static long lastRecordedCalcTime;
private ThreadManager(ConfigHandler c, OutputManager m, OfflinePlayerHandler o) {
config = c;
messageSender = m;
offlinePlayerHandler = o;
public ThreadManager(ConfigHandler config, StatManager statManager, OutputManager outputManager) {
ThreadManager.config = config;
ThreadManager.outputManager = outputManager;
ThreadManager.statManager = statManager;
statThreads = new HashMap<>();
statThreadID = 0;
@ -42,19 +45,6 @@ public final class ThreadManager {
startReloadThread(null);
}
public static ThreadManager getInstance(ConfigHandler config, OutputManager messageSender, OfflinePlayerHandler offlinePlayerHandler) {
ThreadManager threadManager = instance;
if (threadManager != null) {
return threadManager;
}
synchronized (ThreadManager.class) {
if (instance == null) {
instance = new ThreadManager(config, messageSender, offlinePlayerHandler);
}
return instance;
}
}
public static int getTaskThreshold() {
return threshold;
}
@ -63,7 +53,7 @@ public final class ThreadManager {
if (lastActiveReloadThread == null || !lastActiveReloadThread.isAlive()) {
reloadThreadID += 1;
lastActiveReloadThread = new ReloadThread(config, messageSender, offlinePlayerHandler, reloadThreadID, lastActiveStatThread, sender);
lastActiveReloadThread = new ReloadThread(config, outputManager, reloadThreadID, lastActiveStatThread, sender);
lastActiveReloadThread.start();
}
else {
@ -71,19 +61,19 @@ public final class ThreadManager {
}
}
public void startStatThread(StatRequest request) {
public void startStatThread(StatRequest statRequest) {
statThreadID += 1;
String cmdSender = request.getCommandSender().getName();
String cmdSender = statRequest.getCommandSender().getName();
if (config.limitStatRequests() && statThreads.containsKey(cmdSender)) {
Thread runningThread = statThreads.get(cmdSender);
if (runningThread.isAlive()) {
messageSender.sendFeedbackMsg(request.getCommandSender(), StandardMessage.REQUEST_ALREADY_RUNNING);
outputManager.sendFeedbackMsg(statRequest.getCommandSender(), StandardMessage.REQUEST_ALREADY_RUNNING);
} else {
startNewStatThread(request);
startNewStatThread(statRequest);
}
} else {
startNewStatThread(request);
startNewStatThread(statRequest);
}
}
@ -99,9 +89,9 @@ public final class ThreadManager {
return lastRecordedCalcTime;
}
private void startNewStatThread(StatRequest request) {
lastActiveStatThread = new StatThread(config, messageSender, offlinePlayerHandler, statThreadID, request, lastActiveReloadThread);
statThreads.put(request.getCommandSender().getName(), lastActiveStatThread);
private void startNewStatThread(StatRequest statRequest) {
lastActiveStatThread = new StatThread(outputManager, statManager, statThreadID, statRequest, lastActiveReloadThread);
statThreads.put(statRequest.getCommandSender().getName(), lastActiveStatThread);
lastActiveStatThread.start();
}
}

View File

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

@ -0,0 +1,58 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.PlayerStatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.ServerStatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.TopStatRequest;
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();
}
/** 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*/
PlayerStatRequest playerStat(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*/
ServerStatRequest serverStat();
/** 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*/
TopStatRequest topStat(int topListSize);
Formatter getFormatter();
}

View File

@ -0,0 +1,51 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatManager;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.*;
import static org.jetbrains.annotations.ApiStatus.Internal;
/** The implementation of the API Interface */
public final class PlayerStatsAPI implements PlayerStats {
private static StatCalculator statCalculator;
private static StatFormatter statFormatter;
@Internal
public PlayerStatsAPI(StatManager stat, StatFormatter format) {
statCalculator = stat;
statFormatter = format;
}
@Override
public PlayerStatRequest playerStat(String playerName) {
StatRequestHandler statRequestHandler = StatRequestHandler.playerRequestHandler(playerName);
return new PlayerStatRequest(statRequestHandler);
}
@Override
public ServerStatRequest serverStat() {
StatRequestHandler statRequestHandler = StatRequestHandler.serverRequestHandler();
return new ServerStatRequest(statRequestHandler);
}
@Override
public TopStatRequest topStat(int topListSize) {
StatRequestHandler statRequestHandler = StatRequestHandler.topRequestHandler(topListSize);
return new TopStatRequest(statRequestHandler);
}
@Override
public Formatter getFormatter() {
return statFormatter;
}
static StatCalculator statCalculator() {
return statCalculator;
}
static StatFormatter statFormatter() {
return statFormatter;
}
}

View File

@ -0,0 +1,51 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.statistic.result.StatResult;
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.ApiStatus.Internal;
import org.jetbrains.annotations.NotNull;
/** 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> {
@Internal
default StatCalculator getStatCalculator() {
return PlayerStatsAPI.statCalculator();
}
@Internal
default StatFormatter getStatFormatter() {
return PlayerStatsAPI.statFormatter();
}
/** Gets a StatResult for a Statistic of Statistic.Type {@code Untyped}.
@param statistic a Statistic of Type.Untyped
@return a {@link StatResult}
@throws IllegalArgumentException if <code>statistic</code> is not of Type.Untyped*/
StatResult<T> untyped(@NotNull Statistic statistic) throws IllegalArgumentException;
/** Gets a StatResult 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
@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>)
@return a {@link StatResult} */
StatResult<T> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) throws IllegalArgumentException;
/** Gets a StatResult for a Statistic of Statistic.Type Entity.
@param statistic a Statistic of Type.Entity
@param entityType an EntityType
@throws IllegalArgumentException if <code>statistic</code> is not of Type.Entity,
or <code>entityType</code> is not a valid EntityType
@return a {@link StatResult} */
StatResult<T> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException;
}

View File

@ -0,0 +1,39 @@
package com.gmail.artemis.the.gr8.playerstats.api;
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 StatCalculator}
to get the desired statistic data.*/
public interface RequestGenerator {
/** 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*/
StatRequest 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>) */
StatRequest 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*/
StatRequest entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) throws IllegalArgumentException;
}

View File

@ -0,0 +1,32 @@
package com.gmail.artemis.the.gr8.playerstats.api;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.statistic.request.StatRequest;
import org.jetbrains.annotations.ApiStatus.Internal;
import java.util.LinkedHashMap;
/** The {@link StatCalculator} is responsible for getting, calculating and/or ordering raw numbers.
It represents the actual statistic-getting magic that happens once a valid
{@link StatRequest} is passed to it.
<br>
<br>The StatCalculator gets its data from the vanilla statistic files (stored by the server). It can return three kinds of data,
depending on the chosen {@link Target}:
<br>- int (for {@link Target#PLAYER})
<br>- long (for {@link Target#SERVER})
<br>- LinkedHashMap[String player-name, Integer number] (for {@link Target#TOP})
<br>
<br>For more information on how to create a valid StatRequest,
see the class description for {@link StatRequest}.*/
@Internal
public interface StatCalculator {
/** Returns the requested Statistic*/
int getPlayerStat(StatRequest statRequest);
/** Don't call from main Thread!*/
long getServerStat(StatRequest statRequest);
/** Don't call from main Thread!*/
LinkedHashMap<String, Integer> getTopStats(StatRequest statRequest);
}

View File

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

@ -7,7 +7,7 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class ReloadCommand implements CommandExecutor {
public final class ReloadCommand implements CommandExecutor {
private static ThreadManager threadManager;

View File

@ -2,7 +2,7 @@ package com.gmail.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.models.StatResult;
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 org.bukkit.command.Command;
@ -10,9 +10,7 @@ import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class ShareCommand implements CommandExecutor {
public final class ShareCommand implements CommandExecutor {
private static ShareManager shareManager;
private static OutputManager outputManager;
@ -24,12 +22,12 @@ public class ShareCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) {
if (args.length == 1 && shareManager.isEnabled()) {
UUID shareCode;
if (args.length == 1 && ShareManager.isEnabled()) {
int shareCode;
try {
shareCode = UUID.fromString(args[0]);
shareCode = Integer.parseInt(args[0]);
} catch (IllegalArgumentException e) {
MyLogger.logException(e, "ShareCommand", "/statshare is being called without a valid UUID argument");
MyLogger.logException(e, "ShareCommand", "/statshare is being called without a valid share-code!");
return false;
}
if (shareManager.requestAlreadyShared(shareCode)) {
@ -39,11 +37,11 @@ public class ShareCommand implements CommandExecutor {
outputManager.sendFeedbackMsg(sender, StandardMessage.STILL_ON_SHARE_COOLDOWN);
}
else {
StatResult result = shareManager.getStatResult(sender.getName(), shareCode);
if (result == null) { //at this point the only possible cause of statResult being null is the request being older than 25 player-requests ago
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
outputManager.sendFeedbackMsg(sender, StandardMessage.STAT_RESULTS_TOO_OLD);
} else {
outputManager.shareStatResults(result.statResult());
outputManager.sendToAllPlayers(result.formattedValue());
}
}
}

View File

@ -3,31 +3,24 @@ package com.gmail.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.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.models.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class StatCommand implements CommandExecutor {
public final class StatCommand implements CommandExecutor {
private static ThreadManager threadManager;
private static OutputManager outputManager;
private final OfflinePlayerHandler offlinePlayerHandler;
public StatCommand(OutputManager m, ThreadManager t, OfflinePlayerHandler o) {
public StatCommand(OutputManager m, ThreadManager t) {
threadManager = t;
outputManager = m;
offlinePlayerHandler = o;
}
@Override
@ -40,138 +33,44 @@ public class StatCommand implements CommandExecutor {
outputManager.sendExamples(sender);
}
else {
StatRequest request = generateRequest(sender, args);
if (requestIsValid(request)) {
threadManager.startStatThread(request);
StatRequestHandler statRequestHandler = StatRequestHandler.internalRequestHandler(sender);
StatRequest statRequest = statRequestHandler.getRequestFromArgs(args);
if (statRequest.isValid()) {
threadManager.startStatThread(statRequest);
} else {
sendFeedback(statRequest);
return false;
}
}
return true;
}
/** Create a StatRequest Object with all the relevant information from the args[]. */
private StatRequest generateRequest(CommandSender sender, String[] args) {
StatRequest request = new StatRequest(sender);
for (String arg : args) {
//check for statName
if (EnumHandler.isStatistic(arg) && request.getStatistic() == null) {
request.setStatistic(EnumHandler.getStatEnum(arg));
}
//check for subStatEntry and playerFlag
else if (EnumHandler.isSubStatEntry(arg)) {
if (arg.equalsIgnoreCase("player") && !request.playerFlag()) {
request.setPlayerFlag(true);
}
else {
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
}
}
//check for selection
else if (arg.equalsIgnoreCase("top")) {
request.setSelection(Target.TOP);
}
else if (arg.equalsIgnoreCase("server")) {
request.setSelection(Target.SERVER);
}
else if (arg.equalsIgnoreCase("me")) {
if (sender instanceof Player) {
request.setPlayerName(sender.getName());
request.setSelection(Target.PLAYER);
}
else if (sender instanceof ConsoleCommandSender) {
request.setSelection(Target.SERVER);
}
}
else if (offlinePlayerHandler.isRelevantPlayer(arg) && request.getPlayerName() == null) {
request.setPlayerName(arg);
request.setSelection(Target.PLAYER);
}
}
patchRequest(request);
return request;
}
/** 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
*/
private void sendFeedback(StatRequest statRequest) {
CommandSender sender = statRequest.getCommandSender();
/** 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 request) {
if (request.getStatistic() != null) {
Statistic.Type type = request.getStatistic().getType();
if (request.playerFlag()) { //unpack the playerFlag
if (type == Statistic.Type.ENTITY && request.getSubStatEntry() == null) {
request.setSubStatEntry("player");
}
else {
request.setSelection(Target.PLAYER);
}
}
String subStatEntry = request.getSubStatEntry();
switch (type) { //attempt to convert relevant subStatEntries into their corresponding Enum Constant
case BLOCK -> {
Material block = EnumHandler.getBlockEnum(subStatEntry);
if (block != null) request.setBlock(block);
}
case ENTITY -> {
EntityType entity = EnumHandler.getEntityEnum(subStatEntry);
if (entity != null) request.setEntity(entity);
}
case ITEM -> {
Material item = EnumHandler.getItemEnum(subStatEntry);
if (item != null) request.setItem(item);
}
case UNTYPED -> { //remove unnecessary subStatEntries
if (subStatEntry != null) request.setSubStatEntry(null);
}
}
if (statRequest.getStatistic() == null) {
outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_STAT_NAME);
}
}
/** This method validates the StatRequest and returns feedback to the player if it returns false.
It checks the following:
<p>1. Is a Statistic set?</p>
<p>2. Is a subStat needed, and is a subStat Enum Constant present? (block/entity/item)</p>
<p>3. If the target is PLAYER, is a valid PlayerName provided? </p>
@return true if the Request is valid, and false + an explanation message otherwise. */
private boolean requestIsValid(StatRequest request) {
if (request.getStatistic() == null) {
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.MISSING_STAT_NAME);
return false;
}
Statistic.Type type = request.getStatistic().getType();
if (request.getSubStatEntry() == null && type != Statistic.Type.UNTYPED) {
outputManager.sendFeedbackMsgMissingSubStat(request.getCommandSender(), type);
return false;
}
else if (!matchingSubStat(request)) {
outputManager.sendFeedbackMsgWrongSubStat(request.getCommandSender(), type, request.getSubStatEntry());
return false;
}
else if (request.getSelection() == Target.PLAYER && request.getPlayerName() == null) {
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.MISSING_PLAYER_NAME);
return false;
else if (statRequest.getTarget() == Target.PLAYER && statRequest.getPlayerName() == null) {
outputManager.sendFeedbackMsg(sender, StandardMessage.MISSING_PLAYER_NAME);
}
else {
return true;
}
}
private boolean matchingSubStat(StatRequest request) {
Statistic.Type type = request.getStatistic().getType();
switch (type) {
case BLOCK -> {
return request.getBlock() != null;
}
case ENTITY -> {
return request.getEntity() != null;
}
case ITEM -> {
return request.getItem() != null;
}
default -> {
return true;
Statistic.Type type = statRequest.getStatistic().getType();
if (type != Statistic.Type.UNTYPED && statRequest.getSubStatEntryName() == null) {
outputManager.sendFeedbackMsgMissingSubStat(sender, type);
} else {
outputManager.sendFeedbackMsgWrongSubStat(sender, type, statRequest.getSubStatEntryName());
}
}
}

View File

@ -12,16 +12,18 @@ import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TabCompleter implements org.bukkit.command.TabCompleter {
public final class TabCompleter implements org.bukkit.command.TabCompleter {
private final EnumHandler enumHandler;
private final OfflinePlayerHandler offlinePlayerHandler;
private final TabCompleteHelper tabCompleteHelper;
private final List<String> commandOptions;
public TabCompleter(OfflinePlayerHandler o) {
offlinePlayerHandler = o;
tabCompleteHelper = new TabCompleteHelper();
public TabCompleter(EnumHandler enumHandler, OfflinePlayerHandler offlinePlayerHandler) {
this.enumHandler = enumHandler;
this.offlinePlayerHandler = offlinePlayerHandler;
tabCompleteHelper = new TabCompleteHelper(enumHandler);
commandOptions = new ArrayList<>();
commandOptions.add("top");
@ -33,8 +35,8 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
//args[0] = statistic (length = 1)
//args[1] = commandOption (top/player/me) OR substatistic (block/item/entitytype) (length = 2)
//args[2] = playerName OR commandOption (top/player/me) (length = 3)
//args[3] = playerName (length = 4)
//args[2] = executorName OR commandOption (top/player/me) (length = 3)
//args[3] = executorName (length = 4)
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
@ -50,7 +52,7 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
else { //after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions
String previousArg = args[args.length -2];
if (EnumHandler.isStatistic(previousArg)) {
if (enumHandler.isStatistic(previousArg)) {
Statistic stat = EnumHandler.getStatEnum(previousArg);
if (stat != null) {
tabSuggestions = getTabSuggestions(getRelevantList(stat), currentArg);
@ -60,7 +62,7 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
//if previous arg = "player"
else if (previousArg.equalsIgnoreCase("player")) {
if (args.length >= 3 && EnumHandler.isEntityStatistic(args[args.length-3])) {
if (args.length >= 3 && enumHandler.isEntityStatistic(args[args.length-3])) {
tabSuggestions = commandOptions; //if arg before "player" was entity-stat, suggest commandOptions
}
else { //otherwise "player" is target-flag: suggest playerNames
@ -69,7 +71,7 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
}
//after a substatistic, suggest commandOptions
else if (EnumHandler.isSubStatEntry(previousArg)) {
else if (enumHandler.isSubStatEntry(previousArg)) {
tabSuggestions = commandOptions;
}
}
@ -78,7 +80,7 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
}
private List<String> getFirstArgSuggestions(String currentArg) {
List<String> suggestions = EnumHandler.getStatNames();
List<String> suggestions = enumHandler.getStatNames();
suggestions.add("examples");
suggestions.add("help");
return getTabSuggestions(suggestions, currentArg);

View File

@ -10,15 +10,17 @@ import java.util.stream.Collectors;
public final class TabCompleteHelper {
private final EnumHandler enumHandler;
private static List<String> itemBrokenSuggestions;
private static List<String> entitySuggestions;
public TabCompleteHelper() {
public TabCompleteHelper(EnumHandler enumHandler) {
this.enumHandler = enumHandler;
prepareLists();
}
public List<String> getAllItemNames() {
return EnumHandler.getItemNames();
return enumHandler.getItemNames();
}
public List<String> getItemBrokenSuggestions() {
@ -26,7 +28,7 @@ public final class TabCompleteHelper {
}
public List<String> getAllBlockNames() {
return EnumHandler.getBlockNames();
return enumHandler.getBlockNames();
}
public List<String> getEntitySuggestions() {
@ -52,4 +54,4 @@ public final class TabCompleteHelper {
.map(String::toLowerCase)
.collect(Collectors.toList());
}
}
}

View File

@ -2,6 +2,7 @@ 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;
@ -10,7 +11,7 @@ import org.jetbrains.annotations.Nullable;
import java.io.File;
public class ConfigHandler {
public final class ConfigHandler {
private static Main plugin;
private static int configVersion;
@ -18,8 +19,8 @@ public class ConfigHandler {
private File configFile;
private FileConfiguration config;
public ConfigHandler(Main p) {
plugin = p;
public ConfigHandler(Main plugin) {
ConfigHandler.plugin = plugin;
configVersion = 6;
saveDefaultConfig();
@ -29,11 +30,12 @@ public class ConfigHandler {
MyLogger.setDebugLevel(getDebugLevel());
}
/** Checks the number that "config-version" returns to see if the config needs updating, and if so, send it to the Updater.
<p>PlayerStats 1.1: "config-version" doesn't exist.</p>
<p>PlayerStats 1.2: "config-version" is 2.</p>
<p>PlayerStats 1.3: "config-version" is 3. </P>
<p>PlayerStats 1.4: "config-version" is 4.</p>*/
/** 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);
@ -49,7 +51,7 @@ public class ConfigHandler {
}
/** 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 MyLogger. */
Also reads the value for debug-level and passes it on to {@link MyLogger}. */
public boolean reloadConfig() {
if (!configFile.exists()) {
saveDefaultConfig();
@ -65,76 +67,81 @@ public class ConfigHandler {
}
/** Returns the desired debugging level.
<p>1 = low (only show unexpected errors)</p>
<p>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</p>
<p>3 = high (log all tasks and time taken)</p>
<p>Default: 1</p>*/
<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.
<p>Default: true</p>*/
<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.
<p>Default: true</p>*/
<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.
<p>Default: 0</p>*/
<br>Default: 0</br>*/
public int getStatShareWaitingTime() {
return config.getInt("waiting-time-before-sharing-again", 0);
}
/** Returns the config setting for include-whitelist-only.
<p>Default: false</p>*/
<br>Default: false</br>*/
public boolean whitelistOnly() {
return config.getBoolean("include-whitelist-only", false);
}
/** Returns the config setting for exclude-banned-players.
<p>Default: false</p>*/
<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.
<p>Default: 0 (which signals not to use this limit)</p>*/
<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.
<p>Default: true</p>*/
<br>Default: true</br>*/
public boolean useTranslatableComponents() {
return config.getBoolean("translate-to-client-language", true);
}
/** Whether to use HoverComponents for additional information.
<p>Default: true</p>*/
<br>Default: true</br>*/
public boolean useHoverText() {
return config.getBoolean("enable-hover-text", true);
}
/** Whether to use festive formatting, such as pride colors.
<p>Default: true</p> */
<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.
<p>Default: false</p> */
<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;
@ -153,112 +160,112 @@ public class ConfigHandler {
}
/** Returns the config setting for use-dots.
<p>Default: true</p>*/
<br>Default: true</br>*/
public boolean useDots() {
return config.getBoolean("use-dots", true);
}
/** Returns the config setting for top-list-max-size.
<p>Default: 10</p> */
<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.
<p>Default: "Top"</p>*/
<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.
<p>Default: "Total on"</p> */
<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.
<p>Default: "this server"</p>*/
<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.
<p>Default: Blocks for plain text, km for hover-text</p>*/
public String getDistanceUnit(boolean isHoverText) {
return getUnitString(isHoverText, "blocks", "km", "distance-unit");
<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.
<p>Default: Hearts for plain text, HP for hover-text.</p>*/
public String getDamageUnit(boolean isHoverText) {
return getUnitString(isHoverText, "hearts", "hp", "damage-unit");
<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.
<p>Default: true</p>*/
public boolean autoDetectTimeUnit(boolean isHoverText) {
<br>Default: true</br>*/
public boolean autoDetectTimeUnit(boolean isUnitForHoverText) {
String path = "auto-detect-biggest-time-unit";
if (isHoverText) {
if (isUnitForHoverText) {
path = path + "-for-hover-text";
}
boolean defaultValue = !isHoverText;
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.
<p>Default: 1 for plain text, 0 for hover-text</p>*/
public int getNumberOfExtraTimeUnits(boolean isHoverText) {
<br>Default: 1 for plain text, 0 for hover-text</br>*/
public int getNumberOfExtraTimeUnits(boolean isUnitForHoverText) {
String path = "number-of-extra-units";
if (isHoverText) {
if (isUnitForHoverText) {
path = path + "-for-hover-text";
}
int defaultValue = isHoverText ? 0 : 1;
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).
<p>Default: days for plain text, hours for hover-text</p>*/
public String getTimeUnit(boolean isHoverText) {
return getTimeUnit(isHoverText, false);
<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).
<p>Default: hours for plain text, seconds for hover-text</p>*/
public String getTimeUnit(boolean isHoverText, boolean smallUnit) {
<br>Default: hours for plain text, seconds for hover-text</br>*/
public String getTimeUnit(boolean isUnitForHoverText, boolean smallUnit) {
if (smallUnit) {
return getUnitString(isHoverText, "hours", "seconds", "smallest-time-unit");
return getUnitString(isUnitForHoverText, "hours", "seconds", "smallest-time-unit");
}
return getUnitString(isHoverText, "days", "hours", "biggest-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.
<p>Default: 20</p>*/
<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:
* <p>Style: "italic"</p>
* <p>Color: "gray"</p>*/
* <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:
* <p>Style: "none"</p>
* <p>Color: "#845EC2"</p>*/
* <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:
<p>Style: "none"</p>
<p>Color Top: "green"</p>
<p>Color Individual/Server: "gold"</p>*/
<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) {
@ -270,10 +277,10 @@ public class ConfigHandler {
return getDecorationString(selection, getStyleSetting, def, "player-names");
}
/** Returns true if playerNames Style is "bold", false if it is not.
<p>Default: false</p>*/
/** 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.PLAYER);
ConfigurationSection style = getRelevantSection(Target.TOP);
if (style != null) {
String styleString = style.getString("player-names");
@ -283,23 +290,23 @@ public class ConfigHandler {
}
/** Returns a String that represents either a Chat Color, hex color code, or a Style. Default values are:
<p>Style: "none"</p>
<p>Color: "yellow"</p>*/
<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:
<p>Style: "none"</p>
<p>Color: "#FFD52B"</p>*/
<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:
<p>Style: "none"</p>
<p>Color Top: "#55AAFF"</p>
<p>Color Individual/Server: "#ADE7FF"</p> */
<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) {
@ -312,9 +319,9 @@ public class ConfigHandler {
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<p>Style: "none"</p>
<p>Color Top: "yellow"</p>
<p>Color Server: "gold"</p>*/
<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) {
@ -327,34 +334,34 @@ public class ConfigHandler {
}
/** Returns a String that represents either a Chat Color, hex color code, or Style. Default values are:
<p>Style: "none"</p>
<p>Color: "gold"</p>*/
<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:
<p>Style: "none"</p>
<p>Color: "#FFB80E"</p>*/
<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:
<p>Style: "none"</p>
<p>Color: "gold"</p>*/
<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:
<p>Style: "none"</p>
<p>Color: "dark_gray"</p> */
<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 Unit that should be used for a certain Unit.Type.
/** 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

View File

@ -9,7 +9,7 @@ import java.io.IOException;
import com.tchristofferson.configupdater.ConfigUpdater;
public class ConfigUpdateHandler {
public final class ConfigUpdateHandler {
/** Add new key-value pairs to the config without losing comments, using <a href="https://github.com/tchristofferson/Config-Updater">tchristofferson's Config-Updater</a> */
public ConfigUpdateHandler(Main plugin, File configFile, int configVersion) {

View File

@ -1,5 +1,10 @@
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

@ -6,34 +6,63 @@ import net.kyori.adventure.text.format.TextColor;
import java.util.Random;
/** This enum represents the colorscheme PlayerStats uses in its output messages.
<p>GRAY: ChatColor Gray</p>
<p>DARK_PURPLE: #6E3485 (used for default sub-titles, title-underscores and brackets)</p>
<p>MEDIUM_BLUE: #55AAFF (used for all plain feedback and error messages)</p>
<p>LIGHT_BLUE: #55C6FF (used for default hover-text)</p>
<p>GOLD: ChatColor Gold (used for first parts of usage messages and for first parts of hover-text accent)</p>
<p>MEDIUM_GOLD: #FFD52B (used for second parts of usage messages and for second parts of hover-text accent) </p>
<p>LIGHT_GOLD: #FFEA40 (used for third parts of usage messages)</p>
<p>LIGHT_YELLOW: #FFFF8E (used for last parts of explanation message)</p>
*/
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 {
GRAY (NamedTextColor.GRAY), //#AAAAAA
/** 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")),
GOLD (NamedTextColor.GOLD), //#FFAA00
/** 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
NAME_2 (TextColor.fromHexString("#4287F5")), //between blue and medium_blue
NAME_3 (TextColor.fromHexString("#55AAFF")), //same as medium_blue
NAME_4 (TextColor.fromHexString("#D65DB1")), //magenta-purple
NAME_5 (TextColor.fromHexString("#EE8A19")), //dark orange
NAME_6 (TextColor.fromHexString("#01C1A7")), //aqua-cyan-green-ish
NAME_7 (TextColor.fromHexString("#46D858")); //light green
/** 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;
@ -42,18 +71,23 @@ public enum PluginColor {
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)) {

View File

@ -1,5 +1,7 @@
package com.gmail.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.*/
public enum StandardMessage {
RELOADED_CONFIG,
STILL_RELOADING,

View File

@ -1,5 +1,6 @@
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

@ -3,6 +3,7 @@ package com.gmail.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.*/
public enum Unit {
NUMBER (Type.UNTYPED, "Times"),
KM (Type.DISTANCE, "km"),
@ -31,10 +32,15 @@ public enum Unit {
return this.label;
}
/** Returns the Type this enum constant belongs to.*/
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*/
public Unit getSmallerUnit(int stepsSmaller) {
switch (this) {
case DAY -> {
@ -93,6 +99,7 @@ public enum Unit {
}
}
/** Converts the current Unit into seconds (and returns -1 if the current Unit is not of Type TIME)*/
public double getSeconds() {
return switch (this) {
case DAY -> 86400;

View File

@ -5,6 +5,7 @@ import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
/** Listens for new Players that join, and reloads PlayerStats if someone joins that hasn't joined before.*/
public class JoinListener implements Listener {
private static ThreadManager threadManager;

View File

@ -1,108 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.models;
import com.gmail.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.jetbrains.annotations.NotNull;
public final class StatRequest {
private final CommandSender sender;
private Statistic statistic;
private String playerName;
private Target selection;
private String subStatEntry;
private EntityType entity;
private Material block;
private Material item;
private boolean playerFlag;
//make a StatRequest for a given CommandSender with some default values
public StatRequest(@NotNull CommandSender s) {
sender = s;
selection = Target.TOP;
playerFlag = false;
}
public @NotNull CommandSender getCommandSender() {
return sender;
}
public boolean isConsoleSender() {
return sender instanceof ConsoleCommandSender;
}
public void setStatistic(Statistic statistic) {
this.statistic = statistic;
}
/** Returns the set enum constant Statistic, or null if none was set. */
public Statistic getStatistic() {
return statistic;
}
/** Sets the subStatEntry, and automatically tries to get the corresponding item/block/entity if there is a valid statType present.
If the subStatEntry is set to null, any present item/block/entity is set to null again. */
public void setSubStatEntry(String subStatEntry) {
this.subStatEntry = subStatEntry;
}
public String getSubStatEntry() {
return subStatEntry;
}
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
public String getPlayerName() {
return playerName;
}
/** False by default, set to true if args[] contains "player". */
public void setPlayerFlag(boolean playerFlag) {
this.playerFlag = playerFlag;
}
/** The "player" arg is a special case, because it could either be a valid subStatEntry, or indicate that the lookup action should target a specific player.
This is why the playerFlag exists - if this is true, and playerName is null, subStatEntry should be set to "player". */
public boolean playerFlag() {
return playerFlag;
}
public void setSelection(Target selection) {
this.selection = selection;
}
public @NotNull Target getSelection() {
return selection;
}
public void setEntity(EntityType entity) {
this.entity = entity;
}
public EntityType getEntity() {
return entity;
}
public void setBlock(Material block) {
this.block = block;
}
public Material getBlock() {
return block;
}
public void setItem(Material item) {
this.item = item;
}
public Material getItem() {
return item;
}
}

View File

@ -1,8 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.models;
import net.kyori.adventure.text.TextComponent;
import java.util.UUID;
public record StatResult(String playerName, TextComponent statResult, int ID, UUID uuid) {
}

View File

@ -5,12 +5,11 @@ 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.models.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
@ -26,36 +25,49 @@ 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 ComponentFactory.
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 class MessageWriter {
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 MessageWriter(ConfigHandler config) {
private MessageBuilder(ConfigHandler config) {
this (config, new ComponentFactory(config));
}
private MessageWriter(ConfigHandler configHandler, ComponentFactory factory) {
private MessageBuilder(ConfigHandler configHandler, ComponentFactory factory) {
config = configHandler;
useHoverText = config.useHoverText();
componentFactory = factory;
formatter = new NumberFormatter();
languageKeyHandler = new LanguageKeyHandler();
MyLogger.logMsg("MessageWriter created with factory: " + componentFactory.getClass().getSimpleName(), DebugLevel.MEDIUM);
MyLogger.logMsg("MessageBuilder created with factory: " + componentFactory.getClass().getSimpleName(), DebugLevel.MEDIUM);
}
public static MessageWriter defaultWriter(ConfigHandler config) {
return new MessageWriter(config);
public static MessageBuilder defaultBuilder(ConfigHandler config) {
return new MessageBuilder(config);
}
public static MessageWriter fromComponentFactory(ConfigHandler config, ComponentFactory factory) {
return new MessageWriter(config, factory);
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() {
@ -90,7 +102,7 @@ public class MessageWriter {
return componentFactory.pluginPrefix()
.append(space())
.append(componentFactory.message().content(
"Please add a valid " + EnumHandler.getSubStatTypeName(statType) + " to look up this statistic!"));
"Please add a valid " + getSubStatTypeName(statType) + " to look up this statistic!"));
}
public TextComponent missingPlayerName() {
@ -106,7 +118,7 @@ public class MessageWriter {
.append(componentFactory.messageAccent().content("\"" + subStatName + "\""))
.append(space())
.append(componentFactory.message().content(
"is not a valid " + EnumHandler.getSubStatTypeName(statType) + "!"));
"is not a valid " + getSubStatTypeName(statType) + "!"));
}
public TextComponent requestAlreadyRunning() {
@ -156,48 +168,63 @@ public class MessageWriter {
return ExampleMessage.construct(componentFactory);
}
public TextComponent helpMsg(boolean isConsoleSender) {
public TextComponent helpMsg() {
int listSize = config.getTopListMaxSize();
if (!isConsoleSender && config.useHoverText()) {
if (!isConsoleBuilder && useHoverText) {
return HelpMessage.constructHoverMsg(componentFactory, listSize);
} else {
return HelpMessage.constructPlainMsg(componentFactory, listSize);
}
}
public BiFunction<UUID, CommandSender, TextComponent> formattedPlayerStatFunction(int stat, @NotNull StatRequest request) {
/** 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(request.getPlayerName(), Target.PLAYER)
.append(componentFactory.playerName(statRequest.getPlayerName(), Target.PLAYER)
.append(text(":"))
.append(space()))
.append(getStatNumberComponent(request.getStatistic(), stat, Target.PLAYER, request.isConsoleSender()))
.append(getStatNumberComponent(statRequest, stat))
.append(space())
.append(getStatNameComponent(request))
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender())) //space is provided by statUnitComponent
.append(getStatNameComponent(statRequest))
.append(getStatUnitComponent(statRequest.getStatistic(), statRequest.getTarget())) //space is provided by statUnitComponent
.build();
return getFormattingFunction(playerStat, Target.PLAYER);
}
public BiFunction<UUID, CommandSender, TextComponent> formattedServerStatFunction(long stat, @NotNull StatRequest request) {
/** 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(request.getStatistic(), stat, Target.SERVER, request.isConsoleSender()))
.append(getStatNumberComponent(statRequest, stat))
.append(space())
.append(getStatNameComponent(request))
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender())) //space is provided by statUnit
.append(getStatNameComponent(statRequest))
.append(getStatUnitComponent(statRequest.getStatistic(), statRequest.getTarget())) //space is provided by statUnit
.build();
return getFormattingFunction(serverStat, Target.SERVER);
}
public BiFunction<UUID, CommandSender, TextComponent> formattedTopStatFunction(@NotNull LinkedHashMap<String, Integer> topStats, @NotNull StatRequest request) {
final TextComponent title = getTopStatsTitleComponent(request, topStats.size());
final TextComponent shortTitle = getTopStatsTitleShortComponent(request, topStats.size());
final TextComponent list = getTopStatListComponent(topStats, request);
/** 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);
@ -241,9 +268,226 @@ public class MessageWriter {
};
}
private BiFunction<UUID, CommandSender, TextComponent> getFormattingFunction(@NotNull TextComponent statResult, Target selection) {
boolean useEnters = config.useEnters(selection, false);
boolean useEntersForShared = config.useEnters(selection, true);
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();
@ -278,141 +522,6 @@ public class MessageWriter {
};
}
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 request, 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(request)) //space is provided by statUnitComponent
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender()))
.build();
}
private TextComponent getTopStatsTitleShortComponent(StatRequest request, int statListSize) {
return Component.text()
.append(componentFactory.title(config.getTopStatsTitle(), Target.TOP)).append(space())
.append(componentFactory.titleNumber(statListSize)).append(space())
.append(getStatNameComponent(request)) //space is provided by statUnitComponent
.build();
}
private TextComponent getTopStatListComponent(LinkedHashMap<String, Integer> topStats, StatRequest request) {
TextComponent.Builder topList = Component.text();
boolean useDots = config.useDots();
boolean boldNames = config.playerNameIsBold();
Set<String> playerNames = topStats.keySet();
int count = 0;
for (String playerName : playerNames) {
TextComponent.Builder playerNameBuilder = componentFactory.playerName(playerName, Target.TOP).toBuilder();
topList.append(newline())
.append(componentFactory.rankNumber(" " + ++count + "."))
.append(space());
if (useDots) {
topList.append(playerNameBuilder)
.append(space());
int dots = FontUtils.getNumberOfDotsToAlign(count + ". " + playerName, request.isConsoleSender(), boldNames);
if (dots >= 1) {
topList.append(componentFactory.dots().append(text((".".repeat(dots)))));
}
}
else {
topList.append(playerNameBuilder.append(text(":")));
}
topList.append(space()).append(getStatNumberComponent(request.getStatistic(), topStats.get(playerName), Target.TOP, request.isConsoleSender()));
}
return topList.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 request) {
if (config.useTranslatableComponents()) {
String statKey = languageKeyHandler.getStatKey(request.getStatistic());
String subStatKey = request.getSubStatEntry();
if (subStatKey != null) {
switch (request.getStatistic().getType()) {
case BLOCK -> subStatKey = languageKeyHandler.getBlockKey(request.getBlock());
case ENTITY -> subStatKey = languageKeyHandler.getEntityKey(request.getEntity());
case ITEM -> subStatKey = languageKeyHandler.getItemKey(request.getItem());
default -> {
}
}
}
return componentFactory.statAndSubStatNameTranslatable(statKey, subStatKey, request.getSelection());
}
else {
return componentFactory.statAndSubStatName(
StringUtils.prettify(request.getStatistic().toString()),
StringUtils.prettify(request.getSubStatEntry()),
request.getSelection());
}
}
private TextComponent getStatNumberComponent(Statistic statistic, long statNumber, Target selection, boolean isConsoleSender) {
Unit.Type type = Unit.getTypeFromStatistic(statistic);
Unit statUnit;
switch (type) {
case DISTANCE -> statUnit = Unit.fromString(config.getDistanceUnit(false));
case DAMAGE -> statUnit = Unit.fromString(config.getDamageUnit(false));
case TIME -> {
return getTimeNumberComponent(statNumber, selection, getTimeUnitRange(statNumber));
}
default -> statUnit = Unit.NUMBER;
}
String prettyNumber = formatter.format(statNumber, statUnit);
if (!config.useHoverText() || statUnit == Unit.NUMBER) {
return componentFactory.statNumber(prettyNumber, selection);
}
Unit hoverUnit = type == Unit.Type.DISTANCE ? Unit.fromString(config.getDistanceUnit(true)) :
Unit.fromString(config.getDamageUnit(true));
String prettyHoverNumber = formatter.format(statNumber, hoverUnit);
MyLogger.logMsg("mainNumber: " + prettyNumber + ", hoverNumber: " + prettyHoverNumber, DebugLevel.HIGH);
if (hoverUnit == Unit.HEART) {
return componentFactory.damageNumberWithHoverText(
prettyNumber, prettyHoverNumber,
componentFactory.heart(isConsoleSender, true), selection);
}
if (config.useTranslatableComponents()) {
String unitKey = languageKeyHandler.getUnitKey(hoverUnit);
if (unitKey != null) {
return componentFactory.statNumberWithHoverText(prettyNumber, prettyHoverNumber, null, unitKey, selection);
}
}
return componentFactory.statNumberWithHoverText(prettyNumber, prettyHoverNumber, hoverUnit.getLabel(), null, selection);
}
private TextComponent getTimeNumberComponent(long statNumber, Target selection, ArrayList<Unit> unitRange) {
if (unitRange.size() <= 1 || (config.useHoverText() && unitRange.size() <= 3)) {
MyLogger.logMsg(
"There is something wrong with the time-units you specified, please check your config!",
true);
return componentFactory.statNumber("-", selection);
}
else {
String mainNumber = formatter.format(statNumber, unitRange.get(0), unitRange.get(1));
if (!config.useHoverText()) {
return componentFactory.statNumber(mainNumber, selection);
} else {
String hoverNumber = formatter.format(statNumber, unitRange.get(2), unitRange.get(3));
MyLogger.logMsg("mainNumber: " + mainNumber + ", hoverNumber: " + hoverNumber, DebugLevel.HIGH);
return componentFactory.statNumberWithHoverText(mainNumber, hoverNumber,
null, null, selection);
}
}
}
/** Get an ArrayList consisting of 2 or 4 timeUnits. The order of items is:
<p>0. maxUnit</p>
<p>1. minUnit</p>
@ -429,7 +538,7 @@ public class MessageWriter {
unitRange.add(bigUnit);
unitRange.add(bigUnit.getSmallerUnit(config.getNumberOfExtraTimeUnits(false)));
}
if (config.useHoverText()) {
if (useHoverText) {
if (!config.autoDetectTimeUnit(true)) {
unitRange.add(Unit.fromString(config.getTimeUnit(true)));
unitRange.add(Unit.fromString(config.getTimeUnit(true, true)));
@ -444,28 +553,15 @@ public class MessageWriter {
return unitRange;
}
private TextComponent getStatUnitComponent(Statistic statistic, Target selection, boolean isConsoleSender) {
Unit statUnit;
switch (Unit.getTypeFromStatistic(statistic)) {
case DAMAGE -> statUnit = Unit.fromString(config.getDamageUnit(false));
case DISTANCE -> statUnit = Unit.fromString(config.getDistanceUnit(false));
default -> {
return Component.empty();
}
/** 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";
}
if (config.useTranslatableComponents()) {
String unitKey = languageKeyHandler.getUnitKey(statUnit);
if (unitKey != null) {
return Component.space()
.append(componentFactory.statUnit(null, unitKey, selection));
}
}
String statName = statUnit.getLabel();
if (statUnit == Unit.HEART) { //console can do u2665, u2764 looks better in-game
return Component.space()
.append(componentFactory.heart(isConsoleSender, false));
}
return Component.space()
.append(componentFactory.statUnit(statName, null, selection));
return subStat;
}
}

View File

@ -1,10 +1,11 @@
package com.gmail.artemis.the.gr8.playerstats.msg;
import com.gmail.artemis.the.gr8.playerstats.Main;
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.models.StatRequest;
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 net.kyori.adventure.platform.bukkit.BukkitAudiences;
@ -14,162 +15,210 @@ import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.time.LocalDate;
import java.time.Month;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import static com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage.*;
public final class OutputManager {
private static volatile OutputManager instance;
/** 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 {
private static BukkitAudiences adventure;
private static ConfigHandler config;
private static ShareManager shareManager;
private static MessageWriter writer;
private static MessageWriter consoleWriter;
private static MessageBuilder messageBuilder;
private static MessageBuilder consoleMessageBuilder;
private static EnumMap<StandardMessage, Function<MessageWriter, TextComponent>> standardMessages;
private static EnumMap<StandardMessage, Function<MessageBuilder, TextComponent>> standardMessages;
private OutputManager(ConfigHandler config) {
adventure = Main.adventure();
shareManager = ShareManager.getInstance(config);
public OutputManager(BukkitAudiences adventure, ConfigHandler config, ShareManager shareManager) {
OutputManager.adventure = adventure;
OutputManager.config = config;
OutputManager.shareManager = shareManager;
getMessageWriters(config);
getMessageBuilders();
prepareFunctions();
}
public static OutputManager getInstance(ConfigHandler config) {
OutputManager outputManager = instance;
if (outputManager != null) {
return outputManager;
}
synchronized (OutputManager.class) {
if (instance == null) {
instance = new OutputManager(config);
}
return instance;
}
public static void updateMessageBuilders() {
getMessageBuilders();
}
public void updateMessageWriters(ConfigHandler config) {
getMessageWriters(config);
@Override
public TextComponent getPluginPrefix() {
ComponentFactory factory = new ComponentFactory(config);
return factory.pluginPrefix();
}
public void sendFeedbackMsg(CommandSender sender, StandardMessage message) {
@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) {
BiFunction<Integer, CommandSender, TextComponent> playerStatFunction =
getMessageBuilder(statRequest).formattedPlayerStatFunction(playerStat, statRequest);
return processFunction(statRequest.getCommandSender(), playerStatFunction);
}
@Override
public TextComponent formatServerStat(@NotNull StatRequest statRequest, long serverStat) {
BiFunction<Integer, CommandSender, TextComponent> serverStatFunction =
getMessageBuilder(statRequest).formattedServerStatFunction(serverStat, statRequest);
return processFunction(statRequest.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) {
BiFunction<Integer, CommandSender, TextComponent> topStatFunction =
getMessageBuilder(statRequest).formattedTopStatFunction(topStats, statRequest);
return processFunction(statRequest.getCommandSender(), topStatFunction);
}
public void sendFeedbackMsg(@NotNull CommandSender sender, StandardMessage message) {
if (message != null) {
adventure.sender(sender).sendMessage(standardMessages.get(message)
.apply(getWriter(sender)));
.apply(getMessageBuilder(sender)));
}
}
public void sendFeedbackMsgWaitAMoment(CommandSender sender, boolean longWait) {
adventure.sender(sender).sendMessage(getWriter(sender)
public void sendFeedbackMsgWaitAMoment(@NotNull CommandSender sender, boolean longWait) {
adventure.sender(sender).sendMessage(getMessageBuilder(sender)
.waitAMoment(longWait));
}
public void sendFeedbackMsgMissingSubStat(CommandSender sender, Statistic.Type statType) {
adventure.sender(sender).sendMessage(getWriter(sender)
public void sendFeedbackMsgMissingSubStat(@NotNull CommandSender sender, Statistic.Type statType) {
adventure.sender(sender).sendMessage(getMessageBuilder(sender)
.missingSubStatName(statType));
}
public void sendFeedbackMsgWrongSubStat(CommandSender sender, Statistic.Type statType, String subStatName) {
public void sendFeedbackMsgWrongSubStat(@NotNull CommandSender sender, Statistic.Type statType, @Nullable String subStatName) {
if (subStatName == null) {
sendFeedbackMsgMissingSubStat(sender, statType);
} else {
adventure.sender(sender).sendMessage(getWriter(sender)
adventure.sender(sender).sendMessage(getMessageBuilder(sender)
.wrongSubStatType(statType, subStatName));
}
}
public void sendExamples(CommandSender sender) {
adventure.sender(sender).sendMessage(getWriter(sender)
public void sendExamples(@NotNull CommandSender sender) {
adventure.sender(sender).sendMessage(getMessageBuilder(sender)
.usageExamples());
}
public void sendHelp(CommandSender sender) {
adventure.sender(sender).sendMessage(getWriter(sender)
.helpMsg(sender instanceof ConsoleCommandSender));
public void sendHelp(@NotNull CommandSender sender) {
adventure.sender(sender).sendMessage(getMessageBuilder(sender)
.helpMsg());
}
public void shareStatResults(@NotNull TextComponent statResult) {
adventure.players().sendMessage(statResult);
public void sendToAllPlayers(@NotNull TextComponent component) {
adventure.players().sendMessage(component);
}
public void sendPlayerStat(@NotNull StatRequest request, int playerStat) {
CommandSender sender = request.getCommandSender();
BiFunction<UUID, CommandSender, TextComponent> buildFunction =
getWriter(sender).formattedPlayerStatFunction(playerStat, request);
processAndSend(sender, buildFunction);
public void sendToCommandSender(@NotNull CommandSender sender, @NotNull TextComponent component) {
adventure.sender(sender).sendMessage(component);
}
public void sendServerStat(@NotNull StatRequest request, long serverStat) {
CommandSender sender = request.getCommandSender();
BiFunction<UUID, CommandSender, TextComponent> buildFunction =
getWriter(sender).formattedServerStatFunction(serverStat, request);
private TextComponent processFunction(CommandSender sender, @NotNull BiFunction<Integer, CommandSender, TextComponent> statResultFunction) {
boolean saveOutput = !(sender instanceof ConsoleCommandSender) &&
ShareManager.isEnabled() &&
shareManager.senderHasPermission(sender);
processAndSend(sender, buildFunction);
}
public void sendTopStat(@NotNull StatRequest request, LinkedHashMap<String, Integer> topStats) {
CommandSender sender = request.getCommandSender();
BiFunction<UUID, CommandSender, TextComponent> buildFunction =
getWriter(sender).formattedTopStatFunction(topStats, request);
processAndSend(sender, buildFunction);
}
private void processAndSend(CommandSender sender, BiFunction<UUID, CommandSender, TextComponent> buildFunction) {
if (shareManager.isEnabled() && shareManager.senderHasPermission(sender)) {
UUID shareCode = shareManager.saveStatResult(sender.getName(), buildFunction.apply(null, sender));
adventure.sender(sender).sendMessage(
buildFunction.apply(shareCode, null));
if (saveOutput) {
int shareCode =
shareManager.saveStatResult(sender.getName(), statResultFunction.apply(null, sender));
return statResultFunction.apply(shareCode, null);
}
else {
adventure.sender(sender).sendMessage(
buildFunction.apply(null, null));
return statResultFunction.apply(null, null);
}
}
private MessageWriter getWriter(CommandSender sender) {
return sender instanceof ConsoleCommandSender ? consoleWriter : writer;
private MessageBuilder getMessageBuilder(CommandSender sender) {
return sender instanceof ConsoleCommandSender ? consoleMessageBuilder : messageBuilder;
}
private void getMessageWriters(ConfigHandler config) {
boolean isBukkit = Bukkit.getName().equalsIgnoreCase("CraftBukkit");
if (config.useRainbowMode() ||
(config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE))) {
writer = MessageWriter.fromComponentFactory(config, new PrideComponentFactory(config));
}
else {
writer = MessageWriter.defaultWriter(config);
}
if (!isBukkit) {
consoleWriter = writer;
private MessageBuilder getMessageBuilder(StatRequest statRequest) {
if (statRequest.isAPIRequest() || !statRequest.isConsoleSender()) {
return messageBuilder;
} else {
consoleWriter = MessageWriter.fromComponentFactory(config, new BukkitConsoleComponentFactory(config));
return consoleMessageBuilder;
}
}
private static void getMessageBuilders() {
messageBuilder = getClientMessageBuilder();
consoleMessageBuilder = getConsoleMessageBuilder();
}
private static MessageBuilder getClientMessageBuilder() {
if (useRainbowStyle()) {
return MessageBuilder.fromComponentFactory(config, new PrideComponentFactory(config));
}
return MessageBuilder.defaultBuilder(config);
}
private static MessageBuilder getConsoleMessageBuilder() {
MessageBuilder consoleBuilder;
if (isBukkit()) {
consoleBuilder = MessageBuilder.fromComponentFactory(config, new BukkitConsoleComponentFactory(config));
} else {
consoleBuilder = getClientMessageBuilder();
}
consoleBuilder.setConsoleBuilder(true);
consoleBuilder.toggleHoverUse(false);
return consoleBuilder;
}
private static boolean useRainbowStyle() {
return config.useRainbowMode() || (config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE));
}
private static boolean isBukkit() {
return Bukkit.getName().equalsIgnoreCase("CraftBukkit");
}
private void prepareFunctions() {
standardMessages = new EnumMap<>(StandardMessage.class);
standardMessages.put(RELOADED_CONFIG, (MessageWriter::reloadedConfig));
standardMessages.put(STILL_RELOADING, (MessageWriter::stillReloading));
standardMessages.put(MISSING_STAT_NAME, (MessageWriter::missingStatName));
standardMessages.put(MISSING_PLAYER_NAME, (MessageWriter::missingPlayerName));
standardMessages.put(REQUEST_ALREADY_RUNNING, (MessageWriter::requestAlreadyRunning));
standardMessages.put(STILL_ON_SHARE_COOLDOWN, (MessageWriter::stillOnShareCoolDown));
standardMessages.put(RESULTS_ALREADY_SHARED, (MessageWriter::resultsAlreadyShared));
standardMessages.put(STAT_RESULTS_TOO_OLD, (MessageWriter::statResultsTooOld));
standardMessages.put(UNKNOWN_ERROR, (MessageWriter::unknownError));
standardMessages.put(RELOADED_CONFIG, (MessageBuilder::reloadedConfig));
standardMessages.put(STILL_RELOADING, (MessageBuilder::stillReloading));
standardMessages.put(MISSING_STAT_NAME, (MessageBuilder::missingStatName));
standardMessages.put(MISSING_PLAYER_NAME, (MessageBuilder::missingPlayerName));
standardMessages.put(REQUEST_ALREADY_RUNNING, (MessageBuilder::requestAlreadyRunning));
standardMessages.put(STILL_ON_SHARE_COOLDOWN, (MessageBuilder::stillOnShareCoolDown));
standardMessages.put(RESULTS_ALREADY_SHARED, (MessageBuilder::resultsAlreadyShared));
standardMessages.put(STAT_RESULTS_TOO_OLD, (MessageBuilder::statResultsTooOld));
standardMessages.put(UNKNOWN_ERROR, (MessageBuilder::unknownError));
}
}

View File

@ -11,6 +11,8 @@ 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.*/
public class BukkitConsoleComponentFactory extends ComponentFactory {
public BukkitConsoleComponentFactory(ConfigHandler config) {
@ -22,6 +24,7 @@ public class BukkitConsoleComponentFactory extends ComponentFactory {
PREFIX = PluginColor.GOLD.getConsoleColor();
BRACKETS = PluginColor.GRAY.getConsoleColor();
UNDERSCORE = PluginColor.DARK_PURPLE.getConsoleColor();
HEARTS = PluginColor.RED.getConsoleColor();
MSG_MAIN = PluginColor.MEDIUM_BLUE.getConsoleColor();
MSG_ACCENT = PluginColor.BLUE.getConsoleColor();

View File

@ -4,6 +4,8 @@ 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 net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
@ -18,15 +20,11 @@ import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
import static net.kyori.adventure.text.Component.*;
import static net.kyori.adventure.text.Component.text;
/** Creates Components with the desired formatting. This class can put Strings
into formatted Components with TextColor and TextDecoration, or return empty Components
or ComponentBuilders 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.*/
public class ComponentFactory {
private static ConfigHandler config;
@ -34,6 +32,7 @@ public class ComponentFactory {
protected TextColor PREFIX; //gold
protected TextColor BRACKETS; //gray
protected TextColor UNDERSCORE; //dark_purple
protected TextColor HEARTS; //red
protected TextColor MSG_MAIN; //medium_blue
protected TextColor MSG_ACCENT; //blue
@ -56,6 +55,7 @@ public class ComponentFactory {
PREFIX = PluginColor.GOLD.getColor();
BRACKETS = PluginColor.GRAY.getColor();
UNDERSCORE = PluginColor.DARK_PURPLE.getColor();
HEARTS = PluginColor.RED.getColor();
MSG_MAIN = PluginColor.MEDIUM_BLUE.getColor();
MSG_ACCENT = PluginColor.BLUE.getColor();
@ -109,10 +109,10 @@ public class ComponentFactory {
return text().color(MSG_ACCENT).build();
}
public TextComponent title(String content, Target selection) {
public TextComponent title(String content, Target target) {
return getComponent(content,
getColorFromString(config.getTitleDecoration(selection, false)),
getStyleFromString(config.getTitleDecoration(selection, true)));
getColorFromString(config.getTitleDecoration(target, false)),
getStyleFromString(config.getTitleDecoration(target, true)));
}
public TextComponent titleNumber(int number) {
@ -121,8 +121,8 @@ public class ComponentFactory {
getStyleFromString(config.getTitleNumberDecoration(true)));
}
public TextComponent rankNumber(String number) {
return getComponent(number,
public TextComponent rankNumber(int number) {
return getComponent(number + ".",
getColorFromString(config.getRankNumberDecoration(false)),
getStyleFromString(config.getRankNumberDecoration(true)));
}
@ -141,10 +141,10 @@ public class ComponentFactory {
.append(colon);
}
public TextComponent playerName(String playerName, Target selection) {
public TextComponent playerName(String playerName, Target target) {
return getComponent(playerName,
getColorFromString(config.getPlayerNameDecoration(selection, false)),
getStyleFromString(config.getPlayerNameDecoration(selection, true)));
getColorFromString(config.getPlayerNameDecoration(target, false)),
getStyleFromString(config.getPlayerNameDecoration(target, true)));
}
public TextComponent sharerName(String sharerName) {
@ -153,8 +153,8 @@ public class ComponentFactory {
getStyleFromString(config.getSharerNameDecoration(true)));
}
public TextComponent shareButton(UUID shareCode) {
return surroundingBrackets(
public TextComponent shareButton(int shareCode) {
return surroundWithBrackets(
text("Share")
.color(MSG_HOVER)
.clickEvent(ClickEvent.runCommand("/statshare " + shareCode))
@ -163,7 +163,7 @@ public class ComponentFactory {
}
public TextComponent sharedByMessage(Component playerName) {
return surroundingBrackets(
return surroundWithBrackets(
text().append(
getComponent("Shared by",
getColorFromString(config.getSharedByTextDecoration(false)),
@ -174,7 +174,7 @@ public class ComponentFactory {
}
public TextComponent statResultInHoverText(TextComponent statResult) {
return surroundingBrackets(
return surroundWithBrackets(
text().append(text("Hover Here")
.color(MSG_CLICKED)
.decorate(TextDecoration.ITALIC)
@ -184,31 +184,31 @@ public class ComponentFactory {
/** @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 selection) {
public TextComponent statAndSubStatName(String prettyStatName, @Nullable String prettySubStatName, Target target) {
TextComponent.Builder totalStatNameBuilder = getComponentBuilder(prettyStatName,
getColorFromString(config.getStatNameDecoration(selection, false)),
getStyleFromString(config.getStatNameDecoration(selection, true)));
TextComponent subStat = subStatName(prettySubStatName, selection);
getColorFromString(config.getStatNameDecoration(target, false)),
getStyleFromString(config.getStatNameDecoration(target, true)));
TextComponent subStat = subStatName(prettySubStatName, target);
if (!subStat.equals(Component.empty())) {
totalStatNameBuilder
.append(space().decorations(TextDecoration.NAMES.values(), false))
.append(subStatName(prettySubStatName, selection));
.append(subStatName(prettySubStatName, target));
}
return totalStatNameBuilder.build();
}
/** Returns a TextComponent with TranslatableComponent as a child.*/
public TextComponent statAndSubStatNameTranslatable(String statKey, String subStatKey, Target selection) {
public TextComponent statAndSubStatNameTranslatable(String statKey, String subStatKey, Target target) {
TextComponent.Builder totalStatNameBuilder = getComponentBuilder(null,
getColorFromString(config.getStatNameDecoration(selection, false)),
getStyleFromString(config.getStatNameDecoration(selection, true)));
getColorFromString(config.getStatNameDecoration(target, false)),
getStyleFromString(config.getStatNameDecoration(target, true)));
TextComponent subStat = subStatNameTranslatable(subStatKey, selection);
if (statKey.equalsIgnoreCase("stat_type.minecraft.killed")) {
TextComponent subStat = subStatNameTranslatable(subStatKey, target);
if (LanguageKeyHandler.isKeyForKillEntity(statKey)) {
return totalStatNameBuilder.append(killEntityBuilder(subStat)).build();
}
else if (statKey.equalsIgnoreCase("stat_type.minecraft.killed_by")) {
else if (LanguageKeyHandler.isKeyForEntityKilledBy(statKey)) {
return totalStatNameBuilder.append(entityKilledByBuilder(subStat)).build();
}
else {
@ -222,63 +222,95 @@ public class ComponentFactory {
}
}
public TextComponent statNumber(String prettyNumber, Target selection) {
public TextComponent statNumber(String prettyNumber, Target target) {
return getComponent(prettyNumber,
getColorFromString(config.getStatNumberDecoration(selection, false)),
getStyleFromString(config.getStatNumberDecoration(selection, true)));
getColorFromString(config.getStatNumberDecoration(target, false)),
getStyleFromString(config.getStatNumberDecoration(target, true)));
}
public TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber, @Nullable String hoverUnitName, @Nullable String hoverUnitKey, Target selection) {
return statNumberWithHoverText(mainNumber, hoverNumber, hoverUnitName, hoverUnitKey, null, selection);
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 damageNumberWithHoverText(String mainNumber, String hoverNumber, TextComponent heart, Target selection) {
return statNumberWithHoverText(mainNumber, hoverNumber, null, null, heart, selection);
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);
}
public TextComponent statUnit(String unitName, String unitKey, Target selection) {
if (!(unitName == null && unitKey == null)) {
TextComponent.Builder statUnitBuilder = getComponentBuilder(null,
getColorFromString(config.getSubStatNameDecoration(selection, false)),
getStyleFromString(config.getSubStatNameDecoration(selection, true)));
if (unitKey != null) {
statUnitBuilder.append(translatable()
.key(unitKey));
} else {
statUnitBuilder.append(text(unitName));
}
return surroundingBrackets(statUnitBuilder.build());
}
else {
return Component.empty();
}
public TextComponent damageNumberWithHeartUnitInHoverText(String mainNumber, String hoverNumber, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, null, null, clientHeart(true), target);
}
public TextComponent heart(boolean isConsoleSender, boolean isHoverUnit) {
TextColor heartColor = TextColor.fromHexString("#FF1313");
char heart = isConsoleSender ? '\u2665' : '\u2764';
if (isHoverUnit) {
return Component.text(heart).color(heartColor);
public TextComponent distanceNumber(String prettyNumber, Target target) {
return statNumber(prettyNumber, target);
}
public TextComponent distanceNumberWithHoverText(String mainNumber, String hoverNumber, String hoverUnitName, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, hoverUnitName, null, target);
}
public TextComponent distanceNumberWithTranslatableHoverText(String mainNumber, String hoverNumber, String hoverUnitKey, Target target) {
return statNumberWithHoverText(mainNumber, hoverNumber, null, hoverUnitKey, target);
}
public TextComponent statUnit(String unitName, Target target) {
TextComponent statUnit = getComponentBuilder(unitName,
getColorFromString(config.getSubStatNameDecoration(target, false)),
getStyleFromString(config.getSubStatNameDecoration(target, true)))
.build();
return surroundWithBrackets(statUnit);
}
public TextComponent statUnitTranslatable(String unitKey, Target target) {
TextComponent statUnit = getComponentBuilder(null,
getColorFromString(config.getSubStatNameDecoration(target, false)),
getStyleFromString(config.getSubStatNameDecoration(target, true)))
.append(translatable()
.key(unitKey))
.build();
return surroundWithBrackets(statUnit);
}
public TextComponent clientHeart(boolean isDisplayedInHoverText) {
TextComponent basicHeartComponent = basicHeartComponent('\u2764');
if (isDisplayedInHoverText) {
return basicHeartComponent;
}
TextComponent.Builder heartComponent = Component.text()
.content(String.valueOf(heart))
.color(heartColor);
if (config.useHoverText()) {
heartComponent.hoverEvent(HoverEvent.showText(
text(Unit.HEART.getLabel())
.color(MSG_HOVER_ACCENT)));
}
return surroundingBrackets(heartComponent.build());
return surroundWithBrackets(basicHeartComponent);
}
public TextComponent clientHeartWithHoverText() {
TextComponent basicHeartComponent = basicHeartComponent('\u2764')
.toBuilder()
.hoverEvent(HoverEvent.showText(
text(Unit.HEART.getLabel())
.color(MSG_HOVER_ACCENT)))
.build();
return surroundWithBrackets(basicHeartComponent);
}
public TextComponent consoleHeart() {
return surroundWithBrackets(basicHeartComponent('\u2665'));
}
//console can do u2665, u2764 looks better in-game
private TextComponent basicHeartComponent(char heartChar) {
return Component.text()
.content(String.valueOf(heartChar))
.color(HEARTS)
.build();
}
/** Returns a TextComponent for the subStatName, or an empty component.*/
private TextComponent subStatName(@Nullable String prettySubStatName, Target selection) {
private TextComponent subStatName(@Nullable String prettySubStatName, Target target) {
if (prettySubStatName == null) {
return Component.empty();
} else {
return getComponentBuilder(null,
getColorFromString(config.getSubStatNameDecoration(selection, false)),
getStyleFromString(config.getSubStatNameDecoration(selection, true)))
getColorFromString(config.getSubStatNameDecoration(target, false)),
getStyleFromString(config.getSubStatNameDecoration(target, true)))
.append(text("("))
.append(text(prettySubStatName))
.append(text(")"))
@ -287,11 +319,11 @@ public class ComponentFactory {
}
/** Returns a TranslatableComponent for the subStatName, or an empty component.*/
private TextComponent subStatNameTranslatable(String subStatKey, Target selection) {
private TextComponent subStatNameTranslatable(String subStatKey, Target target) {
if (subStatKey != null) {
return getComponentBuilder(null,
getColorFromString(config.getSubStatNameDecoration(selection, false)),
getStyleFromString(config.getSubStatNameDecoration(selection, true)))
getColorFromString(config.getSubStatNameDecoration(target, false)),
getStyleFromString(config.getSubStatNameDecoration(target, true)))
.append(text("("))
.append(translatable()
.key(subStatKey))
@ -305,7 +337,7 @@ public class ComponentFactory {
@return a TranslatableComponent Builder with the subStat Component as args.*/
private TranslatableComponent.Builder killEntityBuilder(@NotNull TextComponent subStat) {
return translatable()
.key("commands.kill.success.single") //"Killed %s"
.key(LanguageKeyHandler.getAlternativeKeyForKillEntity()) //"Killed %s"
.args(subStat);
}
@ -315,21 +347,21 @@ public class ComponentFactory {
with book.byAuthor as key and the subStat Component as args.*/
private TranslatableComponent.Builder entityKilledByBuilder(@NotNull TextComponent subStat) {
return translatable()
.key("stat.minecraft.deaths") //"Number of Deaths"
.key(LanguageKeyHandler.getAlternativeKeyForEntityKilledBy()) //"Number of Deaths"
.append(space())
.append(translatable()
.key("book.byAuthor") //"by %s"
.key(LanguageKeyHandler.getAlternativeKeyForEntityKilledByArg()) //"by %s"
.args(subStat));
}
private TextComponent statNumberWithHoverText(String mainNumber, String hoverNumber, @Nullable String hoverUnitName, @Nullable String hoverUnitKey, @Nullable TextComponent heart, Target selection) {
TextColor baseColor = getColorFromString(config.getStatNumberDecoration(selection, false));
TextDecoration style = getStyleFromString(config.getStatNumberDecoration(selection, true));
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));
TextComponent.Builder hoverText = getComponentBuilder(hoverNumber, getLighterColor(baseColor), style);
if (heart != null) {
if (heartComponent != null) {
hoverText.append(space())
.append(heart);
.append(heartComponent);
}
else if (hoverUnitKey != null) {
hoverText.append(space())
@ -342,7 +374,7 @@ public class ComponentFactory {
return getComponent(mainNumber, baseColor, style).hoverEvent(HoverEvent.showText(hoverText));
}
private TextComponent surroundingBrackets(TextComponent component) {
private TextComponent surroundWithBrackets(TextComponent component) {
return getComponent(null, BRACKETS, null)
.append(text("["))
.append(component)

View File

@ -0,0 +1,76 @@
package com.gmail.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 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.*/
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. */
public static LegacyComponentSerializer getTranslatableComponentSerializer() {
LegacyComponentSerializer serializer = getTextComponentSerializer();
ComponentFlattener flattener = ComponentFlattener.basic().toBuilder()
.mapper(TranslatableComponent.class, trans -> {
StringBuilder totalPrettyName = new StringBuilder();
if (LanguageKeyHandler.isKeyForEntityKilledByArg(trans.key())) {
return "";
}
else if (LanguageKeyHandler.isKeyForEntityKilledBy(trans.key()) ||
LanguageKeyHandler.isKeyForKillEntity(trans.key())) {
TextComponent.Builder temp = Component.text();
trans.iterator(ComponentIteratorType.DEPTH_FIRST, ComponentIteratorFlag.INCLUDE_TRANSLATABLE_COMPONENT_ARGUMENTS)
.forEachRemaining(component -> {
if (component instanceof TextComponent text) {
if (!text.children().isEmpty()) {
text.iterator(ComponentIteratorType.DEPTH_FIRST).forEachRemaining(component1 -> {
if (component1 instanceof TextComponent text1 && text1.content().contains("(")) {
temp.style(text.style()).color(text.color());
}
});
}
} 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(")")));
totalPrettyName.append(
serializer.serialize(temp.build()));
} else if (!LanguageKeyHandler.isKeyForEntityKilledByArg(translatable.key())) {
totalPrettyName.append(
StringUtils.prettify(
LanguageKeyHandler.convertToName(
translatable.key())));
}
}
});
}
else {
return StringUtils.prettify(
LanguageKeyHandler.convertToName(
trans.key()));
}
return totalPrettyName.toString();
})
.build();
return serializer.toBuilder().flattener(flattener).build();
}
private static LegacyComponentSerializer getTextComponentSerializer() {
return LegacyComponentSerializer
.builder()
.hexColors()
.useUnusualXRepeatedCharacterHexFormat()
.build();
}
}

View File

@ -11,6 +11,7 @@ import java.util.List;
import static net.kyori.adventure.text.Component.text;
/** A fully constructed message with examples on how to use PlayerStats.*/
public final class ExampleMessage implements TextComponent {
private final TextComponent exampleMessage;

View File

@ -13,6 +13,7 @@ import java.util.List;
import static net.kyori.adventure.text.Component.text;
/** The help message that explains how to use PlayerStats.*/
public final class HelpMessage implements TextComponent {
private final TextComponent helpMessage;

View File

@ -11,6 +11,7 @@ import java.util.Random;
import static net.kyori.adventure.text.Component.*;
/** A festive version of the {@link ComponentFactory}*/
public class PrideComponentFactory extends ComponentFactory {
public PrideComponentFactory(ConfigHandler c) {
@ -22,6 +23,7 @@ public class PrideComponentFactory extends ComponentFactory {
PREFIX = PluginColor.GOLD.getColor();
BRACKETS = PluginColor.GRAY.getColor();
UNDERSCORE = PluginColor.DARK_PURPLE.getColor();
HEARTS = PluginColor.RED.getColor();
MSG_MAIN = PluginColor.GRAY.getColor(); //difference 1
MSG_ACCENT = PluginColor.LIGHT_GOLD.getColor(); //difference 2

View File

@ -1,12 +1,18 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
import me.clip.placeholderapi.PlaceholderAPI;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.Tag;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.Random;
/**This class is just for fun, and adds some silly names for players on my server.
/**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 {
@ -96,7 +102,7 @@ public final class EasterEggProvider {
if (playerName == null) {
return null;
} else {
return MiniMessage.miniMessage().deserialize(playerName);
return MiniMessage.miniMessage().deserialize(playerName, papiTag(player));
}
}
@ -107,4 +113,16 @@ public final class EasterEggProvider {
private static boolean sillyNumberIsBetween(int sillyNumber, int lowerBound, int upperBound) {
return sillyNumber >= lowerBound && sillyNumber <= upperBound;
}
private static TagResolver papiTag(final @NotNull Player player) {
return TagResolver.resolver("papi", (argumentQueue, context) -> {
final String papiPlaceholder = argumentQueue.popOr("papi tag requires an argument").value();
final String parsedPlaceholder = PlaceholderAPI.setPlaceholders(player, '%' + papiPlaceholder + '%');
TextComponent componentPlaceholder = LegacyComponentSerializer.legacyAmpersand().deserialize(parsedPlaceholder);
if (!componentPlaceholder.content().isEmpty()) {
componentPlaceholder = componentPlaceholder.toBuilder().append(Component.space()).build();
}
return Tag.selfClosingInserting(componentPlaceholder);
});
}
}

View File

@ -2,6 +2,7 @@ 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() {

View File

@ -11,6 +11,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.HashMap;
/** A utility class that provides language keys to be put in a TranslatableComponent.*/
public final class LanguageKeyHandler {
private static HashMap<Statistic, String> statNameKeys;
@ -19,6 +20,74 @@ public final class LanguageKeyHandler {
statNameKeys = generateStatNameKeys();
}
/** Checks if a given Key is the language key "stat_type.minecraft.killed"
or "commands.kill.success.single" (which results in "Killed %s").*/
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" */
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").*/
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()})*/
public static String getAlternativeKeyForEntityKilledBy() {
return "stat.minecraft.deaths";
}
/** Checks if a given Key is the language key "book.byAuthor"
(which results in "by %s"). */
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"*/
public static String getAlternativeKeyForEntityKilledByArg() {
return "book.byAuthor";
}
public static String convertToName(String key) {
if (key.equalsIgnoreCase("soundCategory.block")) {
return Unit.BLOCK.getLabel();
} else if (isKeyForKillEntity(key)) {
return "times_killed";
} else if (isKeyForEntityKilledBy(key)) {
return "number_of_times_killed_by";
} else if (isKeyForEntityKilledByArg(key)) { //this one returns nothing, because the previous one returns the full text
return "";
}
String toReplace = "";
if (key.contains("stat")) {
if (key.contains("type")) {
toReplace = "stat_type";
} else {
toReplace = "stat";
}
} else if (key.contains("entity")) { //for the two entity-related ones, put brackets around it to make up for the multiple-keys/args-serializer issues
toReplace = "entity";
} else if (key.contains("block")) {
toReplace = "block";
} else if (key.contains("item")) {
toReplace = "item";
}
toReplace = toReplace + ".minecraft.";
return key.replace(toReplace, "");
}
public String getStatKey(@NotNull Statistic statistic) {
if (statistic.getType() == Statistic.Type.UNTYPED) {
return "stat.minecraft." + statNameKeys.get(statistic);
@ -33,7 +102,7 @@ public final class LanguageKeyHandler {
public @Nullable String getEntityKey(EntityType entity) {
if (entity == null || entity == EntityType.UNKNOWN) return null;
else {
return "entity.minecraft." + entity.getKey().getKey();
return "entity.minecraft." + entity.getKey().getKey();
}
}

View File

@ -4,6 +4,9 @@ import com.gmail.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.*/
public final class NumberFormatter {
private final DecimalFormat format;
@ -17,25 +20,13 @@ public final class NumberFormatter {
/** Turns the input number into a more readable format depending on its type
(number-of-times, time-, damage- or distance-based) according to the
corresponding config settings, and adds commas in groups of 3.*/
public String format(long number, Unit statUnit) {
return format(number, statUnit, null);
}
public String format(long number, Unit statUnit, Unit smallTimeUnit) {
if (smallTimeUnit == null) {
return switch (statUnit.getType()) {
case DISTANCE -> formatDistance(number, statUnit);
case DAMAGE -> formatDamage(number, statUnit);
default -> format.format(number);
};
} else {
return formatTime(number, statUnit, smallTimeUnit);
}
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. */
private String formatDamage(long number, Unit statUnit) { //7 statistics
public String formatDamageNumber(long number, Unit statUnit) { //7 statistics
if (statUnit == Unit.HEART) {
return format.format(Math.round(number / 2.0));
} else {
@ -45,7 +36,7 @@ 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. */
private String formatDistance(long number, Unit statUnit) { //15 statistics
public String formatDistanceNumber(long number, Unit statUnit) { //15 statistics
switch (statUnit) {
case CM -> {
return format.format(number);
@ -63,7 +54,7 @@ public final class NumberFormatter {
}
/** The unit of time-based statistics is ticks by default.*/
private String formatTime(long number, Unit bigUnit, Unit smallUnit) { //5 statistics
public String formatTimeNumber(long number, Unit bigUnit, Unit smallUnit) { //5 statistics
if (number == 0) {
return "-";
}

View File

@ -2,6 +2,7 @@ package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
/** A small utility class that helps make enum constant names prettier for output in stat-messages.*/
public final class StringUtils {
private StringUtils() {

View File

@ -9,7 +9,8 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
public final class ReloadAction extends RecursiveAction {
/** The action that is executed when a reload-command is triggered. */
final class ReloadAction extends RecursiveAction {
private static int threshold;

View File

@ -7,6 +7,7 @@ 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.StatManager;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.Bukkit;
@ -20,26 +21,20 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Predicate;
public class ReloadThread extends Thread {
/** The Thread that is in charge of reloading PlayerStats. */
public final class ReloadThread extends Thread {
private static ConfigHandler config;
private static OutputManager messageSender;
private final OfflinePlayerHandler offlinePlayerHandler;
private static ShareManager shareManager;
private static OutputManager outputManager;
private final int reloadThreadID;
private final StatThread statThread;
private final CommandSender sender;
public ReloadThread(ConfigHandler c, OutputManager m, OfflinePlayerHandler o, int ID, @Nullable StatThread s, @Nullable CommandSender se) {
public ReloadThread(ConfigHandler c, OutputManager m, int ID, @Nullable StatThread s, @Nullable CommandSender se) {
config = c;
messageSender = m;
offlinePlayerHandler = o;
shareManager = ShareManager.getInstance(c);
outputManager = m;
reloadThreadID = ID;
statThread = s;
@ -49,6 +44,11 @@ public class ReloadThread extends Thread {
MyLogger.threadCreated(this.getName());
}
/** 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 StatManager},
and update the MessageBuilders in the {@link OutputManager}.*/
@Override
public void run() {
long time = System.currentTimeMillis();
@ -69,21 +69,21 @@ public class ReloadThread extends Thread {
reloadEverything();
if (sender != null) {
messageSender.sendFeedbackMsg(sender, StandardMessage.RELOADED_CONFIG);
outputManager.sendFeedbackMsg(sender, StandardMessage.RELOADED_CONFIG);
}
}
else { //during first start-up
MyLogger.setDebugLevel(config.getDebugLevel());
offlinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
OfflinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
}
}
private void reloadEverything() {
MyLogger.setDebugLevel(config.getDebugLevel());
messageSender.updateMessageWriters(config);
offlinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
shareManager.updateSettings(config);
OutputManager.updateMessageBuilders();
OfflinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
ShareManager.updateSettings(config);
}
private ConcurrentHashMap<String, UUID> loadOfflinePlayers() {
@ -125,7 +125,7 @@ public class ReloadThread extends Thread {
MyLogger.actionFinished(1);
MyLogger.logTimeTaken("ReloadThread",
("loaded " + playerMap.size() + " offline players"), time);
("loaded " + playerMap.size() + " offline players"), time, DebugLevel.LOW);
return playerMap;
}
}

View File

@ -1,7 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.models.StatRequest;
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;
@ -9,17 +9,17 @@ import org.bukkit.OfflinePlayer;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.RecursiveTask;
public final class StatAction extends RecursiveAction {
/** 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 request;
private final ConcurrentHashMap<String, Integer> playerStats;
private final StatRequest statRequest;
private final ConcurrentHashMap<String, Integer> allStats;
/**
* Gets the statistic numbers for all players whose name is on the list, puts them in a ConcurrentHashMap
@ -27,34 +27,36 @@ public final class StatAction extends RecursiveAction {
* @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 playerStats the ConcurrentHashMap to put the results on
* @param allStats the ConcurrentHashMap to put the results on
*/
public StatAction(OfflinePlayerHandler offlinePlayerHandler, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> playerStats) {
public StatAction(OfflinePlayerHandler offlinePlayerHandler, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> allStats) {
threshold = ThreadManager.getTaskThreshold();
this.offlinePlayerHandler = offlinePlayerHandler;
this.playerNames = playerNames;
this.request = statRequest;
this.playerStats = playerStats;
this.statRequest = statRequest;
this.allStats = allStats;
MyLogger.subActionCreated(Thread.currentThread().getName());
}
@Override
protected void compute() {
protected ConcurrentHashMap<String, Integer> compute() {
if (playerNames.size() < threshold) {
getStatsDirectly();
return getStatsDirectly();
}
else {
final StatAction subTask1 = new StatAction(offlinePlayerHandler, playerNames.subList(0, playerNames.size()/2), request, playerStats);
final StatAction subTask2 = new StatAction(offlinePlayerHandler, playerNames.subList(playerNames.size()/2, playerNames.size()), request, playerStats);
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);
//queue and compute all subtasks in the right order
invokeAll(subTask1, subTask2);
subTask1.fork();
subTask2.compute();
return subTask1.join();
}
}
private void getStatsDirectly() {
private ConcurrentHashMap<String, Integer> getStatsDirectly() {
Iterator<String> iterator = playerNames.iterator();
if (iterator.hasNext()) {
do {
@ -63,17 +65,18 @@ public final class StatAction extends RecursiveAction {
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
int statistic = 0;
switch (request.getStatistic().getType()) {
case UNTYPED -> statistic = player.getStatistic(request.getStatistic());
case ENTITY -> statistic = player.getStatistic(request.getStatistic(), request.getEntity());
case BLOCK -> statistic = player.getStatistic(request.getStatistic(), request.getBlock());
case ITEM -> statistic = player.getStatistic(request.getStatistic(), request.getItem());
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) {
playerStats.put(playerName, statistic);
allStats.put(playerName, statistic);
}
}
} while (iterator.hasNext());
}
return allStats;
}
}

View File

@ -0,0 +1,93 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.api.StatCalculator;
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 StatManager implements StatCalculator {
private final OfflinePlayerHandler offlinePlayerHandler;
public StatManager(OfflinePlayerHandler offlinePlayerHandler) {
this.offlinePlayerHandler = offlinePlayerHandler;
}
/** Gets the statistic data for an individual player. If somehow the player
cannot be found, this returns 0.*/
@Override
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;
}
@Override
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));
}
@Override
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

@ -2,41 +2,34 @@ 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.models.StatRequest;
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.config.ConfigHandler;
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 net.kyori.adventure.text.TextComponent;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
public class StatThread extends Thread {
/** The Thread that is in charge of getting and calculating statistics.*/
public final class StatThread extends Thread {
private static ConfigHandler config;
private final OutputManager outputManager;
private final OfflinePlayerHandler offlinePlayerHandler;
private static OutputManager outputManager;
private static StatManager statManager;
private final ReloadThread reloadThread;
private final StatRequest request;
private final StatRequest statRequest;
public StatThread(ConfigHandler c, OutputManager m, OfflinePlayerHandler o, int ID, StatRequest s, @Nullable ReloadThread r) {
config = c;
public StatThread(OutputManager m, StatManager t, int ID, StatRequest s, @Nullable ReloadThread r) {
outputManager = m;
offlinePlayerHandler = o;
statManager = t;
reloadThread = r;
request = s;
statRequest = s;
this.setName("StatThread-" + request.getCommandSender().getName() + "-" + ID);
this.setName("StatThread-" + statRequest.getCommandSender().getName() + "-" + ID);
MyLogger.threadCreated(this.getName());
}
@ -44,13 +37,13 @@ public class StatThread extends Thread {
public void run() throws IllegalStateException, NullPointerException {
MyLogger.threadStart(this.getName());
if (request == null) {
throw new NullPointerException("No statistic request was found!");
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(request.getCommandSender(), StandardMessage.STILL_RELOADING);
outputManager.sendFeedbackMsg(statRequest.getCommandSender(), StandardMessage.STILL_RELOADING);
reloadThread.join();
} catch (InterruptedException e) {
@ -61,74 +54,29 @@ public class StatThread extends Thread {
long lastCalc = ThreadManager.getLastRecordedCalcTime();
if (lastCalc > 2000) {
outputManager.sendFeedbackMsgWaitAMoment(request.getCommandSender(), lastCalc > 20000);
outputManager.sendFeedbackMsgWaitAMoment(statRequest.getCommandSender(), lastCalc > 20000);
}
Target selection = request.getSelection();
Target selection = statRequest.getTarget();
try {
switch (selection) {
case PLAYER -> outputManager.sendPlayerStat(request, getIndividualStat());
case TOP -> outputManager.sendTopStat(request, getTopStats());
case SERVER -> outputManager.sendServerStat(request, getServerTotal());
}
} catch (ConcurrentModificationException e) {
if (!request.isConsoleSender()) {
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.UNKNOWN_ERROR);
}
}
}
private LinkedHashMap<String, Integer> getTopStats() throws ConcurrentModificationException {
return getAllStats().entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(config.getTopListMaxSize()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
}
private long getServerTotal() {
List<Integer> numbers = getAllStats().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 in the list)
private @NotNull ConcurrentHashMap<String, Integer> getAllStats() throws ConcurrentModificationException {
long time = System.currentTimeMillis();
int size = offlinePlayerHandler.getOfflinePlayerCount() != 0 ? offlinePlayerHandler.getOfflinePlayerCount() : 16;
ConcurrentHashMap<String, Integer> playerStats = new ConcurrentHashMap<>(size);
ImmutableList<String> playerNames = ImmutableList.copyOf(offlinePlayerHandler.getOfflinePlayerNames());
StatAction task = new StatAction(offlinePlayerHandler, playerNames, request, playerStats);
MyLogger.actionCreated(playerNames.size());
ForkJoinPool commonPool = ForkJoinPool.commonPool();
try {
commonPool.invoke(task);
} catch (ConcurrentModificationException e) {
MyLogger.logMsg("The request 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);
return playerStats;
}
/** Gets the statistic data for an individual player. If somehow the player
cannot be found, this returns 0.*/
private int getIndividualStat() {
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(request.getPlayerName());
if (player != null) {
return switch (request.getStatistic().getType()) {
case UNTYPED -> player.getStatistic(request.getStatistic());
case ENTITY -> player.getStatistic(request.getStatistic(), request.getEntity());
case BLOCK -> player.getStatistic(request.getStatistic(), request.getBlock());
case ITEM -> player.getStatistic(request.getStatistic(), request.getItem());
TextComponent statResult = switch (selection) {
case PLAYER -> outputManager.formatPlayerStat(statRequest, statManager.getPlayerStat(statRequest));
case TOP -> outputManager.formatTopStat(statRequest, statManager.getTopStats(statRequest));
case SERVER -> outputManager.formatServerStat(statRequest, statManager.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);
}
}
return 0;
}
}

View File

@ -0,0 +1,46 @@
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.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 RequestExecutor<Integer> {
private final StatRequestHandler statRequestHandler;
public PlayerStatRequest(StatRequestHandler statRequestHandler) {
this.statRequestHandler = statRequestHandler;
}
@Override
public StatResult<Integer> untyped(@NotNull Statistic statistic) {
StatRequest completedRequest = statRequestHandler.untyped(statistic);
return getStatResult(completedRequest);
}
@Override
public StatResult<Integer> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
StatRequest completedRequest = statRequestHandler.blockOrItemType(statistic, material);
return getStatResult(completedRequest);
}
@Override
public StatResult<Integer> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
StatRequest completedRequest = statRequestHandler.entityType(statistic, entityType);
return getStatResult(completedRequest);
}
private PlayerStatResult getStatResult(StatRequest completedRequest) {
int stat = RequestExecutor.super.getStatCalculator()
.getPlayerStat(completedRequest);
TextComponent prettyStat = RequestExecutor.super.getStatFormatter()
.formatPlayerStat(completedRequest, stat);
return new PlayerStatResult(stat, prettyStat);
}
}

View File

@ -0,0 +1,46 @@
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.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 RequestExecutor<Long> {
private final StatRequestHandler statRequestHandler;
public ServerStatRequest(StatRequestHandler statRequestHandler) {
this.statRequestHandler = statRequestHandler;
}
@Override
public StatResult<Long> untyped(@NotNull Statistic statistic) {
StatRequest completedRequest = statRequestHandler.untyped(statistic);
return getStatResult(completedRequest);
}
@Override
public StatResult<Long> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
StatRequest completedRequest = statRequestHandler.blockOrItemType(statistic, material);
return getStatResult(completedRequest);
}
@Override
public StatResult<Long> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
StatRequest completedRequest = statRequestHandler.entityType(statistic, entityType);
return getStatResult(completedRequest);
}
private ServerStatResult getStatResult(StatRequest completedRequest) {
long stat = RequestExecutor.super.getStatCalculator()
.getServerStat(completedRequest);
TextComponent prettyStat = RequestExecutor.super.getStatFormatter()
.formatServerStat(completedRequest, stat);
return new ServerStatResult(stat, prettyStat);
}
}

View File

@ -0,0 +1,186 @@
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.enums.Target;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
/** 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 {
private final CommandSender sender;
private boolean isAPIRequest;
private Statistic statistic;
private String playerName;
private Target target;
private int topListSize;
private String subStatEntryName;
private EntityType entity;
private Material block;
private Material item;
private boolean playerFlag;
/** Create a new {@link 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
*/
private StatRequest(@NotNull CommandSender sender, boolean isAPIRequest) {
this.sender = sender;
this.isAPIRequest = isAPIRequest;
target = Target.TOP;
playerFlag = false;
}
public static StatRequest getBasicRequest(CommandSender sender) {
return new StatRequest(sender, false);
}
public static StatRequest getBasicAPIRequest() {
return new StatRequest(Bukkit.getConsoleSender(), true);
}
public void setAPIRequest() {
this.isAPIRequest = true;
}
public boolean isAPIRequest() {
return isAPIRequest;
}
public @NotNull CommandSender getCommandSender() {
return sender;
}
public boolean isConsoleSender() {
return sender instanceof ConsoleCommandSender;
}
public void setStatistic(Statistic statistic) {
this.statistic = statistic;
}
public Statistic getStatistic() {
return statistic;
}
public void setSubStatEntryName(String subStatEntry) {
this.subStatEntryName = subStatEntry;
}
public String getSubStatEntryName() {
return subStatEntryName;
}
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
public String getPlayerName() {
return playerName;
}
public void setPlayerFlag(boolean playerFlag) {
this.playerFlag = playerFlag;
}
public boolean getPlayerFlag() {
return playerFlag;
}
public void setTarget(Target target) {
this.target = target;
}
public @NotNull Target getTarget() {
return target;
}
public void setTopListSize(int topListSize) {
this.topListSize = topListSize;
}
public int getTopListSize() {
return this.topListSize;
}
public void setEntity(EntityType entity) {
this.entity = entity;
}
public EntityType getEntity() {
return entity;
}
public void setBlock(Material block) {
this.block = block;
}
public Material getBlock() {
return block;
}
public void setItem(Material item) {
this.item = item;
}
public Material getItem() {
return item;
}
public boolean isValid() {
if (statistic == null) {
return false;
} else if (target == Target.PLAYER && playerName == null) {
return false;
} else if (statistic.getType() != Statistic.Type.UNTYPED &&
subStatEntryName == null) {
return false;
} else {
return hasMatchingSubStat();
}
}
private boolean hasMatchingSubStat() {
switch (statistic.getType()) {
case BLOCK -> {
return block != null;
}
case ENTITY -> {
return entity != null;
}
case ITEM -> {
return item != null;
}
default -> {
return true;
}
}
}
}

View File

@ -0,0 +1,171 @@
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 record StatRequestHandler(StatRequest statRequest) implements RequestGenerator {
public static StatRequestHandler playerRequestHandler(String playerName) {
StatRequest request = StatRequest.getBasicAPIRequest();
request.setTarget(Target.PLAYER);
request.setPlayerName(playerName);
return new StatRequestHandler(request);
}
public static StatRequestHandler serverRequestHandler() {
StatRequest request = StatRequest.getBasicAPIRequest();
request.setTarget(Target.SERVER);
return new StatRequestHandler(request);
}
public static StatRequestHandler topRequestHandler(int topListSize) {
StatRequest request = StatRequest.getBasicAPIRequest();
request.setTarget(Target.TOP);
request.setTopListSize(topListSize != 0 ? topListSize : Main.getConfigHandler().getTopListMaxSize());
return new StatRequestHandler(request);
}
/**
@param sender the CommandSender that requested this specific statistic
*/
public static StatRequestHandler internalRequestHandler(CommandSender sender) {
StatRequest request = StatRequest.getBasicRequest(sender);
request.setTopListSize(Main.getConfigHandler().getTopListMaxSize());
return new StatRequestHandler(request);
}
@Override
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");
}
@Override
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;
}
@Override
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

@ -0,0 +1,48 @@
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.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 RequestExecutor<LinkedHashMap<String, Integer>> {
private final StatRequestHandler statRequestHandler;
public TopStatRequest(StatRequestHandler statRequestHandler) {
this.statRequestHandler = statRequestHandler;
}
@Override
public StatResult<LinkedHashMap<String, Integer>> untyped(@NotNull Statistic statistic) {
StatRequest completedRequest = statRequestHandler.untyped(statistic);
return getStatResult(completedRequest);
}
@Override
public StatResult<LinkedHashMap<String, Integer>> blockOrItemType(@NotNull Statistic statistic, @NotNull Material material) {
StatRequest completedRequest = statRequestHandler.blockOrItemType(statistic, material);
return getStatResult(completedRequest);
}
@Override
public StatResult<LinkedHashMap<String, Integer>> entityType(@NotNull Statistic statistic, @NotNull EntityType entityType) {
StatRequest completedRequest = statRequestHandler.entityType(statistic, entityType);
return getStatResult(completedRequest);
}
private TopStatResult getStatResult(StatRequest completedRequest) {
LinkedHashMap<String, Integer> stat = RequestExecutor.super.getStatCalculator()
.getTopStats(completedRequest);
TextComponent prettyStat = RequestExecutor.super.getStatFormatter()
.formatTopStat(completedRequest, stat);
return new TopStatResult(stat, prettyStat);
}
}

View File

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

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

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

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

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

@ -19,54 +19,27 @@ import java.util.stream.Stream;
and turn a name into its corresponding enum constant. */
public final class EnumHandler {
private final static List<String> blockNames;
private final static List<String> entityNames;
private final static List<String> itemNames;
private final static List<String> statNames;
private final static List<String> subStatNames;
private static List<String> blockNames;
private static List<String> itemNames;
private static List<String> statNames;
private static List<String> subStatNames;
static {
blockNames = Arrays.stream(Material.values())
.filter(Material::isBlock)
.map(Material::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
entityNames = Arrays.stream(EntityType.values())
.map(EntityType::toString)
.map(String::toLowerCase)
.filter(entityName -> !entityName.equalsIgnoreCase("unknown"))
.collect(Collectors.toList());
itemNames = Arrays.stream(Material.values())
.filter(Material::isItem)
.map(Material::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
subStatNames = Stream.of(blockNames, entityNames, itemNames)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
statNames = Arrays.stream(Statistic.values())
.map(Statistic::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
public EnumHandler() {
prepareLists();
}
/** Returns all block-names in lowercase */
public static List<String> getBlockNames() {
public List<String> getBlockNames() {
return blockNames;
}
/** Returns all item-names in lowercase*/
public static List<String> getItemNames() {
public List<String> getItemNames() {
return itemNames;
}
/** Returns all statistic-names in lowercase */
public static List<String> getStatNames() {
public List<String> getStatNames() {
return statNames;
}
@ -115,31 +88,50 @@ public final class EnumHandler {
/** Checks if string is a valid statistic
@param statName String, case-insensitive */
public static boolean isStatistic(@NotNull String statName) {
public boolean isStatistic(@NotNull String statName) {
return statNames.contains(statName.toLowerCase());
}
/** Checks whether the given String equals the name of an entity-type statistic. */
public static boolean isEntityStatistic(String statName) {
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*/
public static boolean isSubStatEntry(@NotNull String statName) {
public boolean isSubStatEntry(@NotNull String statName) {
return subStatNames.contains(statName.toLowerCase());
}
/** 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;
private void prepareLists() {
List<String> entityNames = Arrays.stream(EntityType.values())
.map(EntityType::toString)
.map(String::toLowerCase)
.filter(entityName -> !entityName.equalsIgnoreCase("unknown"))
.collect(Collectors.toList());
blockNames = Arrays.stream(Material.values())
.filter(Material::isBlock)
.map(Material::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
itemNames = Arrays.stream(Material.values())
.filter(Material::isItem)
.map(Material::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
subStatNames = Stream.of(blockNames, entityNames, itemNames)
.flatMap(Collection::stream)
.distinct()
.collect(Collectors.toList());
statNames = Arrays.stream(Statistic.values())
.map(Statistic::toString)
.map(String::toLowerCase)
.collect(Collectors.toList());
}
}

View File

@ -13,6 +13,7 @@ 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;
@ -36,10 +37,10 @@ public final class MyLogger {
}
/** Sets the desired debugging level.
<p>1 = low (only show unexpected errors)</p>
<p>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</p>
<p>3 = high (log all tasks and time taken)</p>
<p>Default: 1</p>*/
<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;
@ -80,6 +81,10 @@ public final class MyLogger {
}
}
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.
@ -96,7 +101,7 @@ public final class MyLogger {
}
}
/** If DebugLevel is MEDIUM or HIGH, logs when the while loop in MessageWriter, getLanguageKey is being run. */
/** 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...");
@ -105,14 +110,14 @@ public final class MyLogger {
/** Output to console that the given thread has been created (but not started yet).*/
public static void threadCreated(String threadName) {
if (debugLevel != DebugLevel.LOW) {
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.MEDIUM || debugLevel == DebugLevel.HIGH) {
if (debugLevel == DebugLevel.HIGH) {
logger.info(threadName + " started!");
}
}
@ -190,14 +195,6 @@ public final class MyLogger {
}
}
/** Output to console how long a certain task has taken (regardless of DebugLevel).
@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 */
public static void logTimeTaken(String className, String methodName, long startTime) {
logTimeTaken(className, methodName, startTime, DebugLevel.LOW);
}
/** 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

View File

@ -7,7 +7,10 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class OfflinePlayerHandler {
/** 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;
@ -17,6 +20,16 @@ public class OfflinePlayerHandler {
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) {
@ -33,16 +46,6 @@ public class OfflinePlayerHandler {
return playerNames;
}
/**
* 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 void updateOfflinePlayerList(ConcurrentHashMap<String, UUID> playerList) {
offlinePlayerUUIDs = playerList;
playerNames = Collections.list(offlinePlayerUUIDs.keys());
}
/**
* 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
@ -53,6 +56,7 @@ public class OfflinePlayerHandler {
return Bukkit.getOfflinePlayer(offlinePlayerUUIDs.get(playerName));
}
else {
MyLogger.logMsg("Cannot calculate statistics for player-name: " + playerName, true);
return null;
}
}

View File

@ -1,5 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
/** A small utility class that calculates with unix time.*/
public final class UnixTimeHandler {
private UnixTimeHandler() {

View File

@ -4,6 +4,8 @@ version: 1.6.1
api-version: 1.13
description: adds commands to view player statistics in chat
author: Artemis_the_gr8
softdepend:
- PlaceholderAPI
commands:
statistic:
aliases: