Merge pull request #96 from itHotL/share-button

Share button
This commit is contained in:
Elise 2022-07-20 14:46:04 +02:00 committed by GitHub
commit 113a47772e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1631 additions and 674 deletions

View File

@ -1,22 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.artemis-the-gr8</groupId>
<groupId>com.gmail.artemis.the.gr8</groupId>
<artifactId>PlayerStats</artifactId>
<version>1.5</version>
<version>1.6</version>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.3</version>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
@ -36,6 +36,16 @@
<exclude>org.jetbrains:annotations</exclude>
</excludes>
</artifactSet>
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.tchristofferson</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
@ -45,6 +55,18 @@
<exclude>images/**</exclude>
</excludes>
</filter>
<filter>
<artifact>net.kyori:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
<filter>
<artifact>com.tchristofferson:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
@ -79,8 +101,8 @@
</dependency>
</dependencies>
<properties>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
<maven.compiler.source>16</maven.compiler.source>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

36
pom.xml
View File

@ -4,14 +4,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gmail.artemis-the-gr8</groupId>
<groupId>com.gmail.artemis.the.gr8</groupId>
<artifactId>PlayerStats</artifactId>
<version>1.5</version>
<version>1.6</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
</properties>
@ -78,14 +78,14 @@
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<source>16</source>
<target>16</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.3</version>
<version>3.3.0</version>
<executions>
<execution>
<phase>package</phase>
@ -106,6 +106,16 @@
<exclude>org.jetbrains:annotations</exclude>
</excludes>
</artifactSet>
<relocations>
<relocation>
<pattern>net.kyori</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.kyori</shadedPattern>
</relocation>
<relocation>
<pattern>com.tchristofferson</pattern>
<shadedPattern>com.gmail.artemis.the.gr8.tchristofferson</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
@ -115,6 +125,18 @@
<exclude>images/**</exclude>
</excludes>
</filter>
<filter>
<artifact>net.kyori:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
<filter>
<artifact>com.tchristofferson:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>

View File

@ -1,26 +1,24 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.commands.ReloadCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.ShareCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.StatCommand;
import com.gmail.artemis.the.gr8.playerstats.commands.TabCompleter;
import com.gmail.artemis.the.gr8.playerstats.commands.cmdutils.TabCompleteHelper;
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.listeners.JoinListener;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
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 {
private BukkitAudiences adventure;
private static BukkitAudiences adventure;
public @NotNull BukkitAudiences adventure() {
public static @NotNull BukkitAudiences adventure() {
if (adventure == null) {
throw new IllegalStateException("Tried to access Adventure when the plugin was disabled!");
}
@ -32,25 +30,24 @@ public class Main extends JavaPlugin {
//initialize the Adventure library
adventure = BukkitAudiences.create(this);
//first get an instance of the ConfigHandler
//first get an instance of all the classes that need to be initialized or passed along to different classes
ConfigHandler config = new ConfigHandler(this);
OutputManager sender = new OutputManager(config);
OfflinePlayerHandler offlinePlayerHandler = new OfflinePlayerHandler();
//for now always use the PrideComponentFactory (it'll use the regular formatting when needed)
MessageWriter messageWriter = new MessageWriter(config);
//initialize the threadManager
ThreadManager threadManager = new ThreadManager(adventure(), config, messageWriter, this);
TabCompleteHelper tab = new TabCompleteHelper();
Bukkit.getLogger().info(tab.getEntityKilledSuggestions().toString());
ThreadManager threadManager = ThreadManager.getInstance(config, sender, offlinePlayerHandler);
ShareManager shareManager = ShareManager.getInstance(config);
//register all commands and the tabCompleter
PluginCommand statcmd = this.getCommand("statistic");
if (statcmd != null) {
statcmd.setExecutor(new StatCommand(adventure(), messageWriter, threadManager));
statcmd.setTabCompleter(new TabCompleter());
statcmd.setExecutor(new StatCommand(sender, threadManager, offlinePlayerHandler));
statcmd.setTabCompleter(new TabCompleter(offlinePlayerHandler));
}
PluginCommand reloadcmd = this.getCommand("statisticreload");
if (reloadcmd != null) reloadcmd.setExecutor(new ReloadCommand(threadManager));
PluginCommand sharecmd = this.getCommand("statisticshare");
if (sharecmd != null) sharecmd.setExecutor(new ShareCommand(shareManager, sender));
//register the listener
Bukkit.getPluginManager().registerEvents(new JoinListener(threadManager), this);

View File

@ -0,0 +1,160 @@
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.utils.MyLogger;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import javax.annotation.Nullable;
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;
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 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 synchronized void updateSettings(ConfigHandler config) {
isEnabled = config.allowStatSharing() && config.useHoverText();
waitingTime = config.getStatShareWaitingTime();
if (isEnabled) {
sharedResults = new ArrayBlockingQueue<>(500); //reset the sharedResultsQueue
if (resultID == null) { //if we went from disabled to enabled, initialize
resultID = new AtomicInteger(); //always starts with value 0
statResultQueue = new ConcurrentHashMap<>();
shareTimeStamp = new ConcurrentHashMap<>();
}
} else {
//if we went from enabled to disabled, purge the existing data
if (statResultQueue != null) {
statResultQueue = null;
shareTimeStamp = null;
sharedResults = null;
}
if (config.allowStatSharing() && !config.useHoverText()) {
MyLogger.logMsg("Stat-sharing does not work without hover-text enabled! " +
"Enable hover-text, or disable stat-sharing to stop seeing this message.", true);
}
}
}
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) {
removeExcessResults(playerName);
int ID = getNextIDNumber();
UUID shareCode = UUID.randomUUID();
statResultQueue.put(shareCode, new StatResult(playerName, statResult, ID, shareCode));
MyLogger.logMsg("Saving statResults with no. " + ID, DebugLevel.MEDIUM);
return shareCode;
}
public boolean isOnCoolDown(String playerName) {
if (waitingTime == 0 || !shareTimeStamp.containsKey(playerName)) {
return false;
} else {
long seconds = SECONDS.between(shareTimeStamp.get(playerName), Instant.now());
return seconds <= (long) waitingTime * 60;
}
}
public boolean requestAlreadyShared(UUID shareCode) {
return sharedResults.contains(shareCode);
}
/** Takes a statResult 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) {
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);
synchronized (this) { //put the last 50 values in the new Queue
UUID[] lastValues = sharedResults.toArray(new UUID[0]);
Arrays.stream(Arrays.copyOfRange(lastValues, 450, 500))
.parallel().iterator()
.forEachRemaining(newQueue::offer);
sharedResults = newQueue;
}
sharedResults.offer(shareCode);
}
return statResultQueue.remove(shareCode);
}
else {
return null;
}
}
/** 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()
.parallelStream()
.filter(result -> result.playerName().equalsIgnoreCase(playerName))
.toList();
if (alreadySavedResults.size() > 25) {
UUID uuid = 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);
}
}
private int getNextIDNumber() {
return resultID.incrementAndGet();
}
}

View File

@ -1,51 +1,69 @@
package com.gmail.artemis.the.gr8.playerstats;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
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.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.models.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import org.bukkit.command.CommandSender;
import java.util.HashMap;
public final class ThreadManager {
public class ThreadManager {
private static volatile ThreadManager instance;
private final int threshold = 10;
private final static int threshold = 10;
private int statThreadID;
private int reloadThreadID;
private final Main plugin;
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static MessageWriter messageWriter;
private static OutputManager messageSender;
private final OfflinePlayerHandler offlinePlayerHandler;
private ReloadThread lastActiveReloadThread;
private StatThread lastActiveStatThread;
private final HashMap<String, Thread> statThreads;
private static long lastRecordedCalcTime;
public ThreadManager(BukkitAudiences a, ConfigHandler c, MessageWriter m, Main p) {
adventure = a;
private ThreadManager(ConfigHandler c, OutputManager m, OfflinePlayerHandler o) {
config = c;
messageWriter = m;
plugin = p;
messageSender = m;
offlinePlayerHandler = o;
statThreads = new HashMap<>();
statThreadID = 0;
reloadThreadID = 0;
lastRecordedCalcTime = 0;
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;
}
public void startReloadThread(CommandSender sender) {
if (lastActiveReloadThread == null || !lastActiveReloadThread.isAlive()) {
reloadThreadID += 1;
lastActiveReloadThread = new ReloadThread(adventure, config, messageWriter, threshold, reloadThreadID, lastActiveStatThread, sender);
lastActiveReloadThread = new ReloadThread(config, messageSender, offlinePlayerHandler, reloadThreadID, lastActiveStatThread, sender);
lastActiveReloadThread.start();
}
else {
@ -60,7 +78,7 @@ public class ThreadManager {
if (config.limitStatRequests() && statThreads.containsKey(cmdSender)) {
Thread runningThread = statThreads.get(cmdSender);
if (runningThread.isAlive()) {
adventure.sender(request.getCommandSender()).sendMessage(messageWriter.requestAlreadyRunning(request.isBukkitConsoleSender()));
messageSender.sendFeedbackMsg(request.getCommandSender(), StandardMessage.REQUEST_ALREADY_RUNNING);
} else {
startNewStatThread(request);
}
@ -82,7 +100,7 @@ public class ThreadManager {
}
private void startNewStatThread(StatRequest request) {
lastActiveStatThread = new StatThread(adventure, config, messageWriter, plugin, statThreadID, threshold, request, lastActiveReloadThread);
lastActiveStatThread = new StatThread(config, messageSender, offlinePlayerHandler, statThreadID, request, lastActiveReloadThread);
statThreads.put(request.getCommandSender().getName(), lastActiveStatThread);
lastActiveStatThread.start();
}

View File

@ -9,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
public class ReloadCommand implements CommandExecutor {
private final ThreadManager threadManager;
private static ThreadManager threadManager;
public ReloadCommand(ThreadManager t) {
threadManager = t;

View File

@ -0,0 +1,52 @@
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.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class ShareCommand implements CommandExecutor {
private static ShareManager shareManager;
private static OutputManager outputManager;
public ShareCommand(ShareManager s, OutputManager m) {
shareManager = s;
outputManager = m;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) {
if (args.length == 1 && shareManager.isEnabled()) {
UUID shareCode;
try {
shareCode = UUID.fromString(args[0]);
} catch (IllegalArgumentException e) {
MyLogger.logException(e, "ShareCommand", "/statshare is being called without a valid UUID argument");
return false;
}
if (shareManager.requestAlreadyShared(shareCode)) {
outputManager.sendFeedbackMsg(sender, StandardMessage.RESULTS_ALREADY_SHARED);
}
else if (shareManager.isOnCoolDown(sender.getName())) {
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
outputManager.sendFeedbackMsg(sender, StandardMessage.STAT_RESULTS_TOO_OLD);
} else {
outputManager.shareStatResults(result.statResult());
}
}
}
return true;
}
}

View File

@ -1,14 +1,12 @@
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.msg.OutputManager;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.models.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.command.Command;
@ -18,39 +16,34 @@ import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class StatCommand implements CommandExecutor {
private final BukkitAudiences adventure;
private final MessageWriter messageWriter;
private final ThreadManager threadManager;
private static ThreadManager threadManager;
private static OutputManager outputManager;
private final OfflinePlayerHandler offlinePlayerHandler;
public StatCommand(BukkitAudiences a, MessageWriter m, ThreadManager t) {
adventure = a;
messageWriter = m;
public StatCommand(OutputManager m, ThreadManager t, OfflinePlayerHandler o) {
threadManager = t;
outputManager = m;
offlinePlayerHandler = o;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
boolean isBukkitConsole = sender instanceof ConsoleCommandSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit");
if (args.length == 0 || args[0].equalsIgnoreCase("help")) { //in case of less than 1 argument or "help", display the help message
adventure.sender(sender).sendMessage(messageWriter.helpMsg(sender instanceof ConsoleCommandSender));
outputManager.sendHelp(sender);
}
else if (args[0].equalsIgnoreCase("examples") ||
args[0].equalsIgnoreCase("example")) { //in case of "statistic examples", show examples
adventure.sender(sender).sendMessage(messageWriter.usageExamples(isBukkitConsole));
outputManager.sendExamples(sender);
}
else {
StatRequest request = generateRequest(sender, args);
TextComponent issues = checkRequest(request, isBukkitConsole);
if (issues == null) {
if (requestIsValid(request)) {
threadManager.startStatThread(request);
}
else {
adventure.sender(sender).sendMessage(issues);
} else {
return false;
}
}
@ -90,7 +83,7 @@ public class StatCommand implements CommandExecutor {
request.setSelection(Target.SERVER);
}
}
else if (OfflinePlayerHandler.isRelevantPlayer(arg) && request.getPlayerName() == null) {
else if (offlinePlayerHandler.isRelevantPlayer(arg) && request.getPlayerName() == null) {
request.setPlayerName(arg);
request.setSelection(Target.PLAYER);
}
@ -136,28 +129,32 @@ public class StatCommand implements CommandExecutor {
}
}
/** This method validates the StatRequest and returns feedback in the form of a TextComponent.
/** 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 null if the Request is valid, and an explanation message otherwise. */
private @Nullable TextComponent checkRequest(StatRequest request, boolean isBukkitConsole) {
@return true if the Request is valid, and false + an explanation message otherwise. */
private boolean requestIsValid(StatRequest request) {
if (request.getStatistic() == null) {
return messageWriter.missingStatName(isBukkitConsole);
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.MISSING_STAT_NAME);
return false;
}
Statistic.Type type = request.getStatistic().getType();
if (request.getSubStatEntry() == null && type != Statistic.Type.UNTYPED) {
return messageWriter.missingSubStatName(type, isBukkitConsole);
outputManager.sendFeedbackMsgMissingSubStat(request.getCommandSender(), type);
return false;
}
else if (!matchingSubStat(request)) {
return messageWriter.wrongSubStatType(type, request.getSubStatEntry(), isBukkitConsole);
outputManager.sendFeedbackMsgWrongSubStat(request.getCommandSender(), type, request.getSubStatEntry());
return false;
}
else if (request.getSelection() == Target.PLAYER && request.getPlayerName() == null) {
return messageWriter.missingPlayerName(isBukkitConsole);
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.MISSING_PLAYER_NAME);
return false;
}
else {
return null;
return true;
}
}

View File

@ -14,18 +14,21 @@ import java.util.stream.Collectors;
public class TabCompleter implements org.bukkit.command.TabCompleter {
private final List<String> commandOptions;
private final OfflinePlayerHandler offlinePlayerHandler;
private final TabCompleteHelper tabCompleteHelper;
//TODO add "example" to the list
public TabCompleter() {
private final List<String> commandOptions;
public TabCompleter(OfflinePlayerHandler o) {
offlinePlayerHandler = o;
tabCompleteHelper = new TabCompleteHelper();
commandOptions = new ArrayList<>();
commandOptions.add("top");
commandOptions.add("player");
commandOptions.add("server");
commandOptions.add("me");
tabCompleteHelper = new TabCompleteHelper();
}
//args[0] = statistic (length = 1)
@ -37,34 +40,31 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
List<String> tabSuggestions = new ArrayList<>();
//after typing "stat", suggest a list of viable statistics
if (args.length >= 1) {
String currentArg = args[args.length -1];
if (args.length == 1) {
tabSuggestions = getTabSuggestions(EnumHandler.getStatNames(), args[0]);
if (args.length == 1) { //after typing "stat", suggest a list of viable statistics
tabSuggestions = getFirstArgSuggestions(args[0]);
}
//after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions
else {
else { //after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions
String previousArg = args[args.length -2];
if (EnumHandler.isStatistic(previousArg)) {
Statistic stat = EnumHandler.getStatEnum(previousArg);
if (stat != null) {
tabSuggestions = getTabSuggestions(getRelevantList(stat), currentArg);
}
}
//if previous arg = "player", suggest playerNames
//if previous arg = "player"
else if (previousArg.equalsIgnoreCase("player")) {
//if args.length-3 is kill_entity or entity_killed_by
if (args.length >= 3 && EnumHandler.isEntityStatistic(args[args.length-3])) {
tabSuggestions = commandOptions;
tabSuggestions = commandOptions; //if arg before "player" was entity-stat, suggest commandOptions
}
else {
tabSuggestions = getTabSuggestions(OfflinePlayerHandler.getOfflinePlayerNames(), currentArg);
else { //otherwise "player" is target-flag: suggest playerNames
tabSuggestions = getTabSuggestions(offlinePlayerHandler.getOfflinePlayerNames(), currentArg);
}
}
@ -77,6 +77,13 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
return tabSuggestions;
}
private List<String> getFirstArgSuggestions(String currentArg) {
List<String> suggestions = EnumHandler.getStatNames();
suggestions.add("examples");
suggestions.add("help");
return getTabSuggestions(suggestions, currentArg);
}
private List<String> getTabSuggestions(List<String> completeList, String currentArg) {
return completeList.stream()
.filter(item -> item.toLowerCase().contains(currentArg.toLowerCase()))
@ -91,14 +98,12 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
case ITEM -> {
if (stat == Statistic.BREAK_ITEM) {
return tabCompleteHelper.getItemBrokenSuggestions();
} else if (stat == Statistic.CRAFT_ITEM) {
return tabCompleteHelper.getAllItemNames(); //TODO fix
} else {
return tabCompleteHelper.getAllItemNames();
}
}
case ENTITY -> {
return tabCompleteHelper.getEntityKilledSuggestions();
return tabCompleteHelper.getEntitySuggestions();
}
default -> {
return commandOptions;

View File

@ -8,10 +8,10 @@ import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TabCompleteHelper {
public final class TabCompleteHelper {
private static List<String> itemBrokenSuggestions;
private static List<String> entityKilledSuggestions;
private static List<String> entitySuggestions;
public TabCompleteHelper() {
prepareLists();
@ -29,12 +29,13 @@ public class TabCompleteHelper {
return EnumHandler.getBlockNames();
}
public List<String> getEntityKilledSuggestions() {
return entityKilledSuggestions;
public List<String> getEntitySuggestions() {
return entitySuggestions;
}
private static void prepareLists() {
//breaking an item means running its durability negative
itemBrokenSuggestions = Arrays.stream(Material.values())
.parallel()
.filter(Material::isItem)
@ -43,7 +44,8 @@ public class TabCompleteHelper {
.map(String::toLowerCase)
.collect(Collectors.toList());
entityKilledSuggestions = Arrays.stream(EntityType.values())
//the only statistics dealing with entities are killed_entity and entity_killed_by
entitySuggestions = Arrays.stream(EntityType.values())
.parallel()
.filter(EntityType::isAlive)
.map(EntityType::toString)

View File

@ -12,18 +12,18 @@ import java.io.File;
public class ConfigHandler {
private static Main plugin;
private static int configVersion;
private File configFile;
private FileConfiguration config;
private final Main plugin;
private final double configVersion;
public ConfigHandler(Main p) {
plugin = p;
configVersion = 6;
saveDefaultConfig();
config = YamlConfiguration.loadConfiguration(configFile);
configVersion = 5;
checkConfigVersion();
MyLogger.setDebugLevel(getDebugLevel());
@ -35,7 +35,7 @@ public class ConfigHandler {
<p>PlayerStats 1.3: "config-version" is 3. </P>
<p>PlayerStats 1.4: "config-version" is 4.</p>*/
private void checkConfigVersion() {
if (!config.contains("config-version") || config.getDouble("config-version") != configVersion) {
if (!config.contains("config-version") || config.getInt("config-version") != configVersion) {
new ConfigUpdateHandler(plugin, configFile, configVersion);
reloadConfig();
}
@ -73,10 +73,25 @@ public class ConfigHandler {
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>*/
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>*/
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>*/
public int getStatShareWaitingTime() {
return config.getInt("waiting-time-before-sharing-again", 0);
}
/** Returns the config setting for include-whitelist-only.
<p>Default: false</p>*/
public boolean whitelistOnly() {
@ -108,47 +123,6 @@ public class ConfigHandler {
return config.getBoolean("enable-hover-text", true);
}
public String getDistanceUnit(boolean isHoverText) {
return getUnitString(isHoverText, "blocks", "km", "distance-unit");
}
public String getDamageUnit(boolean isHoverText) {
return getUnitString(isHoverText, "hearts", "hp", "damage-unit");
}
public boolean autoDetectTimeUnit(boolean isHoverText) {
String path = "auto-detect-biggest-time-unit";
if (isHoverText) {
path = path + "-for-hover-text";
}
boolean defaultValue = !isHoverText;
return config.getBoolean(path, defaultValue);
}
public int getNumberOfExtraTimeUnits(boolean isHoverText) {
String path = "number-of-extra-units";
if (isHoverText) {
path = path + "-for-hover-text";
}
int defaultValue = isHoverText ? 0 : 1;
return config.getInt(path, defaultValue);
}
/** By default, getTimeUnit will return the maxUnit. If the optional minUnit flag is specified,
the minimum unit will be returned instead. */
public String getTimeUnit(boolean isHoverText) {
return getTimeUnit(isHoverText, false);
}
/** By default, getTimeUnit will return the maxUnit. If the optional smallUnit flag is specified,
the minimum unit will be returned instead. */
public String getTimeUnit(boolean isHoverText, boolean smallUnit) {
if (smallUnit) {
return getUnitString(isHoverText, "hours", "seconds", "smallest-time-unit");
}
return getUnitString(isHoverText, "days", "hours", "biggest-time-unit");
}
/** Whether to use festive formatting, such as pride colors.
<p>Default: true</p> */
public boolean useFestiveFormatting() {
@ -161,6 +135,23 @@ public class ConfigHandler {
return config.getBoolean("rainbow-mode", false);
}
public boolean useEnters(Target selection, boolean getSharedSetting) {
ConfigurationSection section = config.getConfigurationSection("use-enters");
boolean def = selection == Target.TOP && !getSharedSetting;
if (section != null) {
String path = switch (selection) {
case TOP -> getSharedSetting ? "top-stats-shared" : "top-stats";
case PLAYER -> getSharedSetting ? "player-stats-shared" : "player-stats";
case SERVER -> getSharedSetting ? "server-stats-shared" : "server-stats";
};
return section.getBoolean(path, def);
}
MyLogger.logMsg("Config settings for use-enters could not be retrieved! " +
"Please check your file if you want to use custom settings. " +
"Using default values...", true);
return def;
}
/** Returns the config setting for use-dots.
<p>Default: true</p>*/
public boolean useDots() {
@ -191,6 +182,57 @@ public class ConfigHandler {
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");
}
/** 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");
}
/** Whether PlayerStats should automatically detect the most suitable unit to use for time-based statistics.
<p>Default: true</p>*/
public boolean autoDetectTimeUnit(boolean isHoverText) {
String path = "auto-detect-biggest-time-unit";
if (isHoverText) {
path = path + "-for-hover-text";
}
boolean defaultValue = !isHoverText;
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) {
String path = "number-of-extra-units";
if (isHoverText) {
path = path + "-for-hover-text";
}
int defaultValue = isHoverText ? 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);
}
/** 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) {
if (smallUnit) {
return getUnitString(isHoverText, "hours", "seconds", "smallest-time-unit");
}
return getUnitString(isHoverText, "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>*/
@ -198,11 +240,26 @@ public class ConfigHandler {
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>*/
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>*/
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>*/
public String getPlayerNameDecoration(Target selection, boolean getStyle) {
public String getPlayerNameDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "green";
@ -210,7 +267,7 @@ public class ConfigHandler {
else {
def = "gold";
}
return getDecorationString(selection, getStyle, def, "player-names");
return getDecorationString(selection, getStyleSetting, def, "player-names");
}
/** Returns true if playerNames Style is "bold", false if it is not.
@ -228,22 +285,22 @@ 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>*/
public String getStatNameDecoration(Target selection, boolean getStyle) {
return getDecorationString(selection, getStyle, "yellow", "stat-names");
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>*/
public String getSubStatNameDecoration(Target selection, boolean getStyle) {
return getDecorationString(selection, getStyle, "#FFD52B", "sub-stat-names");
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> */
public String getStatNumberDecoration(Target selection, boolean getStyle) {
public String getStatNumberDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "#55AAFF";
@ -251,14 +308,14 @@ public class ConfigHandler {
else {
def = "#ADE7FF";
}
return getDecorationString(selection, getStyle, def,"stat-numbers");
return getDecorationString(selection, getStyleSetting, def,"stat-numbers");
}
/** 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>*/
public String getTitleDecoration(Target selection, boolean getStyle) {
public String getTitleDecoration(Target selection, boolean getStyleSetting) {
String def;
if (selection == Target.TOP) {
def = "yellow";
@ -266,35 +323,35 @@ public class ConfigHandler {
else {
def = "gold";
}
return getDecorationString(selection, getStyle, def, "title");
return getDecorationString(selection, getStyleSetting, def, "title");
}
/** 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>*/
public String getTitleNumberDecoration(boolean getStyle) {
return getDecorationString(Target.TOP, getStyle, "gold", "title-number");
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>*/
public String getServerNameDecoration(boolean getStyle) {
return getDecorationString(Target.SERVER, getStyle, "#FFB80E", "server-name");
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>*/
public String getRankNumberDecoration(boolean getStyle) {
return getDecorationString(Target.TOP, getStyle, "gold", "rank-numbers");
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> */
public String getDotsDecoration(boolean getStyle) {
return getDecorationString(Target.TOP, getStyle, "dark_gray", "dots");
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.
@ -316,12 +373,12 @@ public class ConfigHandler {
/** Returns the config value for a color or style option in string-format, the supplied default value,
or null if no configSection was found.
@param selection the Target this decoration is meant for (Player, Server or Top)
@param getStyle if true, the result will be a style String, otherwise a color String
@param getStyleSetting if true, the result will be a style String, otherwise a color String
@param defaultColor the default color to return if the config value cannot be found (for style, the default is always "none")
@param pathName the config path to retrieve the value from*/
private @Nullable String getDecorationString(Target selection, boolean getStyle, String defaultColor, String pathName){
String path = getStyle ? pathName + "-style" : pathName;
String defaultValue = getStyle ? "none" : defaultColor;
private @Nullable String getDecorationString(Target selection, boolean getStyleSetting, String defaultColor, String pathName){
String path = getStyleSetting ? pathName + "-style" : pathName;
String defaultValue = getStyleSetting ? "none" : defaultColor;
ConfigurationSection section = getRelevantSection(selection);
return section != null ? section.getString(path, defaultValue) : null;
@ -329,6 +386,9 @@ public class ConfigHandler {
/** Returns the config section that contains the relevant color or style option. */
private @Nullable ConfigurationSection getRelevantSection(Target selection) {
if (selection == null) { //rather than rework the whole Target enum, I have added shared-stats as the null-option for now
return config.getConfigurationSection("shared-stats");
}
switch (selection) {
case TOP -> {
return config.getConfigurationSection("top-list");

View File

@ -1,6 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.config;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import org.bukkit.configuration.file.YamlConfiguration;
import java.io.File;
@ -11,7 +12,7 @@ import com.tchristofferson.configupdater.ConfigUpdater;
public 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, double configVersion) {
public ConfigUpdateHandler(Main plugin, File configFile, int configVersion) {
YamlConfiguration configuration = YamlConfiguration.loadConfiguration(configFile);
updateTopListDefault(configuration);
updateDefaultColors(configuration);
@ -19,14 +20,14 @@ public class ConfigUpdateHandler {
try {
configuration.save(configFile);
ConfigUpdater.update(plugin, configFile.getName(), configFile);
plugin.getLogger().warning("Your config has been updated to version " + configVersion +
". This version includes some slight changes in the default color scheme, but none of your custom settings should have been changed!");
MyLogger.logMsg("Your config has been updated to version " + configVersion +
", but all of your custom settings should still be there!");
} catch (IOException e) {
e.printStackTrace();
}
}
/** Adjusts the value for "top-list" to migrate the config file from versions 1 or 2 to version 3.*/
/** Adjusts the value for "top-list" to migrate the config file from versions 1 or 2 to version 3 and above.*/
private void updateTopListDefault(YamlConfiguration configuration) {
String oldTitle = configuration.getString("top-list-title");
if (oldTitle != null && oldTitle.equalsIgnoreCase("Top [x]")) {
@ -34,11 +35,14 @@ public class ConfigUpdateHandler {
}
}
/** Adjusts some of the default colors to migrate from versions 2 or 3 to version 4.1.*/
/** Adjusts some of the default colors to migrate from versions 2 or 3 to version 4 and above.*/
private void updateDefaultColors(YamlConfiguration configuration) {
updateColor(configuration, "top-list.title", "yellow", "#FFD52B");
updateColor(configuration, "top-list.title", "#FFEA40", "#FFD52B");
updateColor(configuration, "top-list.stat-names", "yellow", "#FFD52B");
updateColor(configuration, "top-list.stat-names", "#FFEA40", "#FFD52B");
updateColor(configuration, "top-list.sub-stat-names", "#FFD52B", "yellow");
updateColor(configuration, "individual-statistics.stat-names", "yellow", "#FFD52B");
updateColor(configuration, "individual-statistics.sub-stat-names", "#FFD52B", "yellow");
updateColor(configuration, "total-server.title", "gold", "#55AAFF");

View File

@ -3,6 +3,8 @@ package com.gmail.artemis.the.gr8.playerstats.enums;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import java.util.Random;
/** This enum represents the colorscheme PlayerStats uses in its output messages.
<p>GRAY: ChatColor Gray</p>
<p>DARK_PURPLE: #6E3485 (used for default sub-titles, title-underscores and brackets)</p>
@ -16,12 +18,22 @@ import net.kyori.adventure.text.format.TextColor;
public enum PluginColor {
GRAY (NamedTextColor.GRAY), //#AAAAAA
DARK_PURPLE (TextColor.fromHexString("#6E3485")),
LIGHT_PURPLE (TextColor.fromHexString("#845EC2")),
BLUE (NamedTextColor.BLUE),
MEDIUM_BLUE (TextColor.fromHexString("#55AAFF")),
LIGHT_BLUE (TextColor.fromHexString("#55C6FF")),
GOLD (NamedTextColor.GOLD), //#FFAA00
MEDIUM_GOLD (TextColor.fromHexString("#FFD52B")),
LIGHT_GOLD (TextColor.fromHexString("#FFEA40")),
LIGHT_YELLOW (TextColor.fromHexString("#FFFF8E"));
LIGHT_YELLOW (TextColor.fromHexString("#FFFF8E")),
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
private final TextColor color;
@ -37,4 +49,26 @@ public enum PluginColor {
public TextColor getConsoleColor() {
return NamedTextColor.nearestTo(color);
}
}
public static TextColor getRandomNameColor() {
return getRandomNameColor(false);
}
public static TextColor getRandomNameColor(boolean isConsole) {
Random randomizer = new Random();
PluginColor color = switch (randomizer.nextInt(7)) {
case 0 -> NAME_1;
case 2 -> NAME_3;
case 3 -> NAME_4;
case 4 -> NAME_5;
case 5 -> NAME_6;
case 6 -> NAME_7;
default -> NAME_2;
};
return getCorrespondingColor(color, isConsole);
}
private static TextColor getCorrespondingColor(PluginColor nameColor, boolean isConsole) {
return isConsole ? nameColor.getConsoleColor() : nameColor.getColor();
}
}

View File

@ -0,0 +1,13 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
public enum StandardMessage {
RELOADED_CONFIG,
STILL_RELOADING,
MISSING_STAT_NAME,
MISSING_PLAYER_NAME,
REQUEST_ALREADY_RUNNING,
STILL_ON_SHARE_COOLDOWN,
RESULTS_ALREADY_SHARED,
STAT_RESULTS_TOO_OLD,
UNKNOWN_ERROR,
}

View File

@ -3,7 +3,6 @@ package com.gmail.artemis.the.gr8.playerstats.enums;
import org.bukkit.Statistic;
import org.jetbrains.annotations.NotNull;
public enum Unit {
NUMBER (Type.UNTYPED, "Times"),
KM (Type.DISTANCE, "km"),
@ -182,4 +181,4 @@ public enum Unit {
Type() {
}
}
}
}

View File

@ -7,7 +7,7 @@ import org.bukkit.event.player.PlayerJoinEvent;
public class JoinListener implements Listener {
private final ThreadManager threadManager;
private static ThreadManager threadManager;
public JoinListener(ThreadManager t) {
threadManager = t;

View File

@ -1,7 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
package com.gmail.artemis.the.gr8.playerstats.models;
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;
@ -9,8 +8,7 @@ import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
public class StatRequest {
public final class StatRequest {
private final CommandSender sender;
private Statistic statistic;
@ -38,10 +36,6 @@ public class StatRequest {
return sender instanceof ConsoleCommandSender;
}
public boolean isBukkitConsoleSender() {
return sender instanceof ConsoleCommandSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit");
}
public void setStatistic(Statistic statistic) {
this.statistic = statistic;
}

View File

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

@ -0,0 +1,63 @@
package com.gmail.artemis.the.gr8.playerstats.msg;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static net.kyori.adventure.text.Component.text;
public class BukkitConsoleComponentFactory extends ComponentFactory {
public BukkitConsoleComponentFactory(ConfigHandler config) {
super(config);
}
@Override
protected void prepareColors() {
PREFIX = PluginColor.GOLD.getConsoleColor();
BRACKETS = PluginColor.GRAY.getConsoleColor();
UNDERSCORE = PluginColor.DARK_PURPLE.getConsoleColor();
MSG_MAIN = PluginColor.MEDIUM_BLUE.getConsoleColor();
MSG_MAIN_2 = PluginColor.GOLD.getConsoleColor();
MSG_ACCENT_2A = PluginColor.MEDIUM_GOLD.getConsoleColor();
MSG_ACCENT_2B = PluginColor.LIGHT_YELLOW.getConsoleColor();
CLICKED_MSG = PluginColor.LIGHT_PURPLE.getConsoleColor();
HOVER_MSG = PluginColor.LIGHT_BLUE.getConsoleColor();
HOVER_ACCENT = PluginColor.LIGHT_GOLD.getConsoleColor();
}
@Override
public TextColor getSharerNameColor() {
return PluginColor.NAME_5.getConsoleColor();
}
@Override
protected TextComponent getComponent(String content, @NotNull TextColor color, @Nullable TextDecoration style) {
return getComponentBuilder(content, NamedTextColor.nearestTo(color), style).build();
}
@Override
protected TextComponent.Builder getComponentBuilder(@Nullable String content, @NotNull TextColor color, @Nullable TextDecoration style) {
TextComponent.Builder builder = text()
.decorations(TextDecoration.NAMES.values(), false)
.color(NamedTextColor.nearestTo(color));
if (content != null) {
builder.append(text(content));
}
if (style != null) {
builder.decorate(style);
}
return builder;
}
@Override
protected TextColor getHexColor(String hexColor) {
TextColor hex = TextColor.fromHexString(hexColor);
return hex != null ? NamedTextColor.nearestTo(hex) : NamedTextColor.WHITE;
}
}

View File

@ -7,6 +7,7 @@ import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
@ -17,49 +18,123 @@ 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, and turn
certain Strings into appropriate LanguageKeys to return a TranslatableComponent.*/
into formatted Components with TextColor and TextDecoration, or return empty Components
or ComponentBuilders with the desired formatting.*/
public class ComponentFactory {
private static ConfigHandler config;
protected TextColor PREFIX; //gold
protected TextColor BRACKETS; //gray
protected TextColor UNDERSCORE; //dark_purple
protected TextColor MSG_MAIN; //medium_blue
protected TextColor MSG_ACCENT; //blue
protected TextColor MSG_MAIN_2; //gold
protected TextColor MSG_ACCENT_2A; //medium_gold
protected TextColor MSG_ACCENT_2B; //light_yellow
protected TextColor HOVER_MSG; //light_blue
protected TextColor CLICKED_MSG; //light_purple
protected TextColor HOVER_ACCENT; //light_gold
public ComponentFactory(ConfigHandler c) {
config = c;
prepareColors();
}
protected void prepareColors() {
PREFIX = PluginColor.GOLD.getColor();
BRACKETS = PluginColor.GRAY.getColor();
UNDERSCORE = PluginColor.DARK_PURPLE.getColor();
MSG_MAIN = PluginColor.MEDIUM_BLUE.getColor();
MSG_ACCENT = PluginColor.BLUE.getColor();
MSG_MAIN_2 = PluginColor.GOLD.getColor();
MSG_ACCENT_2A = PluginColor.MEDIUM_GOLD.getColor();
MSG_ACCENT_2B = PluginColor.LIGHT_YELLOW.getColor();
CLICKED_MSG = PluginColor.LIGHT_PURPLE.getColor();
HOVER_MSG = PluginColor.LIGHT_BLUE.getColor();
HOVER_ACCENT = PluginColor.LIGHT_GOLD.getColor();
}
public TextColor prefix() {
return PREFIX;
}
public TextColor brackets() {
return BRACKETS;
}
public TextColor underscore() {
return UNDERSCORE;
}
public TextColor msgMain() {
return MSG_MAIN;
}
public TextColor msgAccent() {
return MSG_ACCENT;
}
public TextColor msgMain2() {
return MSG_MAIN_2;
}
public TextColor msgAccent2A() {
return MSG_ACCENT_2A;
}
public TextColor msgAccent2B() {
return MSG_ACCENT_2B;
}
public TextColor clickedMsg() {
return CLICKED_MSG;
}
public TextColor hoverMsg() {
return HOVER_MSG;
}
public TextColor hoverAccent() {
return HOVER_ACCENT;
}
public TextColor getExampleNameColor() {
return MSG_ACCENT_2B;
}
public TextColor getSharerNameColor() {
return getColorFromString(config.getSharerNameDecoration(false));
}
/** Returns [PlayerStats]. */
public TextComponent pluginPrefixComponent(boolean isBukkitConsole) {
public TextComponent pluginPrefixComponent() {
return text("[")
.color(PluginColor.GRAY.getColor())
.append(text("PlayerStats").color(PluginColor.GOLD.getColor()))
.color(BRACKETS)
.append(text("PlayerStats").color(PREFIX))
.append(text("]"));
}
/** Returns [PlayerStats] surrounded by underscores on both sides. */
public TextComponent prefixTitleComponent(boolean isBukkitConsole) {
String underscores = "____________"; //12 underscores for both console and in-game
TextColor underscoreColor = isBukkitConsole ?
PluginColor.DARK_PURPLE.getConsoleColor() : PluginColor.DARK_PURPLE.getColor();
return text(underscores).color(underscoreColor)
public TextComponent prefixTitleComponent() {
//12 underscores for both console and in-game
return text("____________").color(UNDERSCORE)
.append(text(" ")) //4 spaces
.append(pluginPrefixComponent(isBukkitConsole))
.append(pluginPrefixComponent())
.append(text(" ")) //4 spaces
.append(text(underscores));
.append(text("____________"));
}
/** Returns a TextComponent with the input String as content, with color Gray and decoration Italic.*/
public TextComponent subTitleComponent(String content) {
return text(content).color(PluginColor.GRAY.getColor()).decorate(TextDecoration.ITALIC);
return text(content).color(BRACKETS).decorate(TextDecoration.ITALIC);
}
/** Returns a TextComponents that represents a full message, with [PlayerStats] prepended. */
/** Returns a TextComponents in the style of a default plugin message, with color Medium_Blue. */
public TextComponent messageComponent() {
return text().color(PluginColor.MEDIUM_BLUE.getColor()).build();
return text().color(MSG_MAIN).build();
}
public TextComponent messageAccentComponent() {
return text().color(MSG_ACCENT).build();
}
public TextComponent.Builder playerNameBuilder(String playerName, Target selection) {
@ -197,15 +272,14 @@ public class ComponentFactory {
if (!(unitName == null && unitKey == null)) {
TextComponent.Builder statUnitBuilder = getComponentBuilder(null,
getColorFromString(config.getSubStatNameDecoration(selection, false)),
getStyleFromString(config.getSubStatNameDecoration(selection, true)))
.append(text("["));
getStyleFromString(config.getSubStatNameDecoration(selection, true)));
if (unitKey != null) {
statUnitBuilder.append(translatable()
.key(unitKey));
} else {
statUnitBuilder.append(text(unitName));
}
return statUnitBuilder.append(text("]")).build();
return surroundingBracketComponent(statUnitBuilder.build());
}
else {
return Component.empty();
@ -224,14 +298,51 @@ public class ComponentFactory {
if (config.useHoverText()) {
heartComponent.hoverEvent(HoverEvent.showText(
text(Unit.HEART.getLabel())
.color(PluginColor.LIGHT_GOLD.getColor())
.decorate(TextDecoration.ITALIC)));
.color(HOVER_ACCENT)));
}
return Component.text().color(PluginColor.GRAY.getColor())
return surroundingBracketComponent(heartComponent.build());
}
public TextComponent shareButtonComponent(UUID shareCode) {
return surroundingBracketComponent(
text("Share")
.color(HOVER_MSG)
.clickEvent(ClickEvent.runCommand("/statshare " + shareCode))
.hoverEvent(HoverEvent.showText(text("Click here to share this statistic in chat!")
.color(HOVER_ACCENT))));
}
public TextComponent hoveringStatResultComponent(TextComponent statResult) {
return surroundingBracketComponent(
text().append(text("Hover Here")
.color(CLICKED_MSG)
.decorate(TextDecoration.ITALIC)
.hoverEvent(HoverEvent.showText(statResult)))
.build());
}
public TextComponent messageSharedComponent(Component playerName) {
return surroundingBracketComponent(
text().append(
getComponent("Shared by",
getColorFromString(config.getSharedByTextDecoration(false)),
getStyleFromString(config.getSharedByTextDecoration(true))))
.append(space())
.append(playerName)
.build());
}
public TextComponent sharerNameComponent(String sharerName) {
return getComponent(sharerName,
getSharerNameColor(),
getStyleFromString(config.getSharerNameDecoration(true)));
}
private TextComponent surroundingBracketComponent(TextComponent component) {
return getComponent(null, BRACKETS, null)
.append(text("["))
.append(heartComponent)
.append(text("]"))
.build();
.append(component)
.append(text("]"));
}
public TextComponent titleComponent(String content, Target selection) {
@ -266,11 +377,11 @@ public class ComponentFactory {
getStyleFromString(config.getDotsDecoration(true)));
}
private TextComponent getComponent(String content, TextColor color, @Nullable TextDecoration style) {
protected TextComponent getComponent(String content, @NotNull TextColor color, @Nullable TextDecoration style) {
return getComponentBuilder(content, color, style).build();
}
private TextComponent.Builder getComponentBuilder(@Nullable String content, TextColor color, @Nullable TextDecoration style) {
protected TextComponent.Builder getComponentBuilder(@Nullable String content, TextColor color, @Nullable TextDecoration style) {
TextComponent.Builder builder = text()
.decorations(TextDecoration.NAMES.values(), false)
.color(color);
@ -287,7 +398,7 @@ public class ComponentFactory {
if (configString != null) {
try {
if (configString.contains("#")) {
return TextColor.fromHexString(configString);
return getHexColor(configString);
}
else {
return getTextColorByName(configString);
@ -300,6 +411,10 @@ public class ComponentFactory {
return null;
}
protected TextColor getHexColor(String hexColor) {
return TextColor.fromHexString(hexColor);
}
private TextColor getTextColorByName(String textColor) {
Index<String, NamedTextColor> names = NamedTextColor.NAMES;
return names.value(textColor);

View File

@ -4,135 +4,296 @@ import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.Unit;
import com.gmail.artemis.the.gr8.playerstats.msg.msgutils.*;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
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;
import org.bukkit.Bukkit;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.function.BiFunction;
import static net.kyori.adventure.text.Component.*;
/** Composes messages to send to Players or Console. This class is responsible
/** 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 ComponentFactory.
By default, this class works with the default ComponentFactory, but you can
give it a different ComponentFactory upon creation.*/
public class MessageWriter {
private static ConfigHandler config;
private static ComponentFactory componentFactory;
protected static ConfigHandler config;
private final ComponentFactory componentFactory;
private final LanguageKeyHandler languageKeyHandler;
private final NumberFormatter formatter;
public MessageWriter(ConfigHandler c) {
config = c;
public MessageWriter(ConfigHandler config) {
this (config, new ComponentFactory(config));
}
public MessageWriter(ConfigHandler configHandler, ComponentFactory factory) {
config = configHandler;
componentFactory = factory;
formatter = new NumberFormatter();
languageKeyHandler = new LanguageKeyHandler();
getComponentFactory();
MyLogger.logMsg("MessageWriter created with factory: " + componentFactory.getClass().getSimpleName(), DebugLevel.MEDIUM);
}
public static void updateComponentFactory() {
getComponentFactory();
}
private static void getComponentFactory() {
if (config.useFestiveFormatting() || config.useRainbowMode()) {
componentFactory = new PrideComponentFactory(config);
}
else {
componentFactory = new ComponentFactory(config);
}
}
public TextComponent reloadedConfig(boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent reloadedConfig() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content("Config reloaded!"));
}
public TextComponent stillReloading(boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent stillReloading() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"The plugin is (re)loading, your request will be processed when it is done!"));
}
public TextComponent waitAMoment(boolean longWait, boolean isBukkitConsole) {
public TextComponent waitAMoment(boolean longWait) {
String msg = longWait ? "Calculating statistics, this may take a minute..." :
"Calculating statistics, this may take a few moments...";
return componentFactory.pluginPrefixComponent(isBukkitConsole)
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(msg));
}
public TextComponent missingStatName(boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent missingStatName() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"Please provide a valid statistic name!"));
}
public TextComponent missingSubStatName(Statistic.Type statType, boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent missingSubStatName(Statistic.Type statType) {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"Please add a valid " + EnumHandler.getSubStatTypeName(statType) + " to look up this statistic!"));
}
public TextComponent missingPlayerName(boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent missingPlayerName() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"Please specify a valid player-name!"));
}
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatEntry, boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatName) {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageAccentComponent().content("\"" + subStatName + "\""))
.append(space())
.append(componentFactory.messageComponent().content(
"\"" + subStatEntry + "\" is not a valid " + EnumHandler.getSubStatTypeName(statType) + "!"));
"is not a valid " + EnumHandler.getSubStatTypeName(statType) + "!"));
}
public TextComponent requestAlreadyRunning(boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
public TextComponent requestAlreadyRunning() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"Please wait for your previous lookup to finish!"));
}
public TextComponent unknownError(boolean isBukkitConsole) {
return componentFactory.pluginPrefixComponent(isBukkitConsole)
//TODO Make this say amount of time left
public TextComponent stillOnShareCoolDown() {
int waitTime = config.getStatShareWaitingTime();
String minutes = waitTime == 1 ? " minute" : " minutes";
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content("You need to wait")
.append(space())
.append(componentFactory.messageAccentComponent()
.content(waitTime + minutes))
.append(space())
.append(text("between sharing!")));
}
public TextComponent resultsAlreadyShared() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content("You already shared these results!"));
}
public TextComponent statResultsTooOld() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"It has been too long since you looked up this statistic, please repeat the original command!"));
}
public TextComponent unknownError() {
return componentFactory.pluginPrefixComponent()
.append(space())
.append(componentFactory.messageComponent().content(
"Something went wrong with your request, " +
"please try again or see /statistic for a usage explanation!"));
}
public TextComponent formatPlayerStat(int stat, @NotNull StatRequest request) {
return Component.text()
public TextComponent usageExamples() {
return new ExampleMessage(componentFactory);
}
public TextComponent helpMsg(boolean isConsoleSender) {
return new HelpMessage(componentFactory,
(!isConsoleSender && config.useHoverText()),
config.getTopListMaxSize());
}
public BiFunction<UUID, CommandSender, TextComponent> formattedPlayerStatFunction(int stat, @NotNull StatRequest request) {
TextComponent playerStat = Component.text()
.append(componentFactory.playerNameBuilder(request.getPlayerName(), Target.PLAYER)
.append(text(":"))
.append(space()))
.append(getStatNumberComponent(request.getStatistic(), stat, Target.PLAYER, request.isConsoleSender()))
.append(space())
.append(getStatNameComponent(request))
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender()))
.build(); //space is provided by statUnitComponent
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender())) //space is provided by statUnitComponent
.build();
return getFormattingFunction(playerStat, Target.PLAYER);
}
public TextComponent formatTopStats(@NotNull LinkedHashMap<String, Integer> topStats, @NotNull StatRequest request) {
TextComponent.Builder topList = Component.text()
.append(newline())
.append(componentFactory.pluginPrefixComponent(request.isBukkitConsoleSender())).append(space())
.append(componentFactory.titleComponent(config.getTopStatsTitle(), Target.TOP)).append(space())
.append(componentFactory.titleNumberComponent(topStats.size())).append(space())
.append(getStatNameComponent(request)) //space is provided by statUnitComponent
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender()));
public BiFunction<UUID, CommandSender, TextComponent> formattedServerStatFunction(long stat, @NotNull StatRequest request) {
TextComponent serverStat = text()
.append(componentFactory.titleComponent(config.getServerTitle(), Target.SERVER))
.append(space())
.append(componentFactory.serverNameComponent(config.getServerName()))
.append(space())
.append(getStatNumberComponent(request.getStatistic(), stat, Target.SERVER, request.isConsoleSender()))
.append(space())
.append(getStatNameComponent(request))
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender())) //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 = getTopStatsTitle(request, topStats.size());
final TextComponent shortTitle = getTopStatsTitleShort(request, topStats.size());
final TextComponent list = getTopStatList(topStats, request);
final boolean useEnters = config.useEnters(Target.TOP, false);
final boolean useEntersForShared = config.useEnters(Target.TOP, true);
return (shareCode, sender) -> {
TextComponent.Builder topBuilder = text();
//if we're adding a share-button
if (shareCode != null) {
if (useEnters) {
topBuilder.append(newline());
}
topBuilder.append(title)
.append(space())
.append(componentFactory.shareButtonComponent(shareCode))
.append(list);
}
//if we're adding a "shared by" component
else if (sender != null) {
if (useEntersForShared) {
topBuilder.append(newline());
}
topBuilder.append(shortTitle)
.append(space())
.append(componentFactory.hoveringStatResultComponent(text()
.append(title)
.append(list)
.build()))
.append(newline())
.append(componentFactory.messageSharedComponent(
getSharerNameComponent(sender)));
}
//if we're not adding a share-button or a "shared by" component
else {
if (useEnters) {
topBuilder.append(newline());
}
topBuilder.append(title)
.append(list);
}
return topBuilder.build();
};
}
private BiFunction<UUID, CommandSender, TextComponent> getFormattingFunction(@NotNull TextComponent statResult, Target selection) {
boolean useEnters = config.useEnters(selection, false);
boolean useEntersForShared = config.useEnters(selection, true);
return (shareCode, sender) -> {
TextComponent.Builder statBuilder = text();
//if we're adding a share-button
if (shareCode != null) {
if (useEnters) {
statBuilder.append(newline());
}
statBuilder.append(statResult)
.append(space())
.append(componentFactory.shareButtonComponent(shareCode));
}
//if we're adding a "shared by" component
else if (sender != null) {
if (useEntersForShared) {
statBuilder.append(newline());
}
statBuilder.append(statResult)
.append(newline())
.append(componentFactory.messageSharedComponent(
getSharerNameComponent(sender)));
}
//if we're not adding a share-button or a "shared by" component
else {
if (useEnters) {
statBuilder.append(newline());
}
statBuilder.append(statResult);
}
return statBuilder.build();
};
}
private Component getSharerNameComponent(CommandSender sender) {
if (sender instanceof Player player) {
Component senderName = EasterEggProvider.getPlayerName(player);
if (senderName != null) {
return senderName;
}
}
return componentFactory.sharerNameComponent(sender.getName());
}
private TextComponent getTopStatsTitle(StatRequest request, int statListSize) {
return Component.text()
.append(componentFactory.pluginPrefixComponent()).append(space())
.append(componentFactory.titleComponent(config.getTopStatsTitle(), Target.TOP)).append(space())
.append(componentFactory.titleNumberComponent(statListSize)).append(space())
.append(getStatNameComponent(request)) //space is provided by statUnitComponent
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender()))
.build();
}
private TextComponent getTopStatsTitleShort(StatRequest request, int statListSize) {
return Component.text()
.append(componentFactory.titleComponent(config.getTopStatsTitle(), Target.TOP)).append(space())
.append(componentFactory.titleNumberComponent(statListSize)).append(space())
.append(getStatNameComponent(request)) //space is provided by statUnitComponent
.build();
}
private TextComponent getTopStatList(LinkedHashMap<String, Integer> topStats, StatRequest request) {
TextComponent.Builder topList = Component.text();
boolean useDots = config.useDots();
boolean boldNames = config.playerNameIsBold();
Set<String> playerNames = topStats.keySet();
@ -141,7 +302,7 @@ public class MessageWriter {
for (String playerName : playerNames) {
TextComponent.Builder playerNameBuilder = componentFactory.playerNameBuilder(playerName, Target.TOP);
topList.append(newline())
.append(componentFactory.rankingNumberComponent(++count + "."))
.append(componentFactory.rankingNumberComponent(" " + ++count + "."))
.append(space());
if (useDots) {
topList.append(playerNameBuilder)
@ -159,30 +320,6 @@ public class MessageWriter {
return topList.build();
}
public TextComponent formatServerStat(long stat, @NotNull StatRequest request) {
return Component.text()
.append(componentFactory.titleComponent(config.getServerTitle(), Target.SERVER))
.append(space())
.append(componentFactory.serverNameComponent(config.getServerName()))
.append(space())
.append(getStatNumberComponent(request.getStatistic(), stat, Target.SERVER, request.isConsoleSender()))
.append(space())
.append(getStatNameComponent(request))
.append(getStatUnitComponent(request.getStatistic(), request.getSelection(), request.isConsoleSender())) //space is provided by statUnit
.build();
}
public TextComponent usageExamples(boolean isBukkitConsole) {
return new ExampleMessage(componentFactory, isBukkitConsole);
}
public TextComponent helpMsg(boolean isConsoleSender) {
return new HelpMessage(componentFactory,
config.useHoverText() && !isConsoleSender,
isConsoleSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit"),
config.getTopListMaxSize());
}
/** 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) {

View File

@ -0,0 +1,158 @@
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.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.models.StatRequest;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import 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 class OutputManager {
private static BukkitAudiences adventure;
private static ShareManager shareManager;
private static MessageWriter msg;
private static MessageWriter consoleMsg;
private static EnumMap<StandardMessage, Function<MessageWriter, TextComponent>> standardMessages;
public OutputManager(ConfigHandler config) {
adventure = Main.adventure();
shareManager = ShareManager.getInstance(config);
getMessageWriters(config);
prepareFunctions();
}
public void updateMessageWriters(ConfigHandler config) {
getMessageWriters(config);
}
public void sendFeedbackMsg(CommandSender sender, StandardMessage message) {
if (message != null) {
adventure.sender(sender).sendMessage(standardMessages.get(message)
.apply(getWriter(sender)));
}
}
public void sendFeedbackMsgWaitAMoment(CommandSender sender, boolean longWait) {
adventure.sender(sender).sendMessage(getWriter(sender)
.waitAMoment(longWait));
}
public void sendFeedbackMsgMissingSubStat(CommandSender sender, Statistic.Type statType) {
adventure.sender(sender).sendMessage(getWriter(sender)
.missingSubStatName(statType));
}
public void sendFeedbackMsgWrongSubStat(CommandSender sender, Statistic.Type statType, String subStatName) {
if (subStatName == null) {
sendFeedbackMsgMissingSubStat(sender, statType);
} else {
adventure.sender(sender).sendMessage(getWriter(sender)
.wrongSubStatType(statType, subStatName));
}
}
public void sendExamples(CommandSender sender) {
adventure.sender(sender).sendMessage(getWriter(sender)
.usageExamples());
}
public void sendHelp(CommandSender sender) {
adventure.sender(sender).sendMessage(getWriter(sender)
.helpMsg(sender instanceof ConsoleCommandSender));
}
public void shareStatResults(@NotNull TextComponent statResult) {
adventure.players().sendMessage(statResult);
}
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 sendServerStat(@NotNull StatRequest request, long serverStat) {
CommandSender sender = request.getCommandSender();
BiFunction<UUID, CommandSender, TextComponent> buildFunction =
getWriter(sender).formattedServerStatFunction(serverStat, request);
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));
}
else {
adventure.sender(sender).sendMessage(
buildFunction.apply(null, null));
}
}
private MessageWriter getWriter(CommandSender sender) {
return sender instanceof ConsoleCommandSender ? consoleMsg : msg;
}
private void getMessageWriters(ConfigHandler config) {
boolean isBukkit = Bukkit.getName().equalsIgnoreCase("CraftBukkit");
if (config.useRainbowMode() ||
(config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE))) {
msg = new MessageWriter(config, new PrideComponentFactory(config));
}
else {
msg = new MessageWriter(config);
}
if (!isBukkit) {
consoleMsg = msg;
} else {
consoleMsg = new MessageWriter(config, new BukkitConsoleComponentFactory(config));
}
}
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));
}
}

View File

@ -2,66 +2,95 @@ package com.gmail.artemis.the.gr8.playerstats.msg;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import java.time.LocalDate;
import java.time.Month;
import java.util.Random;
import static net.kyori.adventure.text.Component.*;
public class PrideComponentFactory extends ComponentFactory {
private static ConfigHandler config;
public PrideComponentFactory(ConfigHandler c) {
super(c);
config = c;
}
@Override
public TextComponent prefixTitleComponent(boolean isBukkitConsole) {
if (cancelRainbow(isBukkitConsole)) {
return super.prefixTitleComponent(isBukkitConsole);
}
else {
String title = "<rainbow:16>____________ [PlayerStats] ____________</rainbow>"; //12 underscores
return text()
.append(MiniMessage.miniMessage().deserialize(title))
.build();
}
}
@Override
public TextComponent pluginPrefixComponent(boolean isConsoleSender) {
if (cancelRainbow(isConsoleSender)) {
return super.pluginPrefixComponent(isConsoleSender);
}
protected void prepareColors() {
PREFIX = PluginColor.GOLD.getColor();
BRACKETS = PluginColor.GRAY.getColor();
UNDERSCORE = PluginColor.DARK_PURPLE.getColor();
MSG_MAIN = PluginColor.GRAY.getColor(); //difference 1
MSG_ACCENT = PluginColor.LIGHT_GOLD.getColor(); //difference 2
MSG_MAIN_2 = PluginColor.GOLD.getColor();
MSG_ACCENT_2A = PluginColor.MEDIUM_GOLD.getColor();
MSG_ACCENT_2B = PluginColor.LIGHT_YELLOW.getColor();
CLICKED_MSG = PluginColor.LIGHT_PURPLE.getColor();
HOVER_MSG = PluginColor.LIGHT_BLUE.getColor();
HOVER_ACCENT = PluginColor.LIGHT_GOLD.getColor();
}
@Override
public TextColor getExampleNameColor() {
return getSharerNameColor();
}
@Override
public TextColor getSharerNameColor() {
return PluginColor.getRandomNameColor();
}
@Override
public TextComponent prefixTitleComponent() {
String title = "<rainbow:16>____________ [PlayerStats] ____________</rainbow>"; //12 underscores
return text()
.append(MiniMessage.miniMessage()
.deserialize("<#fe3e3e>[</#fe3e3e>" +
"<#fe5640>P</#fe5640>" +
"<#f67824>l</#f67824>" +
"<#ee8a19>a</#ee8a19>" +
"<#e49b0f>y</#e49b0f>" +
"<#cbbd03>e</#cbbd03>" +
"<#bccb01>r</#bccb01>" +
"<#8aee08>S</#8aee08>" +
"<#45fe31>t</#45fe31>" +
"<#01c1a7>a</#01c1a7>" +
"<#0690d4>t</#0690d4>" +
"<#205bf3>s</#205bf3>" +
"<#6c15fa>]</#6c15fa>"))
.append(MiniMessage.miniMessage().deserialize(title))
.build();
}
/** Don't use rainbow formatting if the rainbow Prefix is disabled,
if festive formatting is disabled or it is not pride month,
or the commandsender is a Bukkit or Spigot console.*/
private boolean cancelRainbow(boolean isBukkitConsole) {
return !(config.useRainbowMode() || (config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE))) ||
(isBukkitConsole);
@Override
public TextComponent pluginPrefixComponent() {
Random randomizer = new Random();
if (randomizer.nextBoolean()) {
return backwardsPluginPrefixComponent();
}
return text()
.append(MiniMessage.miniMessage()
.deserialize("<#f74040>[</#f74040>" +
"<#F54D39>P</#F54D39>" +
"<#F16E28>l</#F16E28>" +
"<#ee8a19>a</#ee8a19>" +
"<#EEA019>y</#EEA019>" +
"<#F7C522>e</#F7C522>" +
"<#C1DA15>r</#C1DA15>" +
"<#84D937>S</#84D937>" +
"<#46D858>t</#46D858>" +
"<#01c1a7>a</#01c1a7>" +
"<#1F8BEB>t</#1F8BEB>" +
"<#3341E6>s</#3341E6>" +
"<#631ae6>]</#631ae6>"))
.build();
}
public TextComponent backwardsPluginPrefixComponent() {
return text()
.append(MiniMessage.miniMessage()
.deserialize("<#631ae6>[</#631ae6>" +
"<#3341E6>P</#3341E6>" +
"<#1F8BEB>l</#1F8BEB>" +
"<#01c1a7>a</#01c1a7>" +
"<#46D858>y</#46D858>" +
"<#84D937>e</#84D937>" +
"<#C1DA15>r</#C1DA15>" +
"<#F7C522>S</#F7C522>" +
"<#EEA019>t</#EEA019>" +
"<#ee8a19>a</#ee8a19>" +
"<#f67824>t</#f67824>" +
"<#f76540>s</#f76540>" +
"<#f74040>]</#f74040>"))
.build();
}
}

View File

@ -0,0 +1,86 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.entity.Player;
import java.util.Random;
//This class is just for fun, and adds some silly names for players on my server.
//It does not impact the rest of the plugin, and will only be used for the players mentioned in here.
public class EasterEggProvider {
private static final Random random;
static{
random = new Random();
}
public static Component getPlayerName(Player player) {
int sillyNumber = getSillyNumber();
String playerName = null;
switch (player.getUniqueId().toString()) {
case "8fb811dc-2ceb-4528-9951-cf803e0550a1" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<bold><#7d330e><#D17300>b</#D17300>e<#D17300>e</#D17300> b<#D17300>o</#D17300>i";
}
}
case "b7d2e46f-cc89-434c-9757-f71a681e168a" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<gradient:#7402d1:#e31bc5:#7402d1>purple slime</gradient>";
}
}
case "46dd0c5a-2b51-4ee6-80e8-29deca6dedc1" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<gradient:#f74040:#FF6600:#f74040>fire demon</gradient>";
}
else if (sillyNumberIsBetween(sillyNumber, 69, 69)) {
playerName = "<gradient:blue:#b01bd1:blue>best admin</gradient>";
}
}
case "0dc5336b-acd2-4dc3-a5e9-0aa9b8f113f7" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<gradient:#f73bdb:#fc8bec:#f73bdb>an UwU sister</gradient>";
}
}
case "10dd9f02-5ec2-4f60-816c-48bb9e2ddf47" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<gradient:gold:#fc7f03:-1>gottem</gradient>";
}
}
case "e4c5dfef-bbcc-4012-9f74-879d28fff431" -> {
if (sillyNumberIsBetween(sillyNumber, 69, 69)) {
playerName = "<gradient:blue:#03befc:blue>nice admin</gradient>";
}
}
case "29c0911d-695a-4c31-817f-3a065a7144b7" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<gradient:gold:#00ff7b:#03b6fc>Tzzzzzzzzz</gradient>";
}
}
case "0410f9c7-f042-479c-ac80-49d46be655e9" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 20)) {
playerName = "<gradient:gold:#ff245e:#a511f0:#7c0aff>SamanthaCation</gradient>";
}
}
case "0bd803b6-f6c2-41bd-9872-74d8754a29fd" -> {
if (sillyNumberIsBetween(sillyNumber, 0, 30)) {
playerName = "<gradient:#14f7a0:#4287f5>Bradwurst</gradient>";
}
}
}
if (playerName == null) {
return null;
} else {
return MiniMessage.miniMessage().deserialize(playerName);
}
}
private static int getSillyNumber() {
return random.nextInt(100);
}
private static boolean sillyNumberIsBetween(int sillyNumber, int lowerBound, int upperBound) {
return sillyNumber >= lowerBound && sillyNumber <= upperBound;
}
}

View File

@ -1,12 +1,11 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import com.gmail.artemis.the.gr8.playerstats.msg.BukkitConsoleComponentFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.ComponentFactory;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
@ -19,37 +18,35 @@ public class ExampleMessage implements TextComponent {
private final TextComponent exampleMessage;
private final ComponentFactory componentFactory;
public ExampleMessage(ComponentFactory componentFactory, boolean isBukkitConsole) {
this.componentFactory = componentFactory;
exampleMessage = getExampleMessage(isBukkitConsole);
public ExampleMessage(ComponentFactory factory) {
componentFactory = factory;
exampleMessage = getExampleMessage();
}
public TextComponent getExampleMessage(boolean isBukkitConsole) {
TextColor mainColor = isBukkitConsole ? PluginColor.GOLD.getConsoleColor() : PluginColor.GOLD.getColor();
TextColor accentColor1 = isBukkitConsole ? PluginColor.MEDIUM_GOLD.getConsoleColor() : PluginColor.MEDIUM_GOLD.getColor();
TextColor accentColor3 = isBukkitConsole ? PluginColor.LIGHT_YELLOW.getConsoleColor() : PluginColor.LIGHT_YELLOW.getColor();
String arrow = isBukkitConsole ? " -> " : ""; //4 spaces, alt + 26, 1 space
public TextComponent getExampleMessage() {
String arrow = componentFactory instanceof BukkitConsoleComponentFactory ? " -> " : ""; //4 spaces, alt + 26, 1 space
return Component.newline()
.append(componentFactory.prefixTitleComponent(isBukkitConsole))
.append(componentFactory.prefixTitleComponent())
.append(Component.newline())
.append(text("Examples: ").color(mainColor))
.append(text("Examples: ").color(componentFactory.msgMain2()))
.append(Component.newline())
.append(text(arrow).color(mainColor)
.append(text(arrow).color(componentFactory.msgMain2())
.append(text("/statistic ")
.append(text("animals_bred ").color(accentColor1)
.append(text("top").color(accentColor3)))))
.append(text("animals_bred ").color(componentFactory.msgAccent2A())
.append(text("top").color(componentFactory.msgAccent2B())))))
.append(Component.newline())
.append(text(arrow).color(mainColor)
.append(text(arrow).color(componentFactory.msgMain2())
.append(text("/statistic ")
.append(text("mine_block diorite ").color(accentColor1)
.append(text("me").color(accentColor3)))))
.append(text("mine_block diorite ").color(componentFactory.msgAccent2A())
.append(text("me").color(componentFactory.msgAccent2B())))))
.append(Component.newline())
.append(text(arrow).color(mainColor)
.append(text(arrow).color(componentFactory.msgMain2())
.append(text("/statistic ")
.append(text("deaths ").color(accentColor1)
.append(text("player ").color(accentColor3)
.append(text("Artemis_the_gr8"))))));
.append(text("deaths ").color(componentFactory.msgAccent2A())
.append(text("player ").color(componentFactory.msgAccent2B())
.append(text("Artemis_the_gr8")
.color(componentFactory.getExampleNameColor()))))));
}
@Override
@ -86,4 +83,4 @@ public class ExampleMessage implements TextComponent {
public @NotNull TextComponent style(@NotNull Style style) {
return exampleMessage.style(style);
}
}
}

View File

@ -16,4 +16,4 @@ public final class FontUtils {
return (int) Math.round((130.0 - (MinecraftFont.Font.getWidth(displayText) * 1.5))/2);
}
}
}
}

View File

@ -1,14 +1,12 @@
package com.gmail.artemis.the.gr8.playerstats.msg.msgutils;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import com.gmail.artemis.the.gr8.playerstats.msg.BukkitConsoleComponentFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.ComponentFactory;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Unmodifiable;
@ -21,145 +19,122 @@ public class HelpMessage implements TextComponent {
private final ComponentFactory componentFactory;
private final TextComponent helpMessage;
boolean isBukkitConsole;
TextColor GRAY;
TextColor DARK_PURPLE;
TextColor GOLD;
TextColor MEDIUM_GOLD;
TextColor LIGHT_GOLD;
TextColor LIGHT_BLUE;
public HelpMessage(ComponentFactory componentFactory, boolean useHover, boolean isBukkitConsole, int listSize) {
public HelpMessage(ComponentFactory componentFactory, boolean useHover, int listSize) {
this.componentFactory = componentFactory;
this.isBukkitConsole = isBukkitConsole;
getPluginColors(isBukkitConsole);
if (!useHover || isBukkitConsole) {
helpMessage = getPlainHelpMsg(isBukkitConsole, listSize);
if (!useHover) {
helpMessage = getPlainHelpMsg(listSize);
} else {
helpMessage = helpMsgHover(listSize);
}
}
private TextComponent getPlainHelpMsg(boolean isBukkitConsole, int listSize) {
String arrowSymbol = isBukkitConsole ? "->" : ""; //alt + 26
String bulletSymbol = isBukkitConsole ? "*" : ""; //alt + 7
private TextComponent getPlainHelpMsg(int listSize) {
String arrowSymbol = ""; //alt + 26
String bulletSymbol = ""; //alt + 7
if (componentFactory instanceof BukkitConsoleComponentFactory) {
arrowSymbol = "->";
bulletSymbol = "*";
}
TextComponent spaces = text(" "); //4 spaces
TextComponent arrow = text(arrowSymbol).color(NamedTextColor.GOLD);
TextComponent bullet = text(bulletSymbol).color(NamedTextColor.GOLD);
TextComponent arrow = text(arrowSymbol).color(componentFactory.msgMain2());
TextComponent bullet = text(bulletSymbol).color(componentFactory.msgMain2());
return Component.newline()
.append(componentFactory.prefixTitleComponent(isBukkitConsole))
.append(componentFactory.prefixTitleComponent())
.append(newline())
.append(text("Type \"/statistic examples\" to see examples!").color(GRAY).decorate(TextDecoration.ITALIC))
.append(text("Type \"/statistic examples\" to see examples!").color(componentFactory.brackets()).decorate(TextDecoration.ITALIC))
.append(newline())
.append(text("Usage:").color(GOLD)).append(space())
.append(text("/statistic").color(LIGHT_GOLD))
.append(text("Usage:").color(componentFactory.msgMain2())).append(space())
.append(text("/statistic").color(componentFactory.hoverAccent()))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("name").color(LIGHT_GOLD))
.append(text("name").color(componentFactory.hoverAccent()))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("{sub-statistic}").color(LIGHT_GOLD)).append(space())
.append(text("(a block, item or entity)").color(GRAY))
.append(text("{sub-statistic}").color(componentFactory.hoverAccent())).append(space())
.append(text("(a block, item or entity)").color(componentFactory.brackets()))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("me | player | server | top").color(LIGHT_GOLD))
.append(text("me | player | server | top").color(componentFactory.hoverAccent()))
.append(newline())
.append(spaces).append(spaces).append(bullet).append(space())
.append(text("me:").color(MEDIUM_GOLD)).append(space())
.append(text("your own statistic").color(GRAY))
.append(text("me:").color(componentFactory.msgAccent2A())).append(space())
.append(text("your own statistic").color(componentFactory.brackets()))
.append(newline())
.append(spaces).append(spaces).append(bullet).append(space())
.append(text("player:").color(MEDIUM_GOLD)).append(space())
.append(text("choose a player").color(GRAY))
.append(text("player:").color(componentFactory.msgAccent2A())).append(space())
.append(text("choose a player").color(componentFactory.brackets()))
.append(newline())
.append(spaces).append(spaces).append(bullet).append(space())
.append(text("server:").color(MEDIUM_GOLD)).append(space())
.append(text("everyone on the server combined").color(GRAY))
.append(text("server:").color(componentFactory.msgAccent2A())).append(space())
.append(text("everyone on the server combined").color(componentFactory.brackets()))
.append(newline())
.append(spaces).append(spaces).append(bullet).append(space())
.append(text("top:").color(MEDIUM_GOLD)).append(space())
.append(text("the top").color(GRAY).append(space()).append(text(listSize)))
.append(text("top:").color(componentFactory.msgAccent2A())).append(space())
.append(text("the top").color(componentFactory.brackets()).append(space()).append(text(listSize)))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("{player-name}").color(LIGHT_GOLD));
.append(text("{player-name}").color(componentFactory.hoverAccent()));
}
private TextComponent helpMsgHover(int listSize) {
TextComponent spaces = text(" ");
TextComponent arrow = text("").color(GOLD);
TextComponent arrow = text("").color(componentFactory.msgMain2());
return Component.newline()
.append(componentFactory.prefixTitleComponent(false))
.append(componentFactory.prefixTitleComponent())
.append(newline())
.append(componentFactory.subTitleComponent("Hover over the arguments for more information!"))
.append(newline())
.append(text("Usage:").color(GOLD)).append(space())
.append(text("/statistic").color(LIGHT_GOLD))
.append(text("Usage:").color(componentFactory.msgMain2())).append(space())
.append(text("/statistic").color(componentFactory.hoverAccent()))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("name").color(LIGHT_GOLD)
.hoverEvent(HoverEvent.showText(text("The name that describes the statistic").color(LIGHT_BLUE)
.append(text("name").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(text("The name that describes the statistic").color(componentFactory.hoverMsg())
.append(newline())
.append(text("Example: ").color(GOLD))
.append(text("\"animals_bred\"").color(LIGHT_GOLD)))))
.append(text("Example: ").color(componentFactory.msgMain2()))
.append(text("\"animals_bred\"").color(componentFactory.hoverAccent())))))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("sub-statistic").color(LIGHT_GOLD)
.append(text("sub-statistic").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(
text("Some statistics need an item, block or entity as extra input").color(LIGHT_BLUE)
text("Some statistics need an item, block or entity as extra input").color(componentFactory.hoverMsg())
.append(newline())
.append(text("Example: ").color(GOLD)
.append(text("\"mine_block diorite\"").color(LIGHT_GOLD))))))
.append(text("Example: ").color(componentFactory.msgMain2())
.append(text("\"mine_block diorite\"").color(componentFactory.hoverAccent()))))))
.append(newline())
.append(spaces).append(arrow
.hoverEvent(HoverEvent.showText(
text("Choose one").color(DARK_PURPLE)))).append(space())
.append(text("me").color(LIGHT_GOLD)
text("Choose one").color(componentFactory.underscore())))).append(space())
.append(text("me").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(
text("See your own statistic").color(LIGHT_BLUE))))
.append(text(" | ").color(LIGHT_GOLD))
.append(text("player").color(LIGHT_GOLD)
text("See your own statistic").color(componentFactory.hoverMsg()))))
.append(text(" | ").color(componentFactory.hoverAccent()))
.append(text("player").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(
text("Choose any player that has played on your server").color(LIGHT_BLUE))))
.append(text(" | ").color(LIGHT_GOLD))
.append(text("server").color(LIGHT_GOLD)
text("Choose any player that has played on your server").color(componentFactory.hoverMsg()))))
.append(text(" | ").color(componentFactory.hoverAccent()))
.append(text("server").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(
text("See the combined total for everyone on your server").color(LIGHT_BLUE))))
.append(text(" | ").color(LIGHT_GOLD))
.append(text("top").color(LIGHT_GOLD)
text("See the combined total for everyone on your server").color(componentFactory.hoverMsg()))))
.append(text(" | ").color(componentFactory.hoverAccent()))
.append(text("top").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(
text("See the top").color(LIGHT_BLUE).append(space())
text("See the top").color(componentFactory.hoverMsg()).append(space())
.append(text(listSize)))))
.append(newline())
.append(spaces).append(arrow).append(space())
.append(text("player-name").color(LIGHT_GOLD)
.append(text("player-name").color(componentFactory.hoverAccent())
.hoverEvent(HoverEvent.showText(
text("In case you typed").color(LIGHT_BLUE).append(space())
.append(text("\"player\"").color(LIGHT_GOLD))
text("In case you typed").color(componentFactory.hoverMsg()).append(space())
.append(text("\"player\"").color(componentFactory.hoverAccent()))
.append(text(", add the player's name")))));
}
private void getPluginColors(boolean isBukkitConsole) {
if (isBukkitConsole) {
GRAY = PluginColor.GRAY.getConsoleColor();
DARK_PURPLE = PluginColor.DARK_PURPLE.getConsoleColor();
GOLD = PluginColor.GOLD.getConsoleColor();
MEDIUM_GOLD = PluginColor.MEDIUM_GOLD.getConsoleColor();
LIGHT_GOLD = PluginColor.LIGHT_GOLD.getConsoleColor();
LIGHT_BLUE = PluginColor.LIGHT_BLUE.getConsoleColor();
} else {
GRAY = PluginColor.GRAY.getColor();
DARK_PURPLE = PluginColor.DARK_PURPLE.getColor();
GOLD = PluginColor.GOLD.getColor();
MEDIUM_GOLD = PluginColor.MEDIUM_GOLD.getColor();
LIGHT_GOLD = PluginColor.LIGHT_GOLD.getColor();
LIGHT_BLUE = PluginColor.LIGHT_BLUE.getColor();
}
}
@Override
public @NotNull String content() {
return helpMessage.content();

View File

@ -13,11 +13,10 @@ import java.util.HashMap;
public final class LanguageKeyHandler {
private final HashMap<Statistic, String> statNameKeys;
private static HashMap<Statistic, String> statNameKeys;
public LanguageKeyHandler() {
statNameKeys = new HashMap<>();
generateStatNameKeys();
statNameKeys = generateStatNameKeys();
}
public String getStatKey(@NotNull Statistic statistic) {
@ -72,47 +71,46 @@ public final class LanguageKeyHandler {
}
}
private void generateDefaultKeys() {
Arrays.stream(Statistic.values()).forEach(statistic -> statNameKeys.put(statistic, statistic.toString().toLowerCase()));
}
private void generateStatNameKeys() {
private @NotNull HashMap<Statistic, String> generateStatNameKeys() {
//get the enum names for all statistics first
generateDefaultKeys();
HashMap<Statistic, String> statNames = new HashMap<>(Statistic.values().length);
Arrays.stream(Statistic.values()).forEach(statistic -> statNames.put(statistic, statistic.toString().toLowerCase()));
//replace the ones for which the language key is different from the enum name
statNameKeys.put(Statistic.ARMOR_CLEANED, "clean_armor");
statNameKeys.put(Statistic.BANNER_CLEANED, "clean_banner");
statNameKeys.put(Statistic.DROP_COUNT, "drop");
statNameKeys.put(Statistic.CAKE_SLICES_EATEN, "eat_cake_slice");
statNameKeys.put(Statistic.ITEM_ENCHANTED, "enchant_item");
statNameKeys.put(Statistic.CAULDRON_FILLED, "fill_cauldron");
statNameKeys.put(Statistic.DISPENSER_INSPECTED, "inspect_dispenser");
statNameKeys.put(Statistic.DROPPER_INSPECTED, "inspect_dropper");
statNameKeys.put(Statistic.HOPPER_INSPECTED, "inspect_hopper");
statNameKeys.put(Statistic.BEACON_INTERACTION, "interact_with_beacon");
statNameKeys.put(Statistic.BREWINGSTAND_INTERACTION, "interact_with_brewingstand");
statNameKeys.put(Statistic.CRAFTING_TABLE_INTERACTION, "interact_with_crafting_table");
statNameKeys.put(Statistic.FURNACE_INTERACTION, "interact_with_furnace");
statNameKeys.put(Statistic.CHEST_OPENED, "open_chest");
statNameKeys.put(Statistic.ENDERCHEST_OPENED, "open_enderchest");
statNameKeys.put(Statistic.SHULKER_BOX_OPENED, "open_shulker_box");
statNameKeys.put(Statistic.NOTEBLOCK_PLAYED, "play_noteblock");
statNameKeys.put(Statistic.PLAY_ONE_MINUTE, "play_time");
statNameKeys.put(Statistic.RECORD_PLAYED, "play_record");
statNameKeys.put(Statistic.FLOWER_POTTED, "pot_flower");
statNameKeys.put(Statistic.TRAPPED_CHEST_TRIGGERED, "trigger_trapped_chest");
statNameKeys.put(Statistic.NOTEBLOCK_TUNED, "tune_noteblock");
statNameKeys.put(Statistic.CAULDRON_USED, "use_cauldron");
statNames.put(Statistic.ARMOR_CLEANED, "clean_armor");
statNames.put(Statistic.BANNER_CLEANED, "clean_banner");
statNames.put(Statistic.DROP_COUNT, "drop");
statNames.put(Statistic.CAKE_SLICES_EATEN, "eat_cake_slice");
statNames.put(Statistic.ITEM_ENCHANTED, "enchant_item");
statNames.put(Statistic.CAULDRON_FILLED, "fill_cauldron");
statNames.put(Statistic.DISPENSER_INSPECTED, "inspect_dispenser");
statNames.put(Statistic.DROPPER_INSPECTED, "inspect_dropper");
statNames.put(Statistic.HOPPER_INSPECTED, "inspect_hopper");
statNames.put(Statistic.BEACON_INTERACTION, "interact_with_beacon");
statNames.put(Statistic.BREWINGSTAND_INTERACTION, "interact_with_brewingstand");
statNames.put(Statistic.CRAFTING_TABLE_INTERACTION, "interact_with_crafting_table");
statNames.put(Statistic.FURNACE_INTERACTION, "interact_with_furnace");
statNames.put(Statistic.CHEST_OPENED, "open_chest");
statNames.put(Statistic.ENDERCHEST_OPENED, "open_enderchest");
statNames.put(Statistic.SHULKER_BOX_OPENED, "open_shulker_box");
statNames.put(Statistic.NOTEBLOCK_PLAYED, "play_noteblock");
statNames.put(Statistic.PLAY_ONE_MINUTE, "play_time");
statNames.put(Statistic.RECORD_PLAYED, "play_record");
statNames.put(Statistic.FLOWER_POTTED, "pot_flower");
statNames.put(Statistic.TRAPPED_CHEST_TRIGGERED, "trigger_trapped_chest");
statNames.put(Statistic.NOTEBLOCK_TUNED, "tune_noteblock");
statNames.put(Statistic.CAULDRON_USED, "use_cauldron");
//do the same for the statistics that have a subtype
statNameKeys.put(Statistic.DROP, "dropped");
statNameKeys.put(Statistic.PICKUP, "picked_up");
statNameKeys.put(Statistic.MINE_BLOCK, "mined");
statNameKeys.put(Statistic.USE_ITEM, "used");
statNameKeys.put(Statistic.BREAK_ITEM, "broken");
statNameKeys.put(Statistic.CRAFT_ITEM, "crafted");
statNameKeys.put(Statistic.KILL_ENTITY, "killed");
statNameKeys.put(Statistic.ENTITY_KILLED_BY, "killed_by");
statNames.put(Statistic.DROP, "dropped");
statNames.put(Statistic.PICKUP, "picked_up");
statNames.put(Statistic.MINE_BLOCK, "mined");
statNames.put(Statistic.USE_ITEM, "used");
statNames.put(Statistic.BREAK_ITEM, "broken");
statNames.put(Statistic.CRAFT_ITEM, "crafted");
statNames.put(Statistic.KILL_ENTITY, "killed");
statNames.put(Statistic.ENTITY_KILLED_BY, "killed_by");
return statNames;
}
}

View File

@ -22,4 +22,4 @@ public final class StringUtils {
}
return capitals.toString();
}
}
}

View File

@ -1,5 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.UnixTimeHandler;
import org.bukkit.OfflinePlayer;
@ -8,9 +9,9 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
public class ReloadAction extends RecursiveAction {
public final class ReloadAction extends RecursiveAction {
private final int threshold;
private static int threshold;
private final OfflinePlayer[] players;
private final int start;
@ -20,21 +21,20 @@ public class ReloadAction extends RecursiveAction {
private final ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
/** Fills a ConcurrentHashMap with PlayerNames and UUIDs for all OfflinePlayers that should be included in statistic calculations.
* @param threshold the maximum length of OfflinePlayers to process in one task
* @param players array of all OfflinePlayers (straight from Bukkit)
* @param lastPlayedLimit whether to set a limit based on last-played-date
* @param offlinePlayerUUIDs the ConcurrentHashMap to put resulting playerNames and UUIDs on
*/
public ReloadAction(int threshold, OfflinePlayer[] players,
public ReloadAction(OfflinePlayer[] players,
int lastPlayedLimit, ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
this(threshold, players, 0, players.length, lastPlayedLimit, offlinePlayerUUIDs);
this(players, 0, players.length, lastPlayedLimit, offlinePlayerUUIDs);
}
protected ReloadAction(int threshold, OfflinePlayer[] players, int start, int end,
private ReloadAction(OfflinePlayer[] players, int start, int end,
int lastPlayedLimit, ConcurrentHashMap<String, UUID> offlinePlayerUUIDs) {
threshold = ThreadManager.getTaskThreshold();
this.threshold = threshold;
this.players = players;
this.start = start;
this.end = end;
@ -53,9 +53,9 @@ public class ReloadAction extends RecursiveAction {
}
else {
final int split = length / 2;
final ReloadAction subTask1 = new ReloadAction(threshold, players, start, (start + split),
final ReloadAction subTask1 = new ReloadAction(players, start, (start + split),
lastPlayedLimit, offlinePlayerUUIDs);
final ReloadAction subTask2 = new ReloadAction(threshold, players, (start + split), end,
final ReloadAction subTask2 = new ReloadAction(players, (start + split), end,
lastPlayedLimit, offlinePlayerUUIDs);
//queue and compute all subtasks in the right order

View File

@ -1,17 +1,17 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.ShareManager;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.enums.DebugLevel;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
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.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Set;
@ -22,24 +22,26 @@ import java.util.function.Predicate;
public class ReloadThread extends Thread {
private final int threshold;
private final int reloadThreadID;
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static MessageWriter messageWriter;
private static OutputManager messageSender;
private final OfflinePlayerHandler offlinePlayerHandler;
private static ShareManager shareManager;
private final int reloadThreadID;
private final StatThread statThread;
private final CommandSender sender;
public ReloadThread(BukkitAudiences a, ConfigHandler c, MessageWriter m, int threshold, int ID, @Nullable StatThread s, @Nullable CommandSender se) {
this.threshold = threshold;
reloadThreadID = ID;
adventure = a;
public ReloadThread(ConfigHandler c, OutputManager m, OfflinePlayerHandler o, int ID, @Nullable StatThread s, @Nullable CommandSender se) {
config = c;
messageWriter = m;
messageSender = m;
offlinePlayerHandler = o;
shareManager = ShareManager.getInstance(c);
reloadThreadID = ID;
statThread = s;
sender = se;
@ -57,31 +59,34 @@ public class ReloadThread extends Thread {
MyLogger.waitingForOtherThread(this.getName(), statThread.getName());
statThread.join();
} catch (InterruptedException e) {
MyLogger.logException(e, "ReloadThread", "run(), trying to join" + statThread.getName());
MyLogger.logException(e, "ReloadThread", "run(), trying to join " + statThread.getName());
throw new RuntimeException(e);
}
}
if (reloadThreadID != 1 && config.reloadConfig()) { //during a reload
MyLogger.logMsg("Reloading!", false);
MyLogger.setDebugLevel(config.getDebugLevel());
MessageWriter.updateComponentFactory();
loadOfflinePlayers();
reloadEverything();
boolean isBukkitConsole = sender instanceof ConsoleCommandSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit");
if (sender != null) {
adventure.sender(sender).sendMessage(
messageWriter.reloadedConfig(isBukkitConsole));
messageSender.sendFeedbackMsg(sender, StandardMessage.RELOADED_CONFIG);
}
}
else { //during first start-up
MyLogger.setDebugLevel(config.getDebugLevel());
loadOfflinePlayers();
offlinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
}
}
private void loadOfflinePlayers() {
private void reloadEverything() {
MyLogger.setDebugLevel(config.getDebugLevel());
messageSender.updateMessageWriters(config);
offlinePlayerHandler.updateOfflinePlayerList(loadOfflinePlayers());
shareManager.updateSettings(config);
}
private ConcurrentHashMap<String, UUID> loadOfflinePlayers() {
long time = System.currentTimeMillis();
OfflinePlayer[] offlinePlayers;
@ -114,13 +119,13 @@ public class ReloadThread extends Thread {
int size = offlinePlayers != null ? offlinePlayers.length : 16;
ConcurrentHashMap<String, UUID> playerMap = new ConcurrentHashMap<>(size);
ReloadAction task = new ReloadAction(threshold, offlinePlayers, config.getLastPlayedLimit(), playerMap);
ReloadAction task = new ReloadAction(offlinePlayers, config.getLastPlayedLimit(), playerMap);
MyLogger.actionCreated((offlinePlayers != null) ? offlinePlayers.length : 0);
ForkJoinPool.commonPool().invoke(task);
MyLogger.actionFinished(1);
OfflinePlayerHandler.updateOfflinePlayerList(playerMap);
MyLogger.logTimeTaken("ReloadThread",
("loaded " + OfflinePlayerHandler.getOfflinePlayerCount() + " offline players"), time);
("loaded " + playerMap.size() + " offline players"), time);
return playerMap;
}
}

View File

@ -1,5 +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.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.google.common.collect.ImmutableList;
@ -10,10 +12,11 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.RecursiveAction;
public class TopStatAction extends RecursiveAction {
public final class StatAction extends RecursiveAction {
private final int threshold;
private static int threshold;
private final OfflinePlayerHandler offlinePlayerHandler;
private final ImmutableList<String> playerNames;
private final StatRequest request;
private final ConcurrentHashMap<String, Integer> playerStats;
@ -21,15 +24,16 @@ public class TopStatAction extends RecursiveAction {
/**
* Gets the statistic numbers for all players whose name is on the list, puts them in a ConcurrentHashMap
* using the default ForkJoinPool, and returns the ConcurrentHashMap when everything is done
* @param threshold the maximum length of playerNames to process in one task
* @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
*/
public TopStatAction(int threshold, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> playerStats) {
this.threshold = threshold;
this.playerNames = playerNames;
public StatAction(OfflinePlayerHandler offlinePlayerHandler, ImmutableList<String> playerNames, StatRequest statRequest, ConcurrentHashMap<String, Integer> playerStats) {
threshold = ThreadManager.getTaskThreshold();
this.offlinePlayerHandler = offlinePlayerHandler;
this.playerNames = playerNames;
this.request = statRequest;
this.playerStats = playerStats;
@ -42,8 +46,8 @@ public class TopStatAction extends RecursiveAction {
getStatsDirectly();
}
else {
final TopStatAction subTask1 = new TopStatAction(threshold, playerNames.subList(0, playerNames.size()/2), request, playerStats);
final TopStatAction subTask2 = new TopStatAction(threshold, playerNames.subList(playerNames.size()/2, playerNames.size()), request, playerStats);
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);
//queue and compute all subtasks in the right order
invokeAll(subTask1, subTask2);
@ -56,7 +60,7 @@ public class TopStatAction extends RecursiveAction {
do {
String playerName = iterator.next();
MyLogger.actionRunning(Thread.currentThread().getName(), playerName, 2);
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
int statistic = 0;
switch (request.getStatistic().getType()) {

View File

@ -1,15 +1,15 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
import com.gmail.artemis.the.gr8.playerstats.Main;
import com.gmail.artemis.the.gr8.playerstats.enums.StandardMessage;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
import com.gmail.artemis.the.gr8.playerstats.models.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 net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -21,81 +21,59 @@ import java.util.stream.Collectors;
public class StatThread extends Thread {
private final int threshold;
private final StatRequest request;
private final ReloadThread reloadThread;
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static MessageWriter messageWriter;
private final Main plugin;
private final OutputManager outputManager;
private final OfflinePlayerHandler offlinePlayerHandler;
//constructor (called on thread creation)
public StatThread(BukkitAudiences a, ConfigHandler c, MessageWriter m, Main p, int ID, int threshold, StatRequest s, @Nullable ReloadThread r) {
this.threshold = threshold;
private final ReloadThread reloadThread;
private final StatRequest request;
request = s;
reloadThread = r;
adventure = a;
public StatThread(ConfigHandler c, OutputManager m, OfflinePlayerHandler o, int ID, StatRequest s, @Nullable ReloadThread r) {
config = c;
messageWriter = m;
plugin = p;
outputManager = m;
offlinePlayerHandler = o;
reloadThread = r;
request = s;
this.setName("StatThread-" + request.getCommandSender().getName() + "-" + ID);
MyLogger.threadCreated(this.getName());
}
//what the thread will do once started
@Override
public void run() throws IllegalStateException, NullPointerException {
MyLogger.threadStart(this.getName());
if (messageWriter == null || plugin == null) {
throw new IllegalStateException("Not all classes off the plugin are running!");
}
if (request == null) {
throw new NullPointerException("No statistic request was found!");
}
if (reloadThread != null && reloadThread.isAlive()) {
try {
MyLogger.waitingForOtherThread(this.getName(), reloadThread.getName());
adventure.sender(request.getCommandSender())
.sendMessage(messageWriter
.stillReloading(request.isBukkitConsoleSender()));
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.STILL_RELOADING);
reloadThread.join();
} catch (InterruptedException e) {
MyLogger.logException(e, "StatThread", "Trying to join" + reloadThread.getName());
MyLogger.logException(e, "StatThread", "Trying to join " + reloadThread.getName());
throw new RuntimeException(e);
}
}
Target selection = request.getSelection();
if (selection == Target.PLAYER) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.formatPlayerStat(getIndividualStat(), request));
long lastCalc = ThreadManager.getLastRecordedCalcTime();
if (lastCalc > 2000) {
outputManager.sendFeedbackMsgWaitAMoment(request.getCommandSender(), lastCalc > 20000);
}
else {
if (ThreadManager.getLastRecordedCalcTime() > 2000) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.waitAMoment(ThreadManager.getLastRecordedCalcTime() > 20000, request.isBukkitConsoleSender()));
Target selection = request.getSelection();
try {
switch (selection) {
case PLAYER -> outputManager.sendPlayerStat(request, getIndividualStat());
case TOP -> outputManager.sendTopStat(request, getTopStats());
case SERVER -> outputManager.sendServerStat(request, getServerTotal());
}
try {
if (selection == Target.TOP) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.formatTopStats(getTopStats(), request));
} else {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.formatServerStat(getServerTotal(), request));
}
} catch (ConcurrentModificationException e) {
if (!request.isConsoleSender()) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.unknownError(false));
}
} catch (ConcurrentModificationException e) {
if (!request.isConsoleSender()) {
outputManager.sendFeedbackMsg(request.getCommandSender(), StandardMessage.UNKNOWN_ERROR);
}
}
}
@ -107,7 +85,7 @@ public class StatThread extends Thread {
}
private long getServerTotal() {
List<Integer> numbers = getAllStats().values().stream().toList();
List<Integer> numbers = getAllStats().values().parallelStream().toList();
return numbers.parallelStream().mapToLong(Integer::longValue).sum();
}
@ -115,11 +93,11 @@ public class StatThread extends Thread {
private @NotNull ConcurrentHashMap<String, Integer> getAllStats() throws ConcurrentModificationException {
long time = System.currentTimeMillis();
int size = OfflinePlayerHandler.getOfflinePlayerCount() != 0 ? (int) (OfflinePlayerHandler.getOfflinePlayerCount() * 1.05) : 16;
int size = offlinePlayerHandler.getOfflinePlayerCount() != 0 ? offlinePlayerHandler.getOfflinePlayerCount() : 16;
ConcurrentHashMap<String, Integer> playerStats = new ConcurrentHashMap<>(size);
ImmutableList<String> playerNames = ImmutableList.copyOf(OfflinePlayerHandler.getOfflinePlayerNames());
ImmutableList<String> playerNames = ImmutableList.copyOf(offlinePlayerHandler.getOfflinePlayerNames());
TopStatAction task = new TopStatAction(threshold, playerNames, request, playerStats);
StatAction task = new StatAction(offlinePlayerHandler, playerNames, request, playerStats);
MyLogger.actionCreated(playerNames.size());
ForkJoinPool commonPool = ForkJoinPool.commonPool();
@ -127,7 +105,8 @@ public class StatThread extends Thread {
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);
"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());
}
@ -141,22 +120,14 @@ public class StatThread extends Thread {
/** 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());
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(request.getPlayerName());
if (player != null) {
switch (request.getStatistic().getType()) {
case UNTYPED -> {
return player.getStatistic(request.getStatistic());
}
case ENTITY -> {
return player.getStatistic(request.getStatistic(), request.getEntity());
}
case BLOCK -> {
return player.getStatistic(request.getStatistic(), request.getBlock());
}
case ITEM -> {
return player.getStatistic(request.getStatistic(), request.getItem());
}
}
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());
};
}
return 0;
}

View File

@ -12,7 +12,12 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class EnumHandler {
/** This class deals with Bukkit Enumerators. It holds private lists of all
block-, item-, entity- and statistic-names, and has one big list of all
possible sub-statistic-entries (block/item/entity). It can give the names
of all aforementioned enums, check if something is a valid enum constant,
and turn a name into its corresponding enum constant. */
public final class EnumHandler {
private final static List<String> blockNames;
private final static List<String> entityNames;
@ -50,14 +55,22 @@ public class EnumHandler {
.collect(Collectors.toList());
}
private EnumHandler() {
/** Returns all block-names in lowercase */
public static List<String> getBlockNames() {
return blockNames;
}
/** Returns all item-names in lowercase*/
public static List<String> getItemNames() {
return itemNames;
}
/** Returns corresponding item enum constant for an itemName
/** Returns all statistic-names in lowercase */
public static List<String> getStatNames() {
return statNames;
}
/** Returns the corresponding Material enum constant for an itemName
@param itemName String, case-insensitive
@return Material enum constant, uppercase */
public static @Nullable Material getItemEnum(String itemName) {
@ -67,12 +80,7 @@ public class EnumHandler {
return (item != null && item.isItem()) ? item : null;
}
/** Returns all entitytype names in lowercase */
public static List<String> getEntityNames() {
return entityNames;
}
/** Returns corresponding EntityType enum constant for an entityName
/** Returns the corresponding EntityType enum constant for an entityName
@param entityName String, case-insensitive
@return EntityType enum constant, uppercase */
public static @Nullable EntityType getEntityEnum(String entityName) {
@ -84,12 +92,7 @@ public class EnumHandler {
}
}
/** Returns all block names in lowercase */
public static List<String> getBlockNames() {
return blockNames;
}
/** Returns corresponding block enum constant for a materialName
/** Returns the corresponding Material enum constant for a materialName
@param materialName String, case-insensitive
@return Material enum constant, uppercase */
public static @Nullable Material getBlockEnum(String materialName) {
@ -99,6 +102,17 @@ public class EnumHandler {
return (block != null && block.isBlock()) ? block : null;
}
/** Returns the statistic enum constant, or null if that failed.
@param statName String, case-insensitive */
public static @Nullable Statistic getStatEnum(@NotNull String statName) {
try {
return Statistic.valueOf(statName.toUpperCase());
}
catch (IllegalArgumentException e) {
return null;
}
}
/** Checks if string is a valid statistic
@param statName String, case-insensitive */
public static boolean isStatistic(@NotNull String statName) {
@ -111,20 +125,10 @@ public class EnumHandler {
statName.equalsIgnoreCase(Statistic.KILL_ENTITY.toString());
}
/** Returns the names of all general statistics in lowercase */
public static List<String> getStatNames() {
return statNames;
}
/** Returns the statistic enum constant, or null if that failed.
@param statName String, case-insensitive */
public static @Nullable Statistic getStatEnum(@NotNull String statName) {
try {
return Statistic.valueOf(statName.toUpperCase());
}
catch (IllegalArgumentException e) {
return null;
}
/** 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) {
return subStatNames.contains(statName.toLowerCase());
}
/** Returns "block", "entity", "item", or "sub-statistic" if the provided Type is null. */
@ -138,10 +142,4 @@ public class EnumHandler {
}
return subStat;
}
/** 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) {
return subStatNames.contains(statName.toLowerCase());
}
}

View File

@ -12,27 +12,24 @@ public class OfflinePlayerHandler {
private static ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
private static ArrayList<String> playerNames;
static {
public OfflinePlayerHandler() {
offlinePlayerUUIDs = new ConcurrentHashMap<>();
playerNames = new ArrayList<>();
}
private OfflinePlayerHandler() {
}
/** 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 static boolean isRelevantPlayer(String playerName) {
public boolean isRelevantPlayer(String playerName) {
return offlinePlayerUUIDs.containsKey(playerName);
}
/** Returns the number of OfflinePlayers that are included in statistic calculations */
public static int getOfflinePlayerCount() {
public int getOfflinePlayerCount() {
return offlinePlayerUUIDs.size();
}
/** Get an ArrayList of names from all OfflinePlayers that should be included in statistic calculations */
public static ArrayList<String> getOfflinePlayerNames() {
public ArrayList<String> getOfflinePlayerNames() {
return playerNames;
}
@ -41,7 +38,7 @@ public class OfflinePlayerHandler {
* 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) {
public void updateOfflinePlayerList(ConcurrentHashMap<String, UUID> playerList) {
offlinePlayerUUIDs = playerList;
playerNames = Collections.list(offlinePlayerUUIDs.keys());
}
@ -51,7 +48,7 @@ public class OfflinePlayerHandler {
* @param playerName name of the target player
* @return OfflinePlayer (if this player is on the list, otherwise null)
*/
public static @Nullable OfflinePlayer getOfflinePlayer(String playerName) {
public @Nullable OfflinePlayer getOfflinePlayer(String playerName) {
if (offlinePlayerUUIDs.get(playerName) != null) {
return Bukkit.getOfflinePlayer(offlinePlayerUUIDs.get(playerName));
}

View File

@ -1,7 +1,6 @@
package com.gmail.artemis.the.gr8.playerstats.utils;
public class UnixTimeHandler {
public final class UnixTimeHandler {
/** Calculates whether a player has played recently enough to fall within the lastPlayedLimit.
If lastPlayedLimit == 0, this always returns true (since there is no limit).

View File

@ -1,7 +1,7 @@
# ------------------------------------------------------------------------------------------------------ #
# PlayerStats Configuration #
# ------------------------------------------------------------------------------------------------------ #
config-version: 5
config-version: 6
# # ------------------------------- # #
@ -15,9 +15,16 @@ config-version: 5
debug-level: 1
# Whether players have to wait for their lookup to finish before they can request another statistic
# Warning: disabling this could allow players to stress out your server by spamming the stat-command!
# Warning: disabling this could put stress on your server if players spam the stat-command!
only-allow-one-lookup-at-a-time-per-player: true
# Whether statistics can be shared with everyone in chat
enable-stat-sharing: true
# How often players can share statistics in chat (use this if you want to limit chat spam)
# Leave this on 0 to disable the cool-down, or specify the number of minutes you want players to wait
waiting-time-before-sharing-again: 0
# Filtering options to control which players should be included in statistic calculations
include-whitelist-only: false
exclude-banned-players: false
@ -38,37 +45,21 @@ translate-to-client-language: true
# Use hover-text for additional info about statistic numbers
enable-hover-text: true
# The unit to display certain statistics in.
# Minecraft measures distance in cm. PlayerStats supports: blocks, cm, m (= blocks), miles, km
distance-unit: blocks
distance-unit-for-hover-text: km
# Minecraft measures damage in 0.5 hearts (1HP). PlayerStats supports: hp, hearts
damage-unit: hearts
damage-unit-for-hover-text: hp
# Minecraft measures time in ticks. With the below settings, PlayerStats will:
# Auto-detect the best maximum unit to use (weeks/days/hours/minutes/seconds) for your players' statistics
# Show a specified amount of additional smaller units (example: "x days" would become "x days, y hours, z minutes")
auto-detect-biggest-time-unit: true
number-of-extra-units: 1
auto-detect-biggest-time-unit-for-hover-text: false
number-of-extra-units-for-hover-text: 0
# If you don't want the unit to be auto-detected, set the auto-detect settings to false and specify your own range here
# If the max and min are the same, only that unit will be displayed
# PlayerStats supports: days, hours, minutes, seconds (and ticks if you want the original number)
biggest-time-unit: days
smallest-time-unit: hours
biggest-time-unit-for-hover-text: hours
smallest-time-unit-for-hover-text: seconds
# Automatically use themed formatting for the duration of certain holidays or festivals
enable-festive-formatting: true
# Always use rainbow for the [PlayerStats] prefix instead of the default gold/purple
# Always use the rainbow theme
rainbow-mode: false
# Start the below stat-results with an empty line in chat before the result
use-enters:
top-stats: true
top-stats-shared: false
player-stats: false
player-stats-shared: false
server-stats: false
server-stats-shared: false
# Align the stat-numbers in the top list with dots
use-dots: true
@ -84,6 +75,35 @@ total-server-stat-title: 'Total on'
your-server-name: 'this server'
# # ------------------------------- # #
# # Units # #
# # ------------------------------- # #
# Minecraft measures distance in cm. PlayerStats supports: blocks, cm, m (= blocks), miles, km
distance-unit: blocks
distance-unit-for-hover-text: km
# Minecraft measures damage in 0.5 hearts (1HP). PlayerStats supports: hp, hearts
damage-unit: hearts
damage-unit-for-hover-text: hp
# Minecraft measures time in ticks. With the below settings, PlayerStats will:
# Auto-detect the biggest unit to use (weeks/days/hours/minutes/seconds) for your players' statistics
# Show as many additional smaller units as you choose (so for 3 extra units, "9D" would become "9D 5H 20M")
auto-detect-biggest-time-unit: true
number-of-extra-units: 1
auto-detect-biggest-time-unit-for-hover-text: false
number-of-extra-units-for-hover-text: 0
# To always use the same units, set the auto-detect settings to false and select your own unit range here
# If the biggest and smallest unit are the same, only that unit will be displayed
# PlayerStats supports: days, hours, minutes, seconds (and ticks if you want the original number)
biggest-time-unit: days
smallest-time-unit: hours
biggest-time-unit-for-hover-text: hours
smallest-time-unit-for-hover-text: seconds
# # ------------------------------- # #
# # Color & Style # #
# # ------------------------------- # #
@ -107,14 +127,22 @@ hover-text-amount-lighter: 40
# # black white # #
# # ------------------------------ # #
shared-stats:
shared-by: gray
shared-by-style: italic
player-name: "#EE8A19"
player-name-style: italic
top-list:
title: '#FFEA40'
title: '#FFD52B'
title-style: none
title-number: gold
title-number-style: none
stat-names: '#FFEA40'
stat-names: '#FFD52B'
stat-names-style: none
sub-stat-names: yellow

View File

@ -1,7 +1,7 @@
main: com.gmail.artemis.the.gr8.playerstats.Main
name: PlayerStats
version: 1.5
api-version: 1.18
version: 1.6
api-version: 1.13
description: adds commands to view player statistics in chat
author: Artemis_the_gr8
commands:
@ -11,17 +11,27 @@ commands:
- stats
description: general statistic command
permission: playerstats.stat
statisticshare:
aliases:
- statshare
- statsshare
description: shares last stat lookup in chat
usage: "§b/statshare"
permission: playerstats.share
statisticreload:
aliases:
- statreload
- statsreload
description: reloads the config
usage: "§a/statisticreload"
usage: "§a/statreload"
permission: playerstats.reload
permissions:
playerstats.stat:
description: allows usage of /statistic
default: true
playerstats.share:
description: allows sharing stats in chat
default: true
playerstats.reload:
description: allows usage of /statreload
default: op