Merge pull request #76 from itHotL/message-factory-rework

Message factory rework
This commit is contained in:
Elise 2022-06-29 21:36:01 +02:00 committed by GitHub
commit 054c8b6d7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 976 additions and 788 deletions

View File

@ -23,21 +23,30 @@
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>com.gmail.artemis.the.gr8.playerstats.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>org.jetbrains:annotations</exclude>
<exclude>META-INF/versions/**</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
<configuration>
<transformers>
<transformer>
<mainClass>com.gmail.artemis.the.gr8.playerstats.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>org.jetbrains:annotations</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/versions/**</exclude>
<exclude>META-INF/maven/com.tchristofferson/**</exclude>
<exclude>images/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
</build>

37
pom.xml
View File

@ -92,23 +92,32 @@
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.gmail.artemis.the.gr8.playerstats.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>org.jetbrains:annotations</exclude>
<exclude>META-INF/versions/**</exclude>
</excludes>
</artifactSet>
</configuration>
</execution>
</executions>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.gmail.artemis.the.gr8.playerstats.Main</mainClass>
</transformer>
</transformers>
<artifactSet>
<excludes>
<exclude>org.jetbrains:annotations</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/versions/**</exclude>
<exclude>META-INF/maven/com.tchristofferson/**</exclude>
<exclude>images/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -6,8 +6,8 @@ import com.gmail.artemis.the.gr8.playerstats.commands.TabCompleter;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.listeners.JoinListener;
import com.gmail.artemis.the.gr8.playerstats.msg.LanguageKeyHandler;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.PrideMessageFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
import com.gmail.artemis.the.gr8.playerstats.msg.PrideComponentFactory;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand;
@ -31,20 +31,19 @@ public class Main extends JavaPlugin {
//initialize the Adventure library
adventure = BukkitAudiences.create(this);
//first get an instance of the ConfigHandler and LanguageKeyHandler
//first get an instance of the ConfigHandler
ConfigHandler config = new ConfigHandler(this);
LanguageKeyHandler language = new LanguageKeyHandler();
//for now always use the PrideMessageFactory (it'll use the regular formatting when needed)
MessageFactory messageFactory = new PrideMessageFactory(config, language);
//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, messageFactory, this);
ThreadManager threadManager = new ThreadManager(adventure(), config, messageWriter, this);
//register all commands and the tabCompleter
PluginCommand statcmd = this.getCommand("statistic");
if (statcmd != null) {
statcmd.setExecutor(new StatCommand(adventure(), messageFactory, threadManager));
statcmd.setExecutor(new StatCommand(adventure(), messageWriter, threadManager));
statcmd.setTabCompleter(new TabCompleter());
}
PluginCommand reloadcmd = this.getCommand("statisticreload");

View File

@ -1,10 +1,10 @@
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.reload.ReloadThread;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageFactory;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.command.CommandSender;
@ -19,16 +19,16 @@ public class ThreadManager {
private final Main plugin;
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static MessageFactory messageFactory;
private static MessageWriter messageWriter;
private ReloadThread reloadThread;
private StatThread statThread;
private static long lastRecordedCalcTime;
public ThreadManager(BukkitAudiences a, ConfigHandler c, MessageFactory m, Main p) {
public ThreadManager(BukkitAudiences a, ConfigHandler c, MessageWriter m, Main p) {
adventure = a;
config = c;
messageFactory = m;
messageWriter = m;
plugin = p;
statThreadID = 0;
@ -41,7 +41,7 @@ public class ThreadManager {
if (reloadThread == null || !reloadThread.isAlive()) {
reloadThreadID += 1;
reloadThread = new ReloadThread(adventure, config, messageFactory, plugin, threshold, reloadThreadID, statThread, sender);
reloadThread = new ReloadThread(adventure, config, messageWriter, threshold, reloadThreadID, statThread, sender);
reloadThread.start();
}
else {
@ -52,7 +52,7 @@ public class ThreadManager {
public void startStatThread(StatRequest request) {
statThreadID += 1;
statThread = new StatThread(adventure, config, messageFactory, plugin, statThreadID, threshold, request, reloadThread);
statThread = new StatThread(adventure, config, messageWriter, plugin, statThreadID, threshold, request, reloadThread);
statThread.start();
}

View File

@ -1,13 +1,16 @@
package com.gmail.artemis.the.gr8.playerstats.commands;
import com.gmail.artemis.the.gr8.playerstats.ThreadManager;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageFactory;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.command.Command;
@ -19,31 +22,44 @@ import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import static net.kyori.adventure.text.Component.text;
public class StatCommand implements CommandExecutor {
private final BukkitAudiences adventure;
private final MessageFactory messageFactory;
private final MessageWriter messageWriter;
private final ThreadManager threadManager;
public StatCommand(BukkitAudiences a, MessageFactory m, ThreadManager t) {
public StatCommand(BukkitAudiences a, MessageWriter m, ThreadManager t) {
adventure = a;
messageFactory = m;
messageWriter = m;
threadManager = t;
}
@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(messageFactory.helpMsg(sender instanceof ConsoleCommandSender));
adventure.sender(sender).sendMessage(messageWriter.helpMsg(sender instanceof ConsoleCommandSender));
}
else if (args[0].equalsIgnoreCase("examples") ||
args[0].equalsIgnoreCase("example")) { //in case of "statistic examples", show examples
adventure.sender(sender).sendMessage(messageFactory.usageExamples(sender instanceof ConsoleCommandSender));
adventure.sender(sender).sendMessage(messageWriter.usageExamples(isBukkitConsole));
}
else if (args[0].equalsIgnoreCase("test")) {
TextComponent msg = text("Tier 1").color(PluginColor.GOLD.getColor())
.append(text("Tier 2").color(PluginColor.MEDIUM_GOLD.getColor())
.append(text("Tier 3").color(TextColor.fromHexString("#FFEA40"))
.append(text("Tier 4").color(PluginColor.LIGHT_GOLD.getColor()))
.append(text("Tier 3?")))
.append(text("Tier 2?")))
.append(text("Tier 1?"));
adventure.sender(sender).sendMessage(msg);
}
else {
StatRequest request = generateRequest(sender, args);
TextComponent issues = checkRequest(request);
TextComponent issues = checkRequest(request, isBukkitConsole);
if (issues == null) {
threadManager.startStatThread(request);
}
@ -140,20 +156,19 @@ public class StatCommand implements CommandExecutor {
<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 isConsoleSender = request.getCommandSender() instanceof ConsoleCommandSender;
private @Nullable TextComponent checkRequest(StatRequest request, boolean isBukkitConsole) {
if (request.getStatistic() == null) {
return messageFactory.missingStatName(isConsoleSender);
return messageWriter.missingStatName(isBukkitConsole);
}
Statistic.Type type = request.getStatistic().getType();
if (request.getSubStatEntry() == null && type != Statistic.Type.UNTYPED) {
return messageFactory.missingSubStatName(type, isConsoleSender);
return messageWriter.missingSubStatName(type, isBukkitConsole);
}
else if (!matchingSubStat(request)) {
return messageFactory.wrongSubStatType(type, request.getSubStatEntry(), isConsoleSender);
return messageWriter.wrongSubStatType(type, request.getSubStatEntry(), isBukkitConsole);
}
else if (request.getSelection() == Target.PLAYER && request.getPlayerName() == null) {
return messageFactory.missingPlayerName(isConsoleSender);
return messageWriter.missingPlayerName(isBukkitConsole);
}
else {
return null;

View File

@ -26,7 +26,7 @@ public class ConfigHandler {
configVersion = 4;
checkConfigVersion();
MyLogger.setDebugLevel(debugLevel());
MyLogger.setDebugLevel(getDebugLevel());
}
/** Checks the number that "config-version" returns to see if the config needs updating, and if so, send it to the Updater.
@ -56,7 +56,6 @@ public class ConfigHandler {
}
try {
config = YamlConfiguration.loadConfiguration(configFile);
MyLogger.setDebugLevel(debugLevel());
return true;
}
catch (IllegalArgumentException e) {
@ -70,7 +69,7 @@ public class ConfigHandler {
<p>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</p>
<p>3 = high (log all tasks and time taken)</p>
<p>Default: 1</p>*/
public int debugLevel() {
public int getDebugLevel() {
return config.getInt("debug-level", 1);
}
@ -88,7 +87,7 @@ public class ConfigHandler {
/** Returns the number of maximum days since a player has last been online.
<p>Default: 0 (which signals not to use this limit)</p>*/
public int lastPlayedLimit() {
public int getLastPlayedLimit() {
return config.getInt("number-of-days-since-last-joined", 0);
}
@ -112,7 +111,7 @@ public class ConfigHandler {
/** Whether to use rainbow colors for the [PlayerStats] prefix rather than the default gold/purple.
<p>Default: false</p> */
public boolean useRainbowPrefix() {
public boolean useRainbowMode() {
return config.getBoolean("rainbow-mode", false);
}

View File

@ -34,7 +34,7 @@ public class ConfigUpdateHandler {
}
}
/** Adjusts some of the default colors to migrate from versions 2 or 3 to version 4.*/
/** Adjusts some of the default colors to migrate from versions 2 or 3 to version 4.1.*/
private void updateDefaultColors(YamlConfiguration configuration) {
updateColor(configuration, "top-list.title", "yellow", "#FFD52B");
updateColor(configuration, "top-list.stat-names", "yellow", "#FFD52B");

View File

@ -0,0 +1,40 @@
package com.gmail.artemis.the.gr8.playerstats.enums;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
/** This enum represents the colorscheme PlayerStats uses in its output messages.
<p>GRAY: ChatColor Gray</p>
<p>DARK_PURPLE: #6E3485 (used for default sub-titles, title-underscores and brackets)</p>
<p>MEDIUM_BLUE: #55AAFF (used for all plain feedback and error messages)</p>
<p>LIGHT_BLUE: #55C6FF (used for default hover-text)</p>
<p>GOLD: ChatColor Gold (used for first parts of usage messages and for first parts of hover-text accent)</p>
<p>MEDIUM_GOLD: #FFD52B (used for second parts of usage messages and for second parts of hover-text accent) </p>
<p>LIGHT_GOLD: #FFEA40 (used for third parts of usage messages)</p>
<p>LIGHT_YELLOW: #FFFF8E (used for last parts of explanation message)</p>
*/
public enum PluginColor {
GRAY (NamedTextColor.GRAY), //#AAAAAA
DARK_PURPLE (TextColor.fromHexString("#6E3485")),
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"));
private final TextColor color;
PluginColor(TextColor color) {
this.color = color;
}
public TextColor getColor() {
return color;
}
public TextColor getConsoleColor() {
return NamedTextColor.nearestTo(color);
}
}

View File

@ -0,0 +1,359 @@
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 com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.NumberFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.Index;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.*/
public class ComponentFactory {
private static ConfigHandler config;
public ComponentFactory(ConfigHandler c) {
config = c;
}
/** Returns [PlayerStats] followed by a single space. */
public TextComponent pluginPrefix(boolean isBukkitConsole) {
return text("[")
.color(PluginColor.GRAY.getColor())
.append(text("PlayerStats").color(PluginColor.GOLD.getColor()))
.append(text("]"))
.append(space());
}
/** Returns [PlayerStats] surrounded by underscores on both sides. */
public TextComponent prefixTitle(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)
.append(text(" ")) //4 spaces
.append(pluginPrefix(isBukkitConsole))
.append(text(" ")) //3 spaces (since prefix already has one)
.append(text(underscores));
}
/** Returns a TextComponent with the input String as content, with color Gray and decoration Italic.*/
public TextComponent subTitle(String content) {
return text(content).color(PluginColor.GRAY.getColor()).decorate(TextDecoration.ITALIC);
}
/** Returns a TextComponents that represents a full message, with [PlayerStats] prepended. */
public TextComponent msg(String msg, boolean isBukkitConsole) {
return pluginPrefix(isBukkitConsole)
.append(text(msg)
.color(PluginColor.MEDIUM_BLUE.getColor()));
}
/** Returns a plain TextComponent that represents a single message line.
A space will be inserted after part1, part2 and part3.
Each message part has its own designated color.
@param part1 color DARK_GOLD
@param part2 color MEDIUM_GOLD
@param part3 color YELLOW
@param part4 color GRAY
*/
public TextComponent msgPart(@Nullable String part1, @Nullable String part2, @Nullable String part3, @Nullable String part4) {
return msgPart(part1, part2, part3, part4, false);
}
/** Returns a plain TextComponent that represents a single message line.
A space will be inserted after part1, part2 and part3.
Each message part has its own designated color.
if isBukkitConsole is true, the colors will be the nearest ChatColor to the below colors.
@param part1 color DARK_GOLD
@param part2 color MEDIUM_GOLD
@param part3 color YELLOW
@param part4 color GRAY
*/
public TextComponent msgPart(@Nullable String part1, @Nullable String part2, @Nullable String part3, @Nullable String part4, boolean isBukkitConsole) {
TextComponent.Builder msg = Component.text();
if (part1 != null) {
TextColor pluginColor = isBukkitConsole ? PluginColor.GOLD.getConsoleColor() : PluginColor.GOLD.getColor();
msg.append(text(part1)
.color(pluginColor))
.append(space());
}
if (part2 != null) {
TextColor pluginColor = isBukkitConsole ? PluginColor.MEDIUM_GOLD.getConsoleColor() : PluginColor.MEDIUM_GOLD.getColor();
msg.append(text(part2)
.color(pluginColor))
.append(space());
}
if (part3 != null) {
TextColor pluginColor = isBukkitConsole ? PluginColor.LIGHT_GOLD.getConsoleColor() : PluginColor.LIGHT_GOLD.getColor();
msg.append(text(part3)
.color(pluginColor))
.append(space());
}
if (part4 != null) {
TextColor pluginColor = isBukkitConsole ? PluginColor.GRAY.getConsoleColor() : PluginColor.GRAY.getColor();
msg.append(text(part4)
.color(pluginColor));
}
return msg.build();
}
/** Returns a TextComponent with a single line of hover-text in the specified color.
@param plainText the base message
@param hoverText the hovering text
@param hoverColor color of the hovering text */
public TextComponent simpleHoverPart(String plainText, String hoverText, PluginColor hoverColor) {
return simpleHoverPart(plainText, null, hoverText, hoverColor);
}
/** Returns a TextComponent with a single line of hover-text in the specified color.
If a PluginColor is provided for the plainText, the base color is set as well.
@param plainText the base message
@param plainColor color of the base message
@param hoverText the hovering text
@param hoverColor color of the hovering text */
public TextComponent simpleHoverPart(String plainText, @Nullable PluginColor plainColor, String hoverText, PluginColor hoverColor) {
TextComponent.Builder msg = Component.text()
.append(text(plainText))
.hoverEvent(HoverEvent.showText(
text(hoverText)
.color(hoverColor.getColor())));
if (plainColor != null) {
msg.color(plainColor.getColor());
}
return msg.build();
}
/** Returns a TextComponent with hover-text that can consist of three different parts,
divided over two different lines. Each part has its own designated color. If all the
input Strings are null, it will return an empty Component.
@param plainText the non-hovering part
@param color the color for the non-hovering part
@param hoverLineOne text on the first line, with color LIGHT_BLUE
@param hoverLineTwoA text on the second line, with color GOLD
@param hoverLineTwoB text on the second part of the second line, with color LIGHT_GOLD
*/
public TextComponent complexHoverPart(@NotNull String plainText, @NotNull PluginColor color, String hoverLineOne, String hoverLineTwoA, String hoverLineTwoB) {
TextComponent base = Component.text(plainText).color(color.getColor());
TextComponent.Builder hoverText = Component.text();
if (hoverLineOne != null) {
hoverText.append(text(hoverLineOne)
.color(PluginColor.LIGHT_BLUE.getColor()));
if (hoverLineTwoA != null || hoverLineTwoB != null) {
hoverText.append(newline());
}
}
if (hoverLineTwoA != null) {
hoverText.append(text(hoverLineTwoA)
.color(PluginColor.GOLD.getColor()));
if (hoverLineTwoB != null) {
hoverText.append(space());
}
}
if (hoverLineTwoB != null) {
hoverText.append(text(hoverLineTwoB).color(PluginColor.LIGHT_GOLD.getColor()));
}
return base.hoverEvent(HoverEvent.showText(hoverText.build()));
}
public TextComponent playerName(String playerName, Target selection) {
return createComponent(playerName,
getColorFromString(config.getPlayerNameFormatting(selection, false)),
getStyleFromString(config.getPlayerNameFormatting(selection, true)));
}
public TranslatableComponent statName(@NotNull StatRequest request) {
String statName = request.getStatistic().name();
String subStatName = request.getSubStatEntry();
if (!config.useTranslatableComponents()) {
statName = getPrettyName(statName);
subStatName = getPrettyName(subStatName);
}
else {
statName = LanguageKeyHandler.getStatKey(request.getStatistic());
switch (request.getStatistic().getType()) {
case BLOCK -> subStatName = LanguageKeyHandler.getBlockKey(request.getBlock());
case ENTITY -> subStatName = LanguageKeyHandler.getEntityKey(request.getEntity());
case ITEM -> subStatName = LanguageKeyHandler.getItemKey(request.getItem());
case UNTYPED -> {
}
}
}
return statName(statName, subStatName, request.getSelection());
}
private TranslatableComponent statName(@NotNull String statKey, String subStatKey, @NotNull Target selection) {
TranslatableComponent.Builder totalName;
TextComponent subStat = subStatName(subStatKey, selection);
TextColor statNameColor = getColorFromString(config.getStatNameFormatting(selection, false));
TextDecoration statNameStyle = getStyleFromString(config.getStatNameFormatting(selection, true));
if (statKey.equalsIgnoreCase("stat_type.minecraft.killed") && subStat != null) {
totalName = killEntity(subStat);
}
else if (statKey.equalsIgnoreCase("stat_type.minecraft.killed_by") && subStat != null) {
totalName = entityKilledBy(subStat);
}
else {
totalName = translatable().key(statKey);
if (subStat != null) totalName.append(space()).append(subStat);
}
if (statNameStyle != null) totalName.decoration(statNameStyle, TextDecoration.State.TRUE);
return totalName
.color(statNameColor)
.build();
}
private @Nullable TextComponent subStatName(@Nullable String subStatName, Target selection) {
if (subStatName != null) {
TextDecoration style = getStyleFromString(config.getSubStatNameFormatting(selection, true));
TextComponent.Builder subStat = text()
.append(text("("))
.append(translatable()
.key(subStatName))
.append(text(")"))
.color(getColorFromString(config.getSubStatNameFormatting(selection, false)));
subStat.decorations(TextDecoration.NAMES.values(), false);
if (style != null) subStat.decoration(style, TextDecoration.State.TRUE);
return subStat.build();
}
else {
return null;
}
}
/** Construct a custom translation for kill_entity with the language key for commands.kill.success.single ("Killed %s").
@return a TranslatableComponent Builder with the subStat Component as args.*/
private TranslatableComponent.Builder killEntity(@NotNull TextComponent subStat) {
return translatable()
.key("commands.kill.success.single") //"Killed %s"
.args(subStat);
}
/** Construct a custom translation for entity_killed_by with the language keys for stat.minecraft.deaths
("Number of Deaths") and book.byAuthor ("by %s").
@return a TranslatableComponent Builder with stat.minecraft.deaths as key, with a ChildComponent
with book.byAuthor as key and the subStat Component as args.*/
private TranslatableComponent.Builder entityKilledBy(@NotNull TextComponent subStat) {
return translatable()
.key("stat.minecraft.deaths") //"Number of Deaths"
.append(space())
.append(translatable()
.key("book.byAuthor") //"by %s"
.args(subStat));
}
public TextComponent statNumber(long number, Target selection) {
return createComponent(NumberFormatter.format(number),
getColorFromString(config.getStatNumberFormatting(selection, false)),
getStyleFromString(config.getStatNumberFormatting(selection, true)));
}
public TextComponent title(String content, Target selection) {
return createComponent(content,
getColorFromString(config.getTitleFormatting(selection, false)),
getStyleFromString(config.getTitleFormatting(selection, true)));
}
public TextComponent titleNumber(int number) {
return createComponent(number + "",
getColorFromString(config.getTitleNumberFormatting(false)),
getStyleFromString(config.getTitleNumberFormatting(true)));
}
public TextComponent serverName(String serverName) {
TextComponent colon = text(":").color(getColorFromString(config.getServerNameFormatting(false)));
return createComponent(serverName,
getColorFromString(config.getServerNameFormatting(false)),
getStyleFromString(config.getServerNameFormatting(true)))
.append(colon);
}
public TextComponent rankingNumber(String number) {
return createComponent(number,
getColorFromString(config.getRankNumberFormatting(false)),
getStyleFromString(config.getRankNumberFormatting(true)));
}
public TextComponent dots(String dots) {
return createComponent(dots,
getColorFromString(config.getDotsFormatting(false)),
getStyleFromString(config.getDotsFormatting(true)));
}
private TextComponent createComponent(String content, TextColor color, @Nullable TextDecoration style) {
return style == null ? text(content).color(color) : text(content).color(color).decoration(style, TextDecoration.State.TRUE);
}
/** Replace "_" with " " and capitalize each first letter of the input.
@param input String to prettify, case-insensitive*/
private String getPrettyName(String input) {
if (input == null) return null;
StringBuilder capitals = new StringBuilder(input.toLowerCase());
capitals.setCharAt(0, Character.toUpperCase(capitals.charAt(0)));
while (capitals.indexOf("_") != -1) {
MyLogger.replacingUnderscores();
int index = capitals.indexOf("_");
capitals.setCharAt(index + 1, Character.toUpperCase(capitals.charAt(index + 1)));
capitals.setCharAt(index, ' ');
}
return capitals.toString();
}
private TextColor getColorFromString(String configString) {
if (configString != null) {
try {
if (configString.contains("#")) {
return TextColor.fromHexString(configString);
}
else {
return getTextColorByName(configString);
}
}
catch (IllegalArgumentException | NullPointerException exception) {
Bukkit.getLogger().warning(exception.toString());
}
}
return null;
}
private TextColor getTextColorByName(String textColor) {
Index<String, NamedTextColor> names = NamedTextColor.NAMES;
return names.value(textColor);
}
private @Nullable TextDecoration getStyleFromString(@NotNull String configString) {
if (configString.equalsIgnoreCase("none")) {
return null;
}
else if (configString.equalsIgnoreCase("magic")) {
return TextDecoration.OBFUSCATED;
}
else {
Index<String, TextDecoration> styles = TextDecoration.NAMES;
return styles.value(configString);
}
}
}

View File

@ -12,14 +12,17 @@ import java.util.HashMap;
public class LanguageKeyHandler {
private final HashMap<Statistic, String> statNameKeys;
private final static HashMap<Statistic, String> statNameKeys;
public LanguageKeyHandler() {
static {
statNameKeys = new HashMap<>();
generateStatNameKeys();
}
public String getStatKey(@NotNull Statistic statistic) {
private LanguageKeyHandler() {
}
public static String getStatKey(@NotNull Statistic statistic) {
if (statistic.getType() == Statistic.Type.UNTYPED) {
return "stat.minecraft." + statNameKeys.get(statistic);
}
@ -30,7 +33,7 @@ public class LanguageKeyHandler {
/** Get the official Key from the NameSpacedKey for this entityType,
or return null if no enum constant can be retrieved or entityType is UNKNOWN.*/
public @Nullable String getEntityKey(EntityType entity) {
public static @Nullable String getEntityKey(EntityType entity) {
if (entity == null || entity == EntityType.UNKNOWN) return null;
else {
return "entity.minecraft." + entity.getKey().getKey();
@ -39,7 +42,7 @@ public class LanguageKeyHandler {
/** Get the official Key from the NameSpacedKey for this item Material,
or return null if no enum constant can be retrieved.*/
public @Nullable String getItemKey(Material item) {
public static @Nullable String getItemKey(Material item) {
if (item == null) return null;
else if (item.isBlock()) {
return getBlockKey(item);
@ -51,7 +54,7 @@ public class LanguageKeyHandler {
/** Returns the official Key from the NameSpacedKey for the block Material provided,
or return null if no enum constant can be retrieved.*/
public @Nullable String getBlockKey(Material block) {
public static @Nullable String getBlockKey(Material block) {
if (block == null) return null;
else if (block.toString().toLowerCase().contains("wall_banner")) { //replace wall_banner with regular banner, since there is no key for wall banners
String blockName = block.toString().toLowerCase().replace("wall_", "");
@ -63,11 +66,11 @@ public class LanguageKeyHandler {
}
}
private void generateDefaultKeys() {
private static void generateDefaultKeys() {
Arrays.stream(Statistic.values()).forEach(statistic -> statNameKeys.put(statistic, statistic.toString().toLowerCase()));
}
private void generateStatNameKeys() {
private static void generateStatNameKeys() {
//get the enum names for all statistics first
generateDefaultKeys();

View File

@ -1,556 +0,0 @@
package com.gmail.artemis.the.gr8.playerstats.msg;
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.statistic.StatRequest;
import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger;
import com.gmail.artemis.the.gr8.playerstats.utils.NumberFormatter;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import net.kyori.adventure.util.Index;
import org.bukkit.Bukkit;
import org.bukkit.Statistic;
import org.bukkit.map.MinecraftFont;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static net.kyori.adventure.text.Component.*;
public class MessageFactory {
private static ConfigHandler config;
private final LanguageKeyHandler language;
private final TextColor msgColor; //my favorite shade of light blue, somewhere between blue and aqua
private final TextColor hoverBaseColor; //light blue - one shade lighter than msgColor
private final TextColor accentColor1; //gold - one shade lighter than standard gold
private final TextColor accentColor2; //yellow - a few shades darker than standard yellow
public MessageFactory(ConfigHandler c, LanguageKeyHandler l) {
config = c;
language = l;
msgColor = TextColor.fromHexString("#55AAFF");
hoverBaseColor = TextColor.fromHexString("#55C6FF");
accentColor1 = TextColor.fromHexString("#FFB80E");
accentColor2 = TextColor.fromHexString("#FFD52B");
}
protected TextComponent pluginPrefix(boolean isConsoleSender) {
return text("[")
.color(NamedTextColor.GRAY)
.append(text("PlayerStats").color(NamedTextColor.GOLD))
.append(text("]"))
.append(space());
}
public TextComponent reloadedConfig(boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("Config reloaded!")
.color(msgColor));
}
public TextComponent stillReloading(boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("The plugin is still (re)loading, your request will be processed when it is done!")
.color(msgColor));
}
public TextComponent partiallyReloaded(boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("The reload process was interrupted. If you notice unexpected behavior, please reload PlayerStats again to fix it!")
.color(msgColor));
}
public TextComponent waitAMoment(boolean longWait, boolean isConsoleSender) {
return longWait ? pluginPrefix(isConsoleSender)
.append(text("Calculating statistics, this may take a minute...")
.color(msgColor))
: pluginPrefix(isConsoleSender)
.append(text("Calculating statistics, this may take a few moments...")
.color(msgColor));
}
public TextComponent formatExceptions(@NotNull String exception, boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text(exception)
.color(msgColor));
}
public TextComponent missingStatName(boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("Please provide a valid statistic name!")
.color(msgColor));
}
public TextComponent missingSubStatName(Statistic.Type statType, boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("Please add a valid ")
.append(text(getSubStatTypeName(statType)))
.append(text(" to look up this statistic!"))
.color(msgColor));
}
public TextComponent missingPlayerName(boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("Please specify a valid player-name!")
.color(msgColor));
}
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatEntry, boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("\"")
.append(text(subStatEntry))
.append(text("\""))
.append(text(" is not a valid "))
.append(text(getSubStatTypeName(statType)))
.append(text("!"))
.color(msgColor));
}
public TextComponent unknownError(boolean isConsoleSender) {
return pluginPrefix(isConsoleSender)
.append(text("Something went wrong with your request, please try again or see /statistic for a usage explanation!")
.color(msgColor));
}
public TextComponent helpMsg(boolean isConsoleSender) {
if (!isConsoleSender) {
return config.useHoverText() ? helpMsgHover() : helpMsgPlain(false);
}
else {
return helpMsgPlain(true);
}
}
public TextComponent usageExamples(boolean isConsoleSender) {
TextComponent spaces = text(" "); //4 spaces
TextComponent arrow = text("").color(NamedTextColor.GOLD);
TextColor accentColor = TextColor.fromHexString("#FFE339");
if (isConsoleSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit")) {
arrow = text("-> ").color(NamedTextColor.GOLD);
accentColor = NamedTextColor.YELLOW;
}
return Component.newline()
.append(getPrefixAsTitle(isConsoleSender))
.append(newline())
.append(text("Examples: ").color(NamedTextColor.GOLD))
.append(newline())
.append(spaces).append(arrow)
.append(text("/statistic animals_bred top").color(accentColor))
.append(newline())
.append(spaces).append(arrow)
.append(text("/statistic mine_block diorite me").color(accentColor))
.append(newline())
.append(spaces).append(arrow)
.append(text("/statistic deaths player Artemis_the_gr8").color(accentColor))
.append(newline());
}
public TextComponent formatPlayerStat(int stat, @NotNull StatRequest request) {
if (!request.isValid()) return unknownError(request.isConsoleSender());
return Component.text()
.append(playerNameComponent(Target.PLAYER, request.getPlayerName() + ": "))
.append(statNumberComponent(Target.PLAYER, stat))
.append(space())
.append(statNameComponent(request))
.append(space())
.build();
}
public TextComponent formatTopStats(@NotNull LinkedHashMap<String, Integer> topStats, @NotNull StatRequest request) {
if (!request.isValid()) return unknownError(request.isConsoleSender());
TextComponent.Builder topList = Component.text();
topList.append(getTopStatTitle(topStats.size(), request));
boolean useDots = config.useDots();
Set<String> playerNames = topStats.keySet();
MinecraftFont font = new MinecraftFont();
int count = 0;
for (String playerName : playerNames) {
count = count+1;
topList.append(newline())
.append(rankingNumberComponent(count + ". "))
.append(playerNameComponent(Target.TOP, playerName));
if (useDots) {
topList.append(space());
int dots = (int) Math.round((130.0 - font.getWidth(count + ". " + playerName))/2);
if (request.isConsoleSender()) {
dots = (int) Math.round((130.0 - font.getWidth(count + ". " + playerName))/6) + 7;
}
else if (config.playerNameIsBold()) {
dots = (int) Math.round((130.0 - font.getWidth(count + ". ") - (font.getWidth(playerName) * 1.19))/2);
}
if (dots >= 1) {
topList.append(dotsComponent(".".repeat(dots)));
}
}
else {
topList.append(playerNameComponent(Target.TOP, ":"));
}
topList.append(space()).append(statNumberComponent(Target.TOP, topStats.get(playerName)));
}
return topList.build();
}
public TextComponent formatServerStat(long stat, @NotNull StatRequest request) {
if (!request.isValid()) return unknownError(request.isConsoleSender());
return Component.text()
.append(titleComponent(Target.SERVER, config.getServerTitle()))
.append(space())
.append(serverNameComponent())
.append(space())
.append(statNumberComponent(Target.SERVER, stat))
.append(space())
.append(statNameComponent(request))
.append(space())
.build();
}
protected TextComponent getPrefixAsTitle(boolean isConsoleSender) {
String underscores = "____________"; //12 underscores for both console and in-game
TextColor underscoreColor = TextColor.fromHexString("#6E3485"); //a dark shade of purple
if (isConsoleSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit")) {
underscoreColor = NamedTextColor.DARK_PURPLE;
}
return text(underscores).color(underscoreColor)
.append(text(" ")) //4 spaces
.append(pluginPrefix(isConsoleSender))
.append(text(" ")) //3 spaces (since prefix already has one)
.append(text(underscores));
}
protected TextComponent getTopStatTitle(int topLength, @NotNull StatRequest request) {
return Component.text()
.append(newline())
.append(pluginPrefix(request.isConsoleSender()))
.append(titleComponent(Target.TOP, config.getTopStatsTitle()))
.append(space())
.append(titleNumberComponent(topLength))
.append(space())
.append(statNameComponent(request))
.append(space())
.build();
}
protected TextComponent playerNameComponent(Target selection, String playerName) {
return getComponent(playerName,
getColorFromString(config.getPlayerNameFormatting(selection, false)),
getStyleFromString(config.getPlayerNameFormatting(selection, true)));
}
protected TranslatableComponent statNameComponent(@NotNull StatRequest request) {
if (request.getStatistic() == null) return null;
TextColor statNameColor = getColorFromString(config.getStatNameFormatting(request.getSelection(), false));
TextDecoration statNameStyle = getStyleFromString(config.getStatNameFormatting(request.getSelection(), true));
String statName = request.getStatistic().name();
String subStatName = request.getSubStatEntry();
if (!config.useTranslatableComponents()) {
statName = getPrettyName(statName);
subStatName = getPrettyName(subStatName);
}
else {
statName = language.getStatKey(request.getStatistic());
switch (request.getStatistic().getType()) {
case BLOCK -> subStatName = language.getBlockKey(request.getBlock());
case ENTITY -> subStatName = language.getEntityKey(request.getEntity());
case ITEM -> subStatName = language.getItemKey(request.getItem());
case UNTYPED -> {
}
}
}
TextComponent subStat = subStatNameComponent(request.getSelection(), subStatName);
TranslatableComponent.Builder totalName;
if (statName.equalsIgnoreCase("stat_type.minecraft.killed") && subStat != null) {
totalName = killEntityComponent(subStat);
}
else if (statName.equalsIgnoreCase("stat_type.minecraft.killed_by") && subStat != null) {
totalName = entityKilledByComponent(subStat);
}
else {
totalName = translatable().key(statName);
if (subStat != null) totalName.append(space()).append(subStat);
}
if (statNameStyle != null) totalName.decoration(statNameStyle, TextDecoration.State.TRUE);
return totalName
.color(statNameColor)
.build();
}
protected @Nullable TextComponent subStatNameComponent(Target selection, @Nullable String subStatName) {
if (subStatName != null) {
TextDecoration style = getStyleFromString(config.getSubStatNameFormatting(selection, true));
TextComponent.Builder subStat = text()
.append(text("("))
.append(translatable()
.key(subStatName))
.append(text(")"))
.color(getColorFromString(config.getSubStatNameFormatting(selection, false)));
subStat.decorations(TextDecoration.NAMES.values(), false);
if (style != null) subStat.decoration(style, TextDecoration.State.TRUE);
return subStat.build();
}
else {
return null;
}
}
/** Construct a custom translation for kill_entity with the language key for commands.kill.success.single ("Killed %s").
@return a TranslatableComponent Builder with the subStat Component as args.*/
private TranslatableComponent.Builder killEntityComponent(@NotNull TextComponent subStat) {
return translatable()
.key("commands.kill.success.single") //"Killed %s"
.args(subStat);
}
/** Construct a custom translation for entity_killed_by with the language keys for stat.minecraft.deaths
("Number of Deaths") and book.byAuthor ("by %s").
@return a TranslatableComponent Builder with stat.minecraft.deaths as key, with a ChildComponent
with book.byAuthor as key and the subStat Component as args.*/
private TranslatableComponent.Builder entityKilledByComponent(@NotNull TextComponent subStat) {
return translatable()
.key("stat.minecraft.deaths") //"Number of Deaths"
.append(space())
.append(translatable()
.key("book.byAuthor") //"by %s"
.args(subStat));
}
protected TextComponent statNumberComponent(Target selection, long number) {
return getComponent(NumberFormatter.format(number),
getColorFromString(config.getStatNumberFormatting(selection, false)),
getStyleFromString(config.getStatNumberFormatting(selection, true)));
}
protected TextComponent titleComponent(Target selection, String content) {
return getComponent(content,
getColorFromString(config.getTitleFormatting(selection, false)),
getStyleFromString(config.getTitleFormatting(selection, true)));
}
protected TextComponent titleNumberComponent(int number) {
return getComponent(number + "",
getColorFromString(config.getTitleNumberFormatting(false)),
getStyleFromString(config.getTitleNumberFormatting(true)));
}
protected TextComponent serverNameComponent() {
TextComponent colon = text(":").color(getColorFromString(config.getServerNameFormatting(false)));
return getComponent(config.getServerName(),
getColorFromString(config.getServerNameFormatting(false)),
getStyleFromString(config.getServerNameFormatting(true)))
.append(colon);
}
protected TextComponent rankingNumberComponent(String number) {
return getComponent(number,
getColorFromString(config.getRankNumberFormatting(false)),
getStyleFromString(config.getRankNumberFormatting(true)));
}
protected TextComponent dotsComponent(String dots) {
return getComponent(dots,
getColorFromString(config.getDotsFormatting(false)),
getStyleFromString(config.getDotsFormatting(true)));
}
private TextComponent getComponent(String content, TextColor color, @Nullable TextDecoration style) {
return style == null ? text(content).color(color) : text(content).color(color).decoration(style, TextDecoration.State.TRUE);
}
/** Replace "_" with " " and capitalize each first letter of the input.
@param input String to prettify, case-insensitive*/
private String getPrettyName(String input) {
if (input == null) return null;
StringBuilder capitals = new StringBuilder(input.toLowerCase());
capitals.setCharAt(0, Character.toUpperCase(capitals.charAt(0)));
while (capitals.indexOf("_") != -1) {
MyLogger.replacingUnderscores();
int index = capitals.indexOf("_");
capitals.setCharAt(index + 1, Character.toUpperCase(capitals.charAt(index + 1)));
capitals.setCharAt(index, ' ');
}
return capitals.toString();
}
private TextColor getColorFromString(String configString) {
if (configString != null) {
try {
if (configString.contains("#")) {
return TextColor.fromHexString(configString);
}
else {
return getTextColorByName(configString);
}
}
catch (IllegalArgumentException | NullPointerException exception) {
Bukkit.getLogger().warning(exception.toString());
}
}
return null;
}
private TextColor getTextColorByName(String textColor) {
Index<String, NamedTextColor> names = NamedTextColor.NAMES;
return names.value(textColor);
}
private @Nullable TextDecoration getStyleFromString(@NotNull String configString) {
if (configString.equalsIgnoreCase("none")) {
return null;
}
else if (configString.equalsIgnoreCase("magic")) {
return TextDecoration.OBFUSCATED;
}
else {
Index<String, TextDecoration> styles = TextDecoration.NAMES;
return styles.value(configString);
}
}
/** Returns "block", "entity", "item", or "sub-statistic" if the provided Type is null. */
private String getSubStatTypeName(Statistic.Type statType) {
String subStat = "sub-statistic";
if (statType == null) return subStat;
switch (statType) {
case BLOCK -> subStat = "block";
case ENTITY -> subStat = "entity";
case ITEM -> subStat = "item";
}
return subStat;
}
/** Returns the usage-explanation with hovering text */
private @NotNull TextComponent helpMsgHover() {
TextComponent spaces = text(" "); //4 spaces
TextComponent arrow = text("").color(NamedTextColor.GOLD); //alt + 26
TextColor arguments = NamedTextColor.YELLOW;
return Component.newline()
.append(getPrefixAsTitle(false))
.append(newline())
.append(text("Hover over the arguments for more information!").color(NamedTextColor.GRAY).decorate(TextDecoration.ITALIC))
.append(newline())
.append(text("Usage: ").color(NamedTextColor.GOLD)).append(text("/statistic").color(arguments))
.append(newline())
.append(spaces).append(arrow)
.append(text("name").color(arguments)
.hoverEvent(HoverEvent.showText(text("The name that describes the statistic").color(hoverBaseColor)
.append(newline())
.append(text("Example: ").color(accentColor1))
.append(text("\"animals_bred\"").color(accentColor2)))))
.append(newline())
.append(spaces).append(arrow)
.append(text("sub-statistic").color(arguments)
.hoverEvent(HoverEvent.showText(
text("Some statistics need an item, block or entity as extra input").color(hoverBaseColor)
.append(newline())
.append(text("Example: ").color(accentColor1)
.append(text("\"mine_block diorite\"").color(accentColor2))))))
.append(newline())
.append(spaces)
.append(text("").color(NamedTextColor.GOLD)
.hoverEvent(HoverEvent.showText(
text("Choose one").color(TextColor.fromHexString("#6E3485")))))
.append(space())
.append(text("me").color(arguments)
.hoverEvent(HoverEvent.showText(
text("See your own statistic").color(hoverBaseColor))))
.append(text(" | ").color(arguments))
.append(text("player").color(arguments)
.hoverEvent(HoverEvent.showText(
text("Choose any player that has played on your server").color(hoverBaseColor))))
.append(text(" | ").color(arguments))
.append(text("server").color(arguments)
.hoverEvent(HoverEvent.showText(
text("See the combined total for everyone on your server").color(hoverBaseColor))))
.append(text(" | ").color(arguments))
.append(text("top").color(arguments)
.hoverEvent(HoverEvent.showText(
text("See the top ").color(hoverBaseColor)
.append(text(config.getTopListMaxSize()).color(hoverBaseColor)))))
.append(newline())
.append(spaces).append(arrow)
.append(text("player-name").color(arguments)
.hoverEvent(HoverEvent.showText(
text("In case you typed ").color(hoverBaseColor)
.append(text("\"player\"").color(accentColor2)
.append(text(", add the player's name").color(hoverBaseColor))))));
}
/** Returns the usage-explanation without any hovering text.
If BukkitVersion is CraftBukkit, this doesn't use unicode symbols or hex colors */
private @NotNull TextComponent helpMsgPlain(boolean isConsoleSender) {
TextComponent spaces = text(" "); //4 spaces
TextComponent arrow = text("").color(NamedTextColor.GOLD); //alt + 26;
TextComponent bullet = text("").color(NamedTextColor.GOLD); //alt + 7
TextColor arguments = NamedTextColor.YELLOW;
TextColor accentColor = accentColor2;
if (isConsoleSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit")) {
arrow = text("-> ").color(NamedTextColor.GOLD);
bullet = text("* ").color(NamedTextColor.GOLD);
accentColor = NamedTextColor.GOLD;
}
return Component.newline()
.append(getPrefixAsTitle(isConsoleSender))
.append(newline())
.append(text("Type \"/statistic examples\" to see examples!").color(NamedTextColor.GRAY).decorate(TextDecoration.ITALIC))
.append(newline())
.append(text("Usage: ").color(NamedTextColor.GOLD))
.append(text("/statistic").color(arguments))
.append(newline())
.append(spaces).append(arrow)
.append(text("name").color(arguments))
.append(newline())
.append(spaces).append(arrow)
.append(text("{sub-statistic}").color(arguments))
.append(space())
.append(text("(a block, item or entity)").color(NamedTextColor.GRAY))
.append(newline())
.append(spaces).append(arrow)
.append(text("me | player | server | top").color(arguments))
.append(newline())
.append(spaces).append(spaces).append(bullet)
.append(text("me:").color(accentColor))
.append(space()).append(text("your own statistic").color(NamedTextColor.GRAY))
.append(newline())
.append(spaces).append(spaces).append(bullet)
.append(text("player:").color(accentColor))
.append(space()).append(text("choose a player").color(NamedTextColor.GRAY))
.append(newline())
.append(spaces).append(spaces).append(bullet)
.append(text("server:").color(accentColor))
.append(space()).append(text("everyone on the server combined").color(NamedTextColor.GRAY))
.append(newline())
.append(spaces).append(spaces).append(bullet)
.append(text("top:").color(accentColor))
.append(space()).append(text("the top").color(NamedTextColor.GRAY)
.append(space()).append(text(config.getTopListMaxSize())))
.append(newline())
.append(spaces).append(arrow)
.append(text("{player-name}").color(arguments));
}
}

View File

@ -0,0 +1,293 @@
package com.gmail.artemis.the.gr8.playerstats.msg;
import com.gmail.artemis.the.gr8.playerstats.enums.PluginColor;
import com.gmail.artemis.the.gr8.playerstats.enums.Target;
import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import com.gmail.artemis.the.gr8.playerstats.statistic.StatRequest;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.TextColor;
import org.bukkit.Bukkit;
import org.bukkit.Statistic;
import org.bukkit.map.MinecraftFont;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import static net.kyori.adventure.text.Component.*;
/** Composes messages to send to Players 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.*/
public class MessageWriter {
private static ConfigHandler config;
private static ComponentFactory componentFactory;
public MessageWriter(ConfigHandler c) {
config = c;
getComponentFactory();
}
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.msg(
"Config reloaded!", isBukkitConsole);
}
public TextComponent stillReloading(boolean isBukkitConsole) {
return componentFactory.msg(
"The plugin is (re)loading, " +
"your request will be processed when it is done!", isBukkitConsole);
}
public TextComponent waitAMoment(boolean longWait, boolean isBukkitConsole) {
String msg = longWait ? "Calculating statistics, this may take a minute..." :
"Calculating statistics, this may take a few moments...";
return componentFactory.msg(msg, isBukkitConsole);
}
public TextComponent missingStatName(boolean isBukkitConsole) {
return componentFactory.msg(
"Please provide a valid statistic name!", isBukkitConsole);
}
public TextComponent missingSubStatName(Statistic.Type statType, boolean isBukkitConsole) {
return componentFactory.msg(
"Please add a valid " +
getSubStatTypeName(statType) +
" to look up this statistic!", isBukkitConsole);
}
public TextComponent missingPlayerName(boolean isBukkitConsole) {
return componentFactory.msg(
"Please specify a valid player-name!", isBukkitConsole);
}
public TextComponent wrongSubStatType(Statistic.Type statType, String subStatEntry, boolean isBukkitConsole) {
return componentFactory.msg(
"\"" + subStatEntry + "\" is not a valid " + getSubStatTypeName(statType) + "!", isBukkitConsole);
}
public TextComponent unknownError(boolean isBukkitConsole) {
return componentFactory.msg(
"Something went wrong with your request, " +
"please try again or see /statistic for a usage explanation!", isBukkitConsole);
}
public TextComponent formatPlayerStat(int stat, @NotNull StatRequest request) {
if (!request.isValid()) return unknownError(request.isBukkitConsoleSender());
return Component.text()
.append(componentFactory.playerName( request.getPlayerName() + ": ", Target.PLAYER))
.append(componentFactory.statNumber(stat, Target.PLAYER))
.append(space())
.append(componentFactory.statName(request))
.append(space())
.build();
}
public TextComponent formatTopStats(@NotNull LinkedHashMap<String, Integer> topStats, @NotNull StatRequest request) {
if (!request.isValid()) return unknownError(request.isBukkitConsoleSender());
TextComponent.Builder topList = Component.text()
.append(newline())
.append(componentFactory.pluginPrefix(request.isBukkitConsoleSender()))
.append(componentFactory.title(config.getTopStatsTitle(), Target.TOP))
.append(space())
.append(componentFactory.titleNumber(topStats.size()))
.append(space())
.append(componentFactory.statName(request));
boolean useDots = config.useDots();
boolean boldNames = config.playerNameIsBold();
Set<String> playerNames = topStats.keySet();
MinecraftFont font = new MinecraftFont();
int count = 0;
for (String playerName : playerNames) {
count = count+1;
topList.append(newline())
.append(componentFactory.rankingNumber(count + ". "))
.append(componentFactory.playerName(playerName, Target.TOP));
if (useDots) {
topList.append(space());
int dots = (int) Math.round((130.0 - font.getWidth(count + ". " + playerName))/2);
if (request.isConsoleSender()) {
dots = (int) Math.round((130.0 - font.getWidth(count + ". " + playerName))/6) + 7;
}
else if (boldNames) {
dots = (int) Math.round((130.0 - font.getWidth(count + ". ") - (font.getWidth(playerName) * 1.19))/2);
}
if (dots >= 1) {
topList.append(componentFactory.dots(".".repeat(dots)));
}
}
else {
topList.append(componentFactory.playerName(":", Target.TOP));
}
topList.append(space()).append(componentFactory.statNumber(topStats.get(playerName), Target.TOP));
}
return topList.build();
}
public TextComponent formatServerStat(long stat, @NotNull StatRequest request) {
if (!request.isValid()) return unknownError(request.isBukkitConsoleSender());
return Component.text()
.append(componentFactory.title(config.getServerTitle(), Target.SERVER))
.append(space())
.append(componentFactory.serverName(config.getServerName()))
.append(space())
.append(componentFactory.statNumber(stat, Target.SERVER))
.append(space())
.append(componentFactory.statName(request))
.append(space())
.build();
}
/** Returns "block", "entity", "item", or "sub-statistic" if the provided Type is null. */
private String getSubStatTypeName(Statistic.Type statType) {
String subStat = "sub-statistic";
if (statType == null) return subStat;
switch (statType) {
case BLOCK -> subStat = "block";
case ENTITY -> subStat = "entity";
case ITEM -> subStat = "item";
}
return subStat;
}
public TextComponent usageExamples(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
return Component.newline()
.append(componentFactory.prefixTitle(isBukkitConsole))
.append(newline())
.append(text("Examples: ").color(mainColor))
.append(newline())
.append(text(arrow).color(mainColor)
.append(text("/statistic ")
.append(text("animals_bred ").color(accentColor1)
.append(text("top").color(accentColor3)))))
.append(newline())
.append(text(arrow).color(mainColor)
.append(text("/statistic ")
.append(text("mine_block diorite ").color(accentColor1)
.append(text("me").color(accentColor3)))))
.append(newline())
.append(text(arrow).color(mainColor)
.append(text("/statistic ")
.append(text("deaths ").color(accentColor1)
.append(text("player ").color(accentColor3)
.append(text("Artemis_the_gr8"))))));
}
public TextComponent helpMsg(boolean isConsoleSender) {
if (isConsoleSender || !config.useHoverText()) {
return helpMsgPlain(isConsoleSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit"));
}
else {
return helpMsgHover();
}
}
/** Returns the usage-explanation with hovering text */
private TextComponent helpMsgHover() {
String arrow = ""; //4 spaces, alt + 26
return Component.newline()
.append(componentFactory.prefixTitle(false))
.append(newline())
.append(componentFactory.subTitle("Hover over the arguments for more information!"))
.append(newline())
.append(componentFactory.msgPart("Usage:", null, "/statistic", null))
.append(newline())
.append(componentFactory.msgPart(arrow, null, null, null)
.append(componentFactory.complexHoverPart("name", PluginColor.LIGHT_GOLD,
"The name that describes the statistic",
"Example:",
"\"animals_bred\"")))
.append(newline())
.append(componentFactory.msgPart(arrow, null, null, null)
.append(componentFactory.complexHoverPart("sub-statistic", PluginColor.LIGHT_GOLD,
"Some statistics need an item, block or entity as extra input",
"Example:",
"\"mine_block diorite\"")))
.append(newline())
.append(text(" ").color(PluginColor.LIGHT_GOLD.getColor())
.append(componentFactory.simpleHoverPart(
"", PluginColor.GOLD,
"Choose one", PluginColor.DARK_PURPLE))
.append(space())
.append(componentFactory.simpleHoverPart(
"me",
"See your own statistic", PluginColor.LIGHT_BLUE))
.append(text(" | "))
.append(componentFactory.simpleHoverPart(
"player",
"Choose any player that has played on your server", PluginColor.LIGHT_BLUE))
.append(text(" | "))
.append(componentFactory.simpleHoverPart(
"server",
"See the combined total for everyone on your server", PluginColor.LIGHT_BLUE))
.append(text(" | "))
.append(componentFactory.simpleHoverPart(
"top",
"See the top " + config.getTopListMaxSize(), PluginColor.LIGHT_BLUE)))
.append(newline())
.append(componentFactory.msgPart(arrow, null, null, null)
.append(text("player-name").color(PluginColor.LIGHT_GOLD.getColor())
.hoverEvent(HoverEvent.showText(
text("In case you typed ").color(PluginColor.LIGHT_BLUE.getColor())
.append(text("\"player\"").color(PluginColor.MEDIUM_GOLD.getColor()))
.append(text(", add the player's name"))))));
}
/** Returns the usage-explanation without any hovering text.
If BukkitVersion is CraftBukkit, this doesn't use unicode symbols or hex colors */
private TextComponent helpMsgPlain(boolean isBukkitConsole) {
String arrow = isBukkitConsole ? " ->" : ""; //4 spaces, alt + 26
String bullet = isBukkitConsole ? " *" : ""; //8 spaces, alt + 7
return Component.newline()
.append(componentFactory.prefixTitle(isBukkitConsole))
.append(newline())
.append(componentFactory.subTitle("Type \"statistic examples\" to see examples!"))
.append(newline())
.append(componentFactory.msgPart("Usage:", null, "/statistic", null, isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(arrow, null, "name", null, isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(arrow, null, "{sub-statistic}", "(a block, item or entity)", isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(arrow, null, "me | player | server | top", null, isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(bullet, "me:", null, "your own statistic", isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(bullet, "player:", null, "choose a player", isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(bullet, "server:", null, "everyone on the server combined", isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(bullet, "top:", null, "the top " + config.getTopListMaxSize(), isBukkitConsole))
.append(newline())
.append(componentFactory.msgPart(arrow, null, "{player-name}", null, isBukkitConsole));
}
}

View File

@ -4,7 +4,6 @@ import com.gmail.artemis.the.gr8.playerstats.config.ConfigHandler;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Bukkit;
import java.time.LocalDate;
import java.time.Month;
@ -12,20 +11,20 @@ import java.time.Month;
import static net.kyori.adventure.text.Component.*;
public class PrideMessageFactory extends MessageFactory {
public class PrideComponentFactory extends ComponentFactory {
private static ConfigHandler config;
public PrideMessageFactory(ConfigHandler c, LanguageKeyHandler l) {
super(c, l);
public PrideComponentFactory(ConfigHandler c) {
super(c);
config = c;
}
@Override
protected TextComponent getPrefixAsTitle(boolean isConsoleSender) {
if (cancelRainbow(isConsoleSender)) {
return super.getPrefixAsTitle(isConsoleSender);
public TextComponent prefixTitle(boolean isBukkitConsole) {
if (cancelRainbow(isBukkitConsole)) {
return super.prefixTitle(isBukkitConsole);
}
else {
String title = "<rainbow:16>____________ [PlayerStats] ____________</rainbow>"; //12 underscores
@ -36,7 +35,7 @@ public class PrideMessageFactory extends MessageFactory {
}
@Override
protected TextComponent pluginPrefix(boolean isConsoleSender) {
public TextComponent pluginPrefix(boolean isConsoleSender) {
if (cancelRainbow(isConsoleSender)) {
return super.pluginPrefix(isConsoleSender);
}
@ -61,8 +60,8 @@ public class PrideMessageFactory extends MessageFactory {
/** 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 isConsoleSender) {
return !(config.useRainbowPrefix() || (config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE))) ||
(isConsoleSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit"));
private boolean cancelRainbow(boolean isBukkitConsole) {
return !(config.useRainbowMode() || (config.useFestiveFormatting() && LocalDate.now().getMonth().equals(Month.JUNE))) ||
(isBukkitConsole);
}
}

View File

@ -1,10 +1,10 @@
package com.gmail.artemis.the.gr8.playerstats.reload;
import com.gmail.artemis.the.gr8.playerstats.Main;
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.statistic.StatThread;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageFactory;
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;
@ -12,11 +12,9 @@ import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@ -29,20 +27,18 @@ public class ReloadThread extends Thread {
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static MessageFactory messageFactory;
private final Main plugin;
private static MessageWriter messageWriter;
private final StatThread statThread;
private final CommandSender sender;
public ReloadThread(BukkitAudiences a, ConfigHandler c, MessageFactory m, Main p, int threshold, int ID, @Nullable StatThread s, @Nullable CommandSender se) {
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;
config = c;
messageFactory = m;
plugin = p;
messageWriter = m;
statThread = s;
sender = se;
@ -56,79 +52,68 @@ public class ReloadThread extends Thread {
long time = System.currentTimeMillis();
MyLogger.threadStart(this.getName());
//if reload is triggered by /statreload (aka this thread does not have ID number 1)...
if (reloadThreadID != 1) {
if (statThread != null && statThread.isAlive()) {
try {
MyLogger.waitingForOtherThread(this.getName(), statThread.getName());
statThread.join();
} catch (InterruptedException e) {
MyLogger.logException(e, "ReloadThread", "run(), trying to join" + statThread.getName());
throw new RuntimeException(e);
}
}
plugin.getLogger().info("Reloading!");
if (config.reloadConfig()) {
try {
OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap());
}
catch (ConcurrentModificationException e) {
MyLogger.logException(e, "ReloadThread", "run(), trying to update OfflinePlayerList during a reload");
if (sender != null) {
adventure.sender(sender).sendMessage(messageFactory.partiallyReloaded(sender instanceof ConsoleCommandSender));
}
}
MyLogger.logTimeTakenDefault("ReloadThread", ("loaded " + OfflinePlayerHandler.getOfflinePlayerCount() + " offline players"), time);
if (sender != null) {
adventure.sender(sender).sendMessage(messageFactory.reloadedConfig(sender instanceof ConsoleCommandSender));
}
if (statThread != null && statThread.isAlive()) {
try {
MyLogger.waitingForOtherThread(this.getName(), statThread.getName());
statThread.join();
} catch (InterruptedException e) {
MyLogger.logException(e, "ReloadThread", "run(), trying to join" + statThread.getName());
throw new RuntimeException(e);
}
}
//during first start-up...
else {
try {
OfflinePlayerHandler.updateOfflinePlayerList(getPlayerMap());
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
MyLogger.logTimeTakenDefault("ReloadThread",
("loaded " + OfflinePlayerHandler.getOfflinePlayerCount() + " offline players"), time);
}
catch (ConcurrentModificationException e) {
MyLogger.logException(e, "ReloadThread", "run(), trying to update OfflinePlayerList during first start-up");
if (reloadThreadID != 1 && config.reloadConfig()) { //during a reload
MyLogger.logMsg("Reloading!", false);
MyLogger.setDebugLevel(config.getDebugLevel());
MessageWriter.updateComponentFactory();
loadOfflinePlayers();
boolean isBukkitConsole = sender instanceof ConsoleCommandSender && Bukkit.getName().equalsIgnoreCase("CraftBukkit");
if (sender != null) {
adventure.sender(sender).sendMessage(
messageWriter.reloadedConfig(isBukkitConsole));
}
}
else { //during first start-up
MyLogger.setDebugLevel(config.getDebugLevel());
loadOfflinePlayers();
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
}
}
private @NotNull ConcurrentHashMap<String, UUID> getPlayerMap() throws ConcurrentModificationException {
private void loadOfflinePlayers() {
long time = System.currentTimeMillis();
OfflinePlayer[] offlinePlayers;
if (config.whitelistOnly()) {
offlinePlayers = Bukkit.getWhitelistedPlayers().toArray(OfflinePlayer[]::new);
MyLogger.logTimeTaken("ReloadThread", "retrieved whitelist", time);
MyLogger.logTimeTaken("ReloadThread",
"retrieved whitelist", time, DebugLevel.MEDIUM);
}
else if (config.excludeBanned()) {
Set<OfflinePlayer> bannedPlayers = Bukkit.getBannedPlayers();
offlinePlayers = Arrays.stream(Bukkit.getOfflinePlayers())
.parallel()
.filter(offlinePlayer -> !bannedPlayers.contains(offlinePlayer)).toArray(OfflinePlayer[]::new);
MyLogger.logTimeTaken("ReloadThread", "retrieved banlist", time);
MyLogger.logTimeTaken("ReloadThread",
"retrieved banlist", time, DebugLevel.MEDIUM);
}
else {
offlinePlayers = Bukkit.getOfflinePlayers();
MyLogger.logTimeTaken("ReloadThread", "retrieved list of Offline Players", time);
MyLogger.logTimeTaken("ReloadThread",
"retrieved list of Offline Players", time, DebugLevel.MEDIUM);
}
int size = offlinePlayers != null ? offlinePlayers.length : 16;
ConcurrentHashMap<String, UUID> playerMap = new ConcurrentHashMap<>(size);
ReloadAction task = new ReloadAction(threshold, offlinePlayers, config.lastPlayedLimit(), playerMap);
ReloadAction task = new ReloadAction(threshold, offlinePlayers, config.getLastPlayedLimit(), playerMap);
MyLogger.actionCreated((offlinePlayers != null) ? offlinePlayers.length : 0);
ForkJoinPool.commonPool().invoke(task);
MyLogger.actionFinished(1);
MyLogger.logTimeTaken("ReloadThread",
("loaded " + OfflinePlayerHandler.getOfflinePlayerCount() + " offline players"), time);
return playerMap;
OfflinePlayerHandler.updateOfflinePlayerList(playerMap);
}
}

View File

@ -1,6 +1,7 @@
package com.gmail.artemis.the.gr8.playerstats.statistic;
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;
@ -57,6 +58,10 @@ 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

@ -2,18 +2,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.Target;
import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter;
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.gmail.artemis.the.gr8.playerstats.msg.MessageFactory;
import com.google.common.collect.ImmutableList;
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
import org.bukkit.OfflinePlayer;
import org.bukkit.Statistic;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -31,11 +28,11 @@ public class StatThread extends Thread {
private final BukkitAudiences adventure;
private static ConfigHandler config;
private static MessageFactory messageFactory;
private static MessageWriter messageWriter;
private final Main plugin;
//constructor (called on thread creation)
public StatThread(BukkitAudiences a, ConfigHandler c, MessageFactory m, Main p, int ID, int threshold, StatRequest s, @Nullable ReloadThread r) {
public StatThread(BukkitAudiences a, ConfigHandler c, MessageWriter m, Main p, int ID, int threshold, StatRequest s, @Nullable ReloadThread r) {
this.threshold = threshold;
request = s;
@ -43,7 +40,7 @@ public class StatThread extends Thread {
adventure = a;
config = c;
messageFactory = m;
messageWriter = m;
plugin = p;
this.setName("StatThread-" + ID);
@ -55,67 +52,55 @@ public class StatThread extends Thread {
public void run() throws IllegalStateException, NullPointerException {
MyLogger.threadStart(this.getName());
if (messageFactory == null || plugin == null) {
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(messageFactory
.stillReloading(request.getCommandSender() instanceof ConsoleCommandSender));
.sendMessage(messageWriter
.stillReloading(request.isBukkitConsoleSender()));
reloadThread.join();
} catch (InterruptedException e) {
plugin.getLogger().warning(e.toString());
MyLogger.logException(e, "StatThread", "Trying to join" + reloadThread.getName());
throw new RuntimeException(e);
}
}
CommandSender sender = request.getCommandSender();
boolean isConsoleSencer = sender instanceof ConsoleCommandSender;
Target selection = request.getSelection();
if (selection == Target.TOP || selection == Target.SERVER) {
if (ThreadManager.getLastRecordedCalcTime() > 20000) {
adventure.sender(sender).sendMessage(messageFactory.waitAMoment(true, isConsoleSencer));
if (selection == Target.PLAYER) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.formatPlayerStat(getIndividualStat(), request));
}
else {
if (ThreadManager.getLastRecordedCalcTime() > 2000) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.waitAMoment(ThreadManager.getLastRecordedCalcTime() > 20000, request.isBukkitConsoleSender()));
}
else if (ThreadManager.getLastRecordedCalcTime() > 2000) {
adventure.sender(sender).sendMessage(messageFactory.waitAMoment(false, isConsoleSencer));
}
try {
if (selection == Target.TOP) {
adventure.sender(sender).sendMessage(messageFactory.formatTopStats(getTopStats(), request));
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.formatTopStats(getTopStats(), request));
} else {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.formatServerStat(getServerTotal(), request));
}
else {
adventure.sender(sender).sendMessage(messageFactory.formatServerStat(getServerTotal(), request));
}
} catch (ConcurrentModificationException e) {
if (!isConsoleSencer) {
adventure.sender(sender).sendMessage(messageFactory.unknownError(false));
if (!request.isConsoleSender()) {
adventure.sender(request.getCommandSender()).sendMessage(
messageWriter.unknownError(false));
}
} catch (Exception e) {
adventure.sender(sender).sendMessage(messageFactory.formatExceptions(e.toString(), isConsoleSencer));
MyLogger.logException(e, "StatThread", "run(), trying to calculate or format a top or server statistic");
}
}
else if (selection == Target.PLAYER) {
try {
adventure.sender(sender).sendMessage(
messageFactory.formatPlayerStat(getIndividualStat(), request));
} catch (UnsupportedOperationException | NullPointerException e) {
adventure.sender(sender).sendMessage(messageFactory.formatExceptions(e.toString(), isConsoleSencer));
}
}
}
private LinkedHashMap<String, Integer> getTopStats() throws ConcurrentModificationException, NullPointerException {
private LinkedHashMap<String, Integer> getTopStats() throws ConcurrentModificationException {
return getAllStats().entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(config.getTopListMaxSize()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
@ -127,7 +112,7 @@ public class StatThread extends Thread {
}
//invokes a bunch of worker pool threads to divide and conquer (get the statistics for all players in the list)
private @NotNull ConcurrentHashMap<String, Integer> getAllStats() throws ConcurrentModificationException, NullPointerException {
private @NotNull ConcurrentHashMap<String, Integer> getAllStats() throws ConcurrentModificationException {
long time = System.currentTimeMillis();
int size = OfflinePlayerHandler.getOfflinePlayerCount() != 0 ? (int) (OfflinePlayerHandler.getOfflinePlayerCount() * 1.05) : 16;
@ -141,20 +126,21 @@ public class StatThread extends Thread {
try {
commonPool.invoke(task);
} catch (ConcurrentModificationException e) {
plugin.getLogger().warning("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!");
MyLogger.logMsg("The request could not be executed due to a ConcurrentModificationException. " +
"This likely happened because Bukkit hasn't fully initialized all player-data yet. Try again and it should be fine!", true);
throw new ConcurrentModificationException(e.toString());
}
MyLogger.actionFinished(2);
ThreadManager.recordCalcTime(System.currentTimeMillis() - time);
MyLogger.logTimeTakenDefault("StatThread", "calculated all stats", time);
MyLogger.logTimeTaken("StatThread", "calculated all stats", time);
return playerStats;
}
//gets the actual statistic data for an individual player
private int getIndividualStat() throws UnsupportedOperationException, NullPointerException {
/** Gets the statistic data for an individual player. If somehow the player
cannot be found, this returns 0.*/
private int getIndividualStat() {
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(request.getPlayerName());
if (player != null) {
switch (request.getStatistic().getType()) {
@ -172,6 +158,6 @@ public class StatThread extends Thread {
}
}
}
throw new NullPointerException("The player you are trying to request either does not exist, or is not on the list for statistic lookups!");
return 0;
}
}

View File

@ -50,29 +50,26 @@ public class TopStatAction extends RecursiveAction {
}
}
private void getStatsDirectly() throws UnsupportedOperationException {
try {
Iterator<String> iterator = playerNames.iterator();
if (iterator.hasNext()) {
do {
String playerName = iterator.next();
MyLogger.actionRunning(Thread.currentThread().getName(), playerName, 2);
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
int statistic = 0;
switch (request.getStatistic().getType()) {
case UNTYPED -> statistic = player.getStatistic(request.getStatistic());
case ENTITY -> statistic = player.getStatistic(request.getStatistic(), request.getEntity());
case BLOCK -> statistic = player.getStatistic(request.getStatistic(), request.getBlock());
case ITEM -> statistic = player.getStatistic(request.getStatistic(), request.getItem());
}
if (statistic > 0) {
playerStats.put(playerName, statistic);
}
private void getStatsDirectly() {
Iterator<String> iterator = playerNames.iterator();
if (iterator.hasNext()) {
do {
String playerName = iterator.next();
MyLogger.actionRunning(Thread.currentThread().getName(), playerName, 2);
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
if (player != null) {
int statistic = 0;
switch (request.getStatistic().getType()) {
case UNTYPED -> statistic = player.getStatistic(request.getStatistic());
case ENTITY -> statistic = player.getStatistic(request.getStatistic(), request.getEntity());
case BLOCK -> statistic = player.getStatistic(request.getStatistic(), request.getBlock());
case ITEM -> statistic = player.getStatistic(request.getStatistic(), request.getItem());
}
} while (iterator.hasNext());
}
} catch (NoSuchElementException ignored) {
if (statistic > 0) {
playerStats.put(playerName, statistic);
}
}
} while (iterator.hasNext());
}
}
}

View File

@ -21,7 +21,7 @@ public class EnumHandler {
private final static List<String> entitySubStatNames;
private final static List<String> subStatNames;
static{
static {
blockNames = Arrays.stream(Material.values())
.filter(Material::isBlock)
.map(Material::toString)

View File

@ -22,7 +22,7 @@ public class MyLogger {
private static final AtomicInteger playersIndex;
private static ConcurrentHashMap<String, Integer> threadNames;
static{
static {
Plugin plugin = Bukkit.getPluginManager().getPlugin("PlayerStats");
logger = (plugin != null) ? plugin.getLogger() : Bukkit.getLogger();
debugLevel = DebugLevel.LOW;
@ -35,17 +35,6 @@ public class MyLogger {
private MyLogger() {
}
/** Accesses the playersIndex to up it by 1 and return its previous value. */
private static int nextPlayersIndex() {
return playersIndex.getAndIncrement();
}
/** Returns true if the playersIndex is 10, or any subsequent increment of 10. */
private static boolean incrementOfTen() {
return (playersIndex.get() == 10 || (playersIndex.get() > 10 && playersIndex.get() % 10 == 0));
}
/** Sets the desired debugging level.
<p>1 = low (only show unexpected errors)</p>
<p>2 = medium (detail all encountered exceptions, log main tasks and show time taken)</p>
@ -63,6 +52,30 @@ public class MyLogger {
}
}
public static void logMsg(String content, boolean logAsWarning) {
logMsg(content, DebugLevel.LOW, logAsWarning);
}
public static void logMsg(String content, DebugLevel logThreshold) {
logMsg(content, logThreshold, false);
}
public static void logMsg(String content, DebugLevel logThreshold, boolean logAsWarning) {
switch (logThreshold) {
case LOW -> log(content, logAsWarning);
case MEDIUM -> {
if (debugLevel != DebugLevel.LOW) {
log(content, logAsWarning);
}
}
case HIGH -> {
if (debugLevel == DebugLevel.HIGH) {
log(content, logAsWarning);
}
}
}
}
/** Log the encountered exception as a warning to console,
with some information about which class/method caught it
and with a printStackTrace if DebugLevel is HIGH.
@ -79,7 +92,7 @@ public class MyLogger {
}
}
/** If DebugLevel is MEDIUM or HIGH, logs when the while loop in MessageFactory, getLanguageKey is being run. */
/** If DebugLevel is MEDIUM or HIGH, logs when the while loop in MessageWriter, getLanguageKey is being run. */
public static void replacingUnderscores() {
if (debugLevel != DebugLevel.LOW) {
logger.info("Replacing underscores and capitalizing names...");
@ -171,21 +184,54 @@ public class MyLogger {
}
}
/** Output to console how long a certain task has taken if DebugLevel is MEDIUM or HIGH.
@param className Name of the class executing the task
@param methodName Name or description of the task
@param startTime Timestamp marking the beginning of the task */
public static void logTimeTaken(String className, String methodName, long startTime) {
if (debugLevel != DebugLevel.LOW) {
logger.info(className + " " + methodName + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
}
/** Output to console how long a certain task has taken (regardless of DebugLevel).
@param className Name of the class executing the task
@param methodName Name or description of the task
@param startTime Timestamp marking the beginning of the task */
public static void logTimeTakenDefault(String className, String methodName, long startTime) {
public static void logTimeTaken(String className, String methodName, long startTime) {
logTimeTaken(className, methodName, startTime, DebugLevel.LOW);
}
/** Output to console how long a certain task has taken if DebugLevel is equal to or higher than the specified threshold.
@param className Name of the class executing the task
@param methodName Name or description of the task
@param startTime Timestamp marking the beginning of the task
@param logThreshold the DebugLevel threshold */
public static void logTimeTaken(String className, String methodName, long startTime, DebugLevel logThreshold) {
switch (logThreshold) {
case LOW -> printTime(className, methodName, startTime);
case MEDIUM -> {
if (debugLevel != DebugLevel.LOW) {
printTime(className, methodName, startTime);
}
}
case HIGH -> {
if (debugLevel == DebugLevel.HIGH) {
printTime(className, methodName, startTime);
}
}
}
}
private static void log(String content, boolean logAsWarning) {
if (logAsWarning) {
logger.warning(content);
} else {
logger.info(content);
}
}
private static void printTime(String className, String methodName, long startTime) {
logger.info(className + " " + methodName + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
/** Accesses the playersIndex to up it by 1 and return its previous value. */
private static int nextPlayersIndex() {
return playersIndex.getAndIncrement();
}
/** Returns true if the playersIndex is 10, or any subsequent increment of 10. */
private static boolean incrementOfTen() {
return (playersIndex.get() == 10 || (playersIndex.get() > 10 && playersIndex.get() % 10 == 0));
}
}

View File

@ -12,7 +12,7 @@ public class OfflinePlayerHandler {
private static ConcurrentHashMap<String, UUID> offlinePlayerUUIDs;
private static ArrayList<String> playerNames;
static{
static {
offlinePlayerUUIDs = new ConcurrentHashMap<>();
playerNames = new ArrayList<>();
}

View File

@ -74,13 +74,13 @@ your-server-name: 'this server'
# # ------------------------------ # #
top-list:
title: '#FFD52B'
title: '#FFEA40'
title-style: none
title-number: gold
title-number-style: none
stat-names: '#FFD52B'
stat-names: '#FFEA40'
stat-names-style: none
sub-stat-names: yellow