mirror of
https://github.com/itHotL/PlayerStats.git
synced 2024-11-23 12:05:47 +01:00
commit
0c93fa85c9
2
pom.xml
2
pom.xml
@ -19,7 +19,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.spigotmc</groupId>
|
<groupId>org.spigotmc</groupId>
|
||||||
<artifactId>spigot-api</artifactId>
|
<artifactId>spigot-api</artifactId>
|
||||||
<version>1.18-R0.1-SNAPSHOT</version>
|
<version>1.18.2-R0.1-SNAPSHOT</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package com.gmail.artemis.the.gr8.playerstats;
|
|||||||
import com.gmail.artemis.the.gr8.playerstats.commands.ReloadCommand;
|
import com.gmail.artemis.the.gr8.playerstats.commands.ReloadCommand;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.commands.StatCommand;
|
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.TabCompleter;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.listeners.JoinListener;
|
import com.gmail.artemis.the.gr8.playerstats.listeners.JoinListener;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
||||||
@ -14,17 +15,16 @@ public class Main extends JavaPlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
|
|
||||||
ConfigHandler config = new ConfigHandler(this);
|
ConfigHandler config = new ConfigHandler(this);
|
||||||
EnumHandler enumHandler = new EnumHandler();
|
EnumHandler enumHandler = new EnumHandler(this);
|
||||||
|
|
||||||
OutputFormatter outputFormatter = new OutputFormatter(config);
|
OutputFormatter outputFormatter = new OutputFormatter(config);
|
||||||
StatManager statManager = new StatManager(enumHandler, this);
|
|
||||||
|
|
||||||
this.getCommand("statistic").setExecutor(new StatCommand(outputFormatter, statManager, this));
|
//prepare private hashMap of offline players
|
||||||
this.getCommand("statistic").setTabCompleter(new TabCompleter(
|
OfflinePlayerHandler.updateOfflinePlayers();
|
||||||
enumHandler, statManager,this));
|
|
||||||
this.getCommand("statisticreload").setExecutor(new ReloadCommand(config, outputFormatter));
|
this.getCommand("statistic").setExecutor(new StatCommand(outputFormatter, enumHandler, this));
|
||||||
|
this.getCommand("statistic").setTabCompleter(new TabCompleter(enumHandler, this));
|
||||||
|
this.getCommand("statisticreload").setExecutor(new ReloadCommand(config, outputFormatter, this));
|
||||||
|
|
||||||
Bukkit.getPluginManager().registerEvents(new JoinListener(), this);
|
Bukkit.getPluginManager().registerEvents(new JoinListener(), this);
|
||||||
this.getLogger().info("Enabled PlayerStats!");
|
this.getLogger().info("Enabled PlayerStats!");
|
||||||
@ -35,14 +35,8 @@ public class Main extends JavaPlugin {
|
|||||||
this.getLogger().info("Disabled PlayerStats!");
|
this.getLogger().info("Disabled PlayerStats!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long logTimeTaken(String className, String methodName, long previousTime, int lineNumber) {
|
||||||
public void logStatRelatedExceptions(Exception exception) {
|
getLogger().info(className + " " + methodName + " " + lineNumber + ": " + (System.currentTimeMillis() - previousTime));
|
||||||
if (exception instanceof IllegalArgumentException) {
|
return System.currentTimeMillis();
|
||||||
getLogger().warning("IllegalArgumentException - this is probably not a valid statistic name!");
|
|
||||||
}
|
|
||||||
else if (exception instanceof NullPointerException) {
|
|
||||||
getLogger().warning("NullPointerException - no statistic name was provided");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,165 +0,0 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats;
|
|
||||||
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
|
||||||
import org.bukkit.Material;
|
|
||||||
import org.bukkit.OfflinePlayer;
|
|
||||||
import org.bukkit.Statistic;
|
|
||||||
import org.bukkit.entity.EntityType;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class StatManager {
|
|
||||||
|
|
||||||
private final Main plugin;
|
|
||||||
private final EnumHandler enumHandler;
|
|
||||||
private final OfflinePlayerHandler offlinePlayerHandler;
|
|
||||||
private final List<String> statNames;
|
|
||||||
private final List<String> entityStatNames;
|
|
||||||
private final List<String> subStatEntryNames;
|
|
||||||
|
|
||||||
public StatManager(EnumHandler e, Main p) {
|
|
||||||
plugin = p;
|
|
||||||
enumHandler = e;
|
|
||||||
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
|
|
||||||
|
|
||||||
statNames = Arrays.stream(Statistic.values()).map(
|
|
||||||
Statistic::toString).map(String::toLowerCase).toList();
|
|
||||||
entityStatNames = Arrays.stream(Statistic.values()).filter(statistic ->
|
|
||||||
statistic.getType().equals(Statistic.Type.ENTITY)).map(
|
|
||||||
Statistic::toString).map(String::toLowerCase).collect(Collectors.toList());
|
|
||||||
|
|
||||||
subStatEntryNames = new ArrayList<>();
|
|
||||||
subStatEntryNames.addAll(enumHandler.getBlockNames());
|
|
||||||
subStatEntryNames.addAll(enumHandler.getEntityTypeNames());
|
|
||||||
subStatEntryNames.addAll(enumHandler.getItemNames());
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getStatistic(String statName, String playerName) throws IllegalArgumentException, NullPointerException {
|
|
||||||
return getStatistic(statName, null, playerName);
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns the integer associated with a certain statistic for a player
|
|
||||||
public int getStatistic(String statName, String subStatEntryName, String playerName) throws IllegalArgumentException, NullPointerException {
|
|
||||||
long time = System.currentTimeMillis();
|
|
||||||
|
|
||||||
OfflinePlayer player = offlinePlayerHandler.getOfflinePlayer(playerName);
|
|
||||||
|
|
||||||
plugin.getLogger().info("StatManager 51: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
if (player == null) throw new NullPointerException("No player called " + playerName + " was found!");
|
|
||||||
|
|
||||||
Statistic stat = getStatistic(statName);
|
|
||||||
plugin.getLogger().info("StatManager 56: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
|
|
||||||
if (stat != null) {
|
|
||||||
switch (stat.getType()) {
|
|
||||||
case UNTYPED -> {
|
|
||||||
plugin.getLogger().info("StatManager 62: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
return player.getStatistic(stat);
|
|
||||||
}
|
|
||||||
case BLOCK -> {
|
|
||||||
plugin.getLogger().info("StatManager 67: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
Material block = enumHandler.getBlock(subStatEntryName);
|
|
||||||
if (block == null) throw new NullPointerException(subStatEntryName + " is not a valid block name!");
|
|
||||||
return player.getStatistic(stat, block);
|
|
||||||
}
|
|
||||||
case ENTITY -> {
|
|
||||||
plugin.getLogger().info("StatManager 74: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
EntityType entity = enumHandler.getEntityType(subStatEntryName);
|
|
||||||
if (entity == null) throw new NullPointerException(subStatEntryName + " is not a valid entity name!");
|
|
||||||
return player.getStatistic(stat, entity);
|
|
||||||
}
|
|
||||||
case ITEM -> {
|
|
||||||
plugin.getLogger().info("StatManager 81: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
Material item = enumHandler.getItem(subStatEntryName);
|
|
||||||
if (item == null) throw new NullPointerException(subStatEntryName + " is not a valid item name!");
|
|
||||||
return player.getStatistic(stat, item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new NullPointerException(statName + " is not a valid statistic name!");
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns the statistic enum constant, or null if non-existent (param: statName, not case sensitive)
|
|
||||||
private Statistic getStatistic(String statName) {
|
|
||||||
try {
|
|
||||||
return Statistic.valueOf(statName.toUpperCase());
|
|
||||||
}
|
|
||||||
catch (IllegalArgumentException | NullPointerException exception) {
|
|
||||||
plugin.logStatRelatedExceptions(exception);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//gets the type of the statistic from the string, otherwise returns null (param: statName, not case sensitive)
|
|
||||||
public Statistic.Type getStatType(String statName) {
|
|
||||||
try {
|
|
||||||
return Statistic.valueOf(statName.toUpperCase()).getType();
|
|
||||||
}
|
|
||||||
catch (IllegalArgumentException | NullPointerException exception) {
|
|
||||||
plugin.logStatRelatedExceptions(exception);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks if string is a valid statistic (param: statName, not case sensitive)
|
|
||||||
public boolean isStatistic(String statName) {
|
|
||||||
return statNames.contains(statName.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks if string is a valid substatistic dealing with entities (param: statName, not case sensitive)
|
|
||||||
public boolean isStatEntityType(String statName) {
|
|
||||||
return entityStatNames.contains(statName.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks in the most general sense if this statistic is a substatistic (param: statName, not case sensitive)
|
|
||||||
public boolean isSubStatEntry(String statName) {
|
|
||||||
return subStatEntryNames.contains(statName.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
//checks whether a subStatEntry is of the type that the statistic requires
|
|
||||||
public boolean isMatchingSubStatEntry(String statName, String subStatEntry) {
|
|
||||||
Statistic.Type type = getStatType(statName);
|
|
||||||
if (type != null && subStatEntry != null) {
|
|
||||||
switch (type) {
|
|
||||||
case ENTITY -> {
|
|
||||||
return enumHandler.isEntityType(subStatEntry);
|
|
||||||
}
|
|
||||||
case ITEM -> {
|
|
||||||
return enumHandler.isItem(subStatEntry);
|
|
||||||
}
|
|
||||||
case BLOCK -> {
|
|
||||||
return enumHandler.isBlock(subStatEntry);
|
|
||||||
}
|
|
||||||
case UNTYPED -> {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns the names of all general statistics in lowercase
|
|
||||||
public List<String> getStatNames() {
|
|
||||||
return statNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns all statistics that have type entities, in lowercase
|
|
||||||
public List<String> getEntityTypeNames() {
|
|
||||||
return entityStatNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
//returns all substatnames in lowercase
|
|
||||||
public List<String> getSubStatEntryNames() {
|
|
||||||
return subStatEntryNames;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,67 @@
|
|||||||
|
package com.gmail.artemis.the.gr8.playerstats;
|
||||||
|
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
public class StatRequest {
|
||||||
|
|
||||||
|
private final CommandSender sender;
|
||||||
|
private String statName;
|
||||||
|
private String subStatEntry;
|
||||||
|
private String playerName;
|
||||||
|
private boolean playerFlag;
|
||||||
|
private boolean topFlag;
|
||||||
|
|
||||||
|
//playerFlag and topFlag are false by default, will be set to true if "player" or "top" is in the args
|
||||||
|
public StatRequest(@NotNull CommandSender s) {
|
||||||
|
sender = s;
|
||||||
|
playerFlag = false;
|
||||||
|
topFlag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandSender getCommandSender() {
|
||||||
|
return sender;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStatName() {
|
||||||
|
return statName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatName(String statName) {
|
||||||
|
this.statName = statName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubStatEntry() {
|
||||||
|
return subStatEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubStatEntry(String subStatEntry) {
|
||||||
|
this.subStatEntry = subStatEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPlayerName() {
|
||||||
|
return playerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerName(String playerName) {
|
||||||
|
this.playerName = playerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
//the "player" arg in the statCommand is a special case, because it could either be a valid subStatEntry, or indicate that the lookup action should target a specific player
|
||||||
|
//this is why the playerFlag exists - if this is true, and playerName is null, subStatEntry will be "player"
|
||||||
|
public boolean playerFlag() {
|
||||||
|
return playerFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlayerFlag(boolean playerFlag) {
|
||||||
|
this.playerFlag = playerFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean topFlag() {
|
||||||
|
return topFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTopFlag(boolean topFlag) {
|
||||||
|
this.topFlag = topFlag;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,154 @@
|
|||||||
|
package com.gmail.artemis.the.gr8.playerstats;
|
||||||
|
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
|
||||||
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.OfflinePlayer;
|
||||||
|
import org.bukkit.Statistic;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.entity.EntityType;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class StatThread extends Thread {
|
||||||
|
|
||||||
|
private final StatRequest request;
|
||||||
|
private final EnumHandler enumHandler;
|
||||||
|
private final OutputFormatter outputFormatter;
|
||||||
|
private final Main plugin;
|
||||||
|
private String className = "StatThread";
|
||||||
|
|
||||||
|
//constructor (called on thread creation)
|
||||||
|
public StatThread(StatRequest s, EnumHandler e, OutputFormatter o, Main p) {
|
||||||
|
request = s;
|
||||||
|
|
||||||
|
enumHandler = e;
|
||||||
|
outputFormatter = o;
|
||||||
|
plugin = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
//what the thread will do once started
|
||||||
|
@Override
|
||||||
|
public void run() throws IllegalStateException, NullPointerException {
|
||||||
|
long time = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (outputFormatter == 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!");
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandSender sender = request.getCommandSender();
|
||||||
|
String playerName = request.getPlayerName();
|
||||||
|
String statName = request.getStatName();
|
||||||
|
String subStatEntry = request.getSubStatEntry();
|
||||||
|
boolean topFlag = request.topFlag();
|
||||||
|
|
||||||
|
if (playerName != null) {
|
||||||
|
try {
|
||||||
|
sender.sendMessage(
|
||||||
|
outputFormatter.formatPlayerStat(
|
||||||
|
playerName, statName, subStatEntry, getStatistic(
|
||||||
|
statName, subStatEntry, playerName)));
|
||||||
|
plugin.logTimeTaken(className, "run(): individual stat", time, 60);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
sender.sendMessage(outputFormatter.formatExceptions(e.toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (topFlag) {
|
||||||
|
try {
|
||||||
|
LinkedHashMap<String, Integer> topStats = getTopStatistics(statName, subStatEntry);
|
||||||
|
plugin.logTimeTaken(className, "run(): for each loop", time, 69);
|
||||||
|
|
||||||
|
String top = outputFormatter.formatTopStats(topStats, statName, subStatEntry);
|
||||||
|
sender.sendMessage(top);
|
||||||
|
plugin.logTimeTaken(className, "run(): format output", time, 73);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
sender.sendMessage(outputFormatter.formatExceptions(e.toString()));
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//returns the integer associated with a certain statistic for a player
|
||||||
|
private int getStatistic(String statName, String subStatEntryName, String playerName) throws IllegalArgumentException, NullPointerException {
|
||||||
|
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
|
||||||
|
if (player != null) {
|
||||||
|
Statistic stat = enumHandler.getStatEnum(statName);
|
||||||
|
if (stat != null) {
|
||||||
|
return getPlayerStat(player, stat, subStatEntryName);
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Statistic " + statName + " could not be retrieved!");
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Player object for " + playerName + " could not be retrieved!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinkedHashMap<String, Integer> getTopStatistics(String statName, String subStatEntry) {
|
||||||
|
Statistic stat = enumHandler.getStatEnum(statName);
|
||||||
|
|
||||||
|
if (stat != null) {
|
||||||
|
HashMap<String, Integer> playerStats = new HashMap<>((int) (OfflinePlayerHandler.getOfflinePlayerCount() * 1.05));
|
||||||
|
OfflinePlayerHandler.getAllOfflinePlayerNames().forEach(playerName -> {
|
||||||
|
OfflinePlayer player = OfflinePlayerHandler.getOfflinePlayer(playerName);
|
||||||
|
if (player != null)
|
||||||
|
try {
|
||||||
|
int statistic = getPlayerStat(player, stat, subStatEntry);
|
||||||
|
if (statistic > 0) {
|
||||||
|
playerStats.put(playerName, statistic);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return playerStats.entrySet().stream()
|
||||||
|
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
|
||||||
|
.limit(10).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
|
||||||
|
}
|
||||||
|
throw new NullPointerException("Statistic " + statName + " could not be retrieved!");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPlayerStat(@NotNull OfflinePlayer player, @NotNull Statistic stat, String subStatEntryName) throws IllegalArgumentException {
|
||||||
|
switch (stat.getType()) {
|
||||||
|
case UNTYPED -> {
|
||||||
|
return player.getStatistic(stat);
|
||||||
|
}
|
||||||
|
case BLOCK -> {
|
||||||
|
Material block = enumHandler.getBlock(subStatEntryName);
|
||||||
|
if (block != null) {
|
||||||
|
return player.getStatistic(stat, block);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException(subStatEntryName + " is not a valid block name!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ENTITY -> {
|
||||||
|
EntityType entity = enumHandler.getEntityType(subStatEntryName);
|
||||||
|
if (entity != null) {
|
||||||
|
return player.getStatistic(stat, entity);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException(subStatEntryName + " is not a valid entity name!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ITEM -> {
|
||||||
|
Material item = enumHandler.getItem(subStatEntryName);
|
||||||
|
if (item != null) {
|
||||||
|
return player.getStatistic(stat, item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new IllegalArgumentException(subStatEntryName + " is not a valid item name!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default ->
|
||||||
|
throw new IllegalArgumentException("This statistic does not seem to be of type:untyped/block/entity/item, I think we should panic");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats.commands;
|
package com.gmail.artemis.the.gr8.playerstats.commands;
|
||||||
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.ConfigHandler;
|
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.Main;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
|
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
@ -12,17 +14,27 @@ public class ReloadCommand implements CommandExecutor {
|
|||||||
|
|
||||||
private final ConfigHandler config;
|
private final ConfigHandler config;
|
||||||
private final OutputFormatter outputFormatter;
|
private final OutputFormatter outputFormatter;
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
public ReloadCommand(ConfigHandler c, OutputFormatter o) {
|
public ReloadCommand(ConfigHandler c, OutputFormatter o, Main p) {
|
||||||
outputFormatter = o;
|
outputFormatter = o;
|
||||||
config = c;
|
config = c;
|
||||||
|
plugin = p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||||
if (config.reloadConfig()) {
|
if (config.reloadConfig()) {
|
||||||
|
long time = System.currentTimeMillis();
|
||||||
|
|
||||||
outputFormatter.updateOutputColors();
|
outputFormatter.updateOutputColors();
|
||||||
|
time = plugin.logTimeTaken("ReloadCommand", "onCommand", time, 33);
|
||||||
|
|
||||||
|
OfflinePlayerHandler.updateOfflinePlayers();
|
||||||
|
time = plugin.logTimeTaken("ReloadCommand", "onCommand", time, 36);
|
||||||
|
|
||||||
sender.sendMessage(ChatColor.GREEN + "Config reloaded!");
|
sender.sendMessage(ChatColor.GREEN + "Config reloaded!");
|
||||||
|
plugin.logTimeTaken("ReloadCommand", "onCommand", time, 39);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats.commands;
|
package com.gmail.artemis.the.gr8.playerstats.commands;
|
||||||
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.Main;
|
import com.gmail.artemis.the.gr8.playerstats.Main;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.StatManager;
|
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.StatRequest;
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.StatThread;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
|
import com.gmail.artemis.the.gr8.playerstats.utils.OutputFormatter;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
@ -13,98 +15,81 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
|
|
||||||
public class StatCommand implements CommandExecutor {
|
public class StatCommand implements CommandExecutor {
|
||||||
|
|
||||||
private final OfflinePlayerHandler offlinePlayerHandler;
|
|
||||||
private final OutputFormatter outputFormatter;
|
private final OutputFormatter outputFormatter;
|
||||||
private final StatManager statManager;
|
private final EnumHandler enumHandler;
|
||||||
private final Main plugin;
|
private final Main plugin;
|
||||||
|
|
||||||
public StatCommand(OutputFormatter o, StatManager s, Main p) {
|
public StatCommand(OutputFormatter o, EnumHandler e, Main p) {
|
||||||
outputFormatter = o;
|
outputFormatter = o;
|
||||||
statManager = s;
|
enumHandler = e;
|
||||||
plugin = p;
|
plugin = p;
|
||||||
|
|
||||||
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
long startTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
|
//part 1: collecting all relevant information from the args
|
||||||
if (args.length >= 2) {
|
if (args.length >= 2) {
|
||||||
|
StatRequest request = new StatRequest(sender);
|
||||||
|
|
||||||
String statName = null;
|
|
||||||
String subStatEntry = null;
|
|
||||||
String playerName = null;
|
|
||||||
boolean playerFlag = false;
|
|
||||||
|
|
||||||
plugin.getLogger().info("onCommand 40: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
|
|
||||||
//all args are in lowercase
|
|
||||||
for (String arg : args) {
|
for (String arg : args) {
|
||||||
if (statManager.isStatistic(arg)) {
|
if (enumHandler.isStatistic(arg) && request.getStatName() == null) {
|
||||||
statName = (statName == null) ? arg : statName;
|
request.setStatName(arg);
|
||||||
plugin.getLogger().info("onCommand 48: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
}
|
}
|
||||||
else if (statManager.isSubStatEntry(arg)) {
|
else if (enumHandler.isSubStatEntry(arg)) {
|
||||||
if (arg.equalsIgnoreCase("player")) {
|
if (arg.equalsIgnoreCase("player")) {
|
||||||
if (!playerFlag) {
|
if (request.playerFlag()) {
|
||||||
subStatEntry = (subStatEntry == null) ? arg : subStatEntry;
|
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
|
||||||
playerFlag = true;
|
}
|
||||||
plugin.getLogger().info("onCommand 56: " + (System.currentTimeMillis() - time));
|
else {
|
||||||
time = System.currentTimeMillis();
|
request.setPlayerFlag(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
subStatEntry = (subStatEntry == null || playerFlag) ? arg : subStatEntry;
|
if (request.getSubStatEntry() == null) request.setSubStatEntry(arg);
|
||||||
plugin.getLogger().info("onCommand 62: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (arg.equalsIgnoreCase("me") && sender instanceof Player) {
|
else if (arg.equalsIgnoreCase("top")) {
|
||||||
playerName = sender.getName();
|
request.setTopFlag(true);
|
||||||
plugin.getLogger().info("onCommand 69: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
}
|
}
|
||||||
else if (offlinePlayerHandler.isOfflinePlayerName(arg)) {
|
else if (arg.equalsIgnoreCase("me") && sender instanceof Player) {
|
||||||
playerName = (playerName == null) ? arg : playerName;
|
request.setPlayerName(sender.getName());
|
||||||
plugin.getLogger().info("onCommand 74: " + (System.currentTimeMillis() - time));
|
}
|
||||||
time = System.currentTimeMillis();
|
else if (OfflinePlayerHandler.isOfflinePlayerName(arg) && request.getPlayerName() == null) {
|
||||||
|
request.setPlayerName(arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (playerName != null && statName != null) {
|
|
||||||
plugin.getLogger().info("onCommand 79: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
subStatEntry = statManager.isMatchingSubStatEntry(statName, subStatEntry) ? subStatEntry : null;
|
|
||||||
plugin.getLogger().info("onCommand 82: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
try {
|
|
||||||
plugin.getLogger().info("onCommand 85: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
|
|
||||||
int stat = statManager.getStatistic(statName, subStatEntry, playerName);
|
//part 2: sending the information to the StatThread
|
||||||
plugin.getLogger().info("onCommand 89: " + (System.currentTimeMillis() - time));
|
if (isValidStatRequest(request)) {
|
||||||
time = System.currentTimeMillis();
|
StatThread statThread = new StatThread(request, enumHandler, outputFormatter, plugin);
|
||||||
|
statThread.start();
|
||||||
String msg = outputFormatter.formatPlayerStat(playerName, statName, subStatEntry, stat);
|
|
||||||
plugin.getLogger().info("onCommand 93: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
|
|
||||||
sender.sendMessage(msg);
|
|
||||||
plugin.getLogger().info("onCommand 97: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
sender.sendMessage(e.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
plugin.logTimeTaken("StatCommand", "onCommand", time, 71);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
plugin.getLogger().info("onCommand 106: " + (System.currentTimeMillis() - time));
|
return false;
|
||||||
plugin.getLogger().info("Total time elapsed: " + (System.currentTimeMillis() - startTime));
|
}
|
||||||
return true;
|
|
||||||
|
//check whether all necessary ingredients are present to proceed with a lookup
|
||||||
|
private boolean isValidStatRequest(StatRequest request) {
|
||||||
|
if (request.getStatName() != null) {
|
||||||
|
if (request.topFlag() || request.getPlayerName() != null) {
|
||||||
|
validatePlayerFlag(request);
|
||||||
|
return enumHandler.isValidStatEntry(request.getStatName(), request.getSubStatEntry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//account for the fact that "player" could be either a subStatEntry or a flag to indicate the target for the lookup, and correct the request if necessary
|
||||||
|
private void validatePlayerFlag(StatRequest request) {
|
||||||
|
if (!enumHandler.isValidStatEntry(request.getStatName(), request.getSubStatEntry()) && request.playerFlag()) {
|
||||||
|
request.setSubStatEntry("player");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats.commands;
|
package com.gmail.artemis.the.gr8.playerstats.commands;
|
||||||
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.Main;
|
import com.gmail.artemis.the.gr8.playerstats.Main;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.StatManager;
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
import com.gmail.artemis.the.gr8.playerstats.utils.EnumHandler;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
||||||
import org.bukkit.command.Command;
|
import org.bukkit.command.Command;
|
||||||
@ -15,16 +14,12 @@ import java.util.stream.Collectors;
|
|||||||
public class TabCompleter implements org.bukkit.command.TabCompleter {
|
public class TabCompleter implements org.bukkit.command.TabCompleter {
|
||||||
|
|
||||||
private final EnumHandler enumHandler;
|
private final EnumHandler enumHandler;
|
||||||
private final OfflinePlayerHandler offlinePlayerHandler;
|
|
||||||
private final StatManager statManager;
|
|
||||||
private final Main plugin;
|
private final Main plugin;
|
||||||
private final List<String> commandOptions;
|
private final List<String> commandOptions;
|
||||||
|
|
||||||
|
|
||||||
public TabCompleter(EnumHandler e, StatManager s, Main p) {
|
public TabCompleter(EnumHandler e, Main p) {
|
||||||
enumHandler = e;
|
enumHandler = e;
|
||||||
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
|
|
||||||
statManager = s;
|
|
||||||
plugin = p;
|
plugin = p;
|
||||||
|
|
||||||
commandOptions = new ArrayList<>();
|
commandOptions = new ArrayList<>();
|
||||||
@ -45,14 +40,14 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
|
|||||||
//after typing "stat", suggest a list of viable statistics
|
//after typing "stat", suggest a list of viable statistics
|
||||||
if (args.length >= 1) {
|
if (args.length >= 1) {
|
||||||
if (args.length == 1) {
|
if (args.length == 1) {
|
||||||
tabSuggestions = statManager.getStatNames().stream().filter(stat ->
|
tabSuggestions = enumHandler.getStatNames().stream().filter(stat ->
|
||||||
stat.contains(args[0].toLowerCase())).collect(Collectors.toList());
|
stat.contains(args[0].toLowerCase())).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
//after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions
|
//after checking if args[0] is a viable statistic, suggest substatistic OR commandOptions
|
||||||
else {
|
else {
|
||||||
if (statManager.isStatistic(args[args.length-2])) {
|
if (enumHandler.isStatistic(args[args.length-2])) {
|
||||||
tabSuggestions = switch (statManager.getStatType(args[args.length-2])) {
|
tabSuggestions = switch (enumHandler.getStatType(args[args.length-2])) {
|
||||||
case UNTYPED -> commandOptions;
|
case UNTYPED -> commandOptions;
|
||||||
case BLOCK -> enumHandler.getBlockNames().stream().filter(block ->
|
case BLOCK -> enumHandler.getBlockNames().stream().filter(block ->
|
||||||
block.contains(args[args.length - 1])).collect(Collectors.toList());
|
block.contains(args[args.length - 1])).collect(Collectors.toList());
|
||||||
@ -61,23 +56,22 @@ public class TabCompleter implements org.bukkit.command.TabCompleter {
|
|||||||
case ENTITY -> enumHandler.getEntityTypeNames().stream().filter(entity ->
|
case ENTITY -> enumHandler.getEntityTypeNames().stream().filter(entity ->
|
||||||
entity.contains(args[args.length - 1])).collect(Collectors.toList());
|
entity.contains(args[args.length - 1])).collect(Collectors.toList());
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//if previous arg = "player", suggest playerNames
|
//if previous arg = "player", suggest playerNames
|
||||||
else if (args[args.length-2].equalsIgnoreCase("player")) {
|
else if (args[args.length-2].equalsIgnoreCase("player")) {
|
||||||
if (args.length >= 3 && statManager.getEntityTypeNames().contains(args[args.length-3].toLowerCase())) {
|
if (args.length >= 3 && enumHandler.getEntityStatNames().contains(args[args.length-3].toLowerCase())) {
|
||||||
tabSuggestions = commandOptions;
|
tabSuggestions = commandOptions;
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
tabSuggestions = offlinePlayerHandler.getAllOfflinePlayerNames().stream().filter(player ->
|
tabSuggestions = OfflinePlayerHandler.getAllOfflinePlayerNames().stream().filter(player ->
|
||||||
player.toLowerCase().contains(args[args.length-1].toLowerCase())).collect(Collectors.toList());
|
player.toLowerCase().contains(args[args.length-1].toLowerCase())).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//after a substatistic, suggest commandOptions
|
//after a substatistic, suggest commandOptions
|
||||||
else if (statManager.isSubStatEntry(args[args.length-2])) {
|
else if (enumHandler.isSubStatEntry(args[args.length-2])) {
|
||||||
tabSuggestions = commandOptions;
|
tabSuggestions = commandOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats;
|
package com.gmail.artemis.the.gr8.playerstats.filehandlers;
|
||||||
|
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.Main;
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
import org.bukkit.configuration.ConfigurationSection;
|
import org.bukkit.configuration.ConfigurationSection;
|
||||||
import org.bukkit.configuration.file.FileConfiguration;
|
import org.bukkit.configuration.file.FileConfiguration;
|
||||||
@ -19,24 +20,53 @@ public class ConfigHandler {
|
|||||||
saveDefaultConfig();
|
saveDefaultConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//returns the config setting for use-dots, or the default value "true" if no value can be retrieved
|
||||||
|
public boolean getUseDots() {
|
||||||
|
ConfigurationSection ranked = config.getConfigurationSection("ranked-list");
|
||||||
|
try {
|
||||||
|
return ranked == null || ranked.getBoolean("use-dots");
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//returns a HashMap with all the available color choices, or a ChatColor.RESET if no colors were found
|
//returns a HashMap with all the available color choices, or a ChatColor.RESET if no colors were found
|
||||||
public HashMap<String, ChatColor> getChatColors() {
|
public HashMap<String, ChatColor> getChatColors() {
|
||||||
HashMap<String, ChatColor> chatColors = new HashMap<>();
|
HashMap<String, ChatColor> chatColors = new HashMap<>();
|
||||||
|
|
||||||
ConfigurationSection individual = config.getConfigurationSection("individual-statistics");
|
ConfigurationSection individual = config.getConfigurationSection("individual-statistics");
|
||||||
chatColors.put("playerNames", getChatColor(individual, "player-names"));
|
chatColors.put("player-names", getChatColor(individual, "player-names"));
|
||||||
chatColors.put("statNames", getChatColor(individual, "stat-names"));
|
chatColors.put("stat-names", getChatColor(individual, "stat-names"));
|
||||||
chatColors.put("subStatNames", getChatColor(individual, "sub-stat-names"));
|
chatColors.put("sub-stat-names", getChatColor(individual, "sub-stat-names"));
|
||||||
chatColors.put("numbers", getChatColor(individual, "numbers"));
|
chatColors.put("stat-numbers", getChatColor(individual, "stat-numbers"));
|
||||||
|
|
||||||
ConfigurationSection ranked = config.getConfigurationSection("ranked-list");
|
ConfigurationSection ranked = config.getConfigurationSection("ranked-list");
|
||||||
chatColors.put("playerNamesRanked", getChatColor(ranked, "player-names"));
|
chatColors.put("player-names-ranked", getChatColor(ranked, "player-names"));
|
||||||
chatColors.put("statNamesRanked", getChatColor(ranked, "stat-names"));
|
chatColors.put("list-title", getChatColor(ranked, "list-title"));
|
||||||
chatColors.put("subStatNamesRanked", getChatColor(ranked, "sub-stat-names"));
|
chatColors.put("sub-stat-names-ranked", getChatColor(ranked, "sub-stat-names"));
|
||||||
chatColors.put("numbersRanked", getChatColor(ranked, "numbers"));
|
chatColors.put("stat-numbers-ranked", getChatColor(ranked, "stat-numbers"));
|
||||||
|
chatColors.put("list-numbers", getChatColor(ranked, "list-numbers"));
|
||||||
|
chatColors.put("dots", getChatColor(ranked, "dots"));
|
||||||
return chatColors;
|
return chatColors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//reload the config after changes have been made to it
|
||||||
|
public boolean reloadConfig() {
|
||||||
|
try {
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
saveDefaultConfig();
|
||||||
|
}
|
||||||
|
config = YamlConfiguration.loadConfiguration(configFile);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//returns the requested entry from the provided configuration section, null if section does not exist, and ChatColor.RESET if there is no entry
|
//returns the requested entry from the provided configuration section, null if section does not exist, and ChatColor.RESET if there is no entry
|
||||||
private ChatColor getChatColor(ConfigurationSection section, String path) {
|
private ChatColor getChatColor(ConfigurationSection section, String path) {
|
||||||
ChatColor color;
|
ChatColor color;
|
||||||
@ -56,21 +86,6 @@ public class ConfigHandler {
|
|||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
//reload the config after changes have been made to it
|
|
||||||
public boolean reloadConfig() {
|
|
||||||
try {
|
|
||||||
if (!configFile.exists()) {
|
|
||||||
saveDefaultConfig();
|
|
||||||
}
|
|
||||||
config = YamlConfiguration.loadConfiguration(configFile);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//create a config file if none exists yet (from the config.yml in the plugin's resources)
|
//create a config file if none exists yet (from the config.yml in the plugin's resources)
|
||||||
private void saveDefaultConfig() {
|
private void saveDefaultConfig() {
|
||||||
config = plugin.getConfig();
|
config = plugin.getConfig();
|
@ -1,21 +1,20 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats.listeners;
|
package com.gmail.artemis.the.gr8.playerstats.listeners;
|
||||||
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
import com.gmail.artemis.the.gr8.playerstats.utils.OfflinePlayerHandler;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
import org.bukkit.event.Listener;
|
import org.bukkit.event.Listener;
|
||||||
import org.bukkit.event.player.PlayerJoinEvent;
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
|
||||||
|
|
||||||
public class JoinListener implements Listener {
|
public class JoinListener implements Listener {
|
||||||
|
|
||||||
private final OfflinePlayerHandler offlinePlayerHandler;
|
|
||||||
|
|
||||||
public JoinListener() {
|
public JoinListener() {
|
||||||
offlinePlayerHandler = OfflinePlayerHandler.getInstance();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@EventHandler
|
||||||
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
|
public void onPlayerJoin(PlayerJoinEvent joinEvent) {
|
||||||
if (!joinEvent.getPlayer().hasPlayedBefore()) {
|
if (!joinEvent.getPlayer().hasPlayedBefore()) {
|
||||||
offlinePlayerHandler.updateOfflinePlayers();
|
OfflinePlayerHandler.updateOfflinePlayers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,57 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats.utils;
|
package com.gmail.artemis.the.gr8.playerstats.utils;
|
||||||
|
|
||||||
|
import com.gmail.artemis.the.gr8.playerstats.Main;
|
||||||
import org.bukkit.Material;
|
import org.bukkit.Material;
|
||||||
|
import org.bukkit.Statistic;
|
||||||
import org.bukkit.entity.EntityType;
|
import org.bukkit.entity.EntityType;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class EnumHandler {
|
public class EnumHandler {
|
||||||
|
|
||||||
private final List<String> blockNames;
|
private final List<String> blockNames;
|
||||||
private final List<String> entityTypeNames;
|
private final List<String> entityTypeNames;
|
||||||
private final List<String> itemNames;
|
private final List<String> itemNames;
|
||||||
|
private final List<String> statNames;
|
||||||
|
private final List<String> entityStatNames;
|
||||||
|
private final List<String> subStatEntryNames;
|
||||||
|
private final Main plugin;
|
||||||
|
|
||||||
|
|
||||||
public EnumHandler() {
|
public EnumHandler(Main p) {
|
||||||
|
plugin = p;
|
||||||
|
|
||||||
blockNames = Arrays.stream(Material.values()).filter(
|
blockNames = Arrays.stream(Material.values()).filter(
|
||||||
Material::isBlock).map(Material::toString).map(String::toLowerCase).toList();
|
Material::isBlock).map(Material::toString).map(String::toLowerCase).toList();
|
||||||
entityTypeNames = Arrays.stream(EntityType.values()).map(
|
entityTypeNames = Arrays.stream(EntityType.values()).map(
|
||||||
EntityType::toString).map(String::toLowerCase).toList();
|
EntityType::toString).map(String::toLowerCase).toList();
|
||||||
itemNames = Arrays.stream(Material.values()).filter(
|
itemNames = Arrays.stream(Material.values()).filter(
|
||||||
Material::isItem).map(Material::toString).map(String::toLowerCase).toList();
|
Material::isItem).map(Material::toString).map(String::toLowerCase).toList();
|
||||||
|
statNames = Arrays.stream(Statistic.values()).map(
|
||||||
|
Statistic::toString).map(String::toLowerCase).toList();
|
||||||
|
|
||||||
|
entityStatNames = Arrays.stream(Statistic.values()).filter(statistic ->
|
||||||
|
statistic.getType().equals(Statistic.Type.ENTITY)).map(
|
||||||
|
Statistic::toString).map(String::toLowerCase).collect(Collectors.toList());
|
||||||
|
|
||||||
|
subStatEntryNames = new ArrayList<>();
|
||||||
|
subStatEntryNames.addAll(getBlockNames());
|
||||||
|
subStatEntryNames.addAll(getEntityTypeNames());
|
||||||
|
subStatEntryNames.addAll(getItemNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//checks whether the provided string is a valid item
|
||||||
public boolean isItem(String itemName) {
|
public boolean isItem(String itemName) {
|
||||||
return itemNames.contains(itemName.toLowerCase());
|
return itemNames.contains(itemName.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
//returns corresponding item enum constant (uppercase), otherwise null (param: itemName, not case sensitive)
|
//returns corresponding item enum constant (uppercase), otherwise null (param: itemName, not case sensitive)
|
||||||
|
@Nullable
|
||||||
public Material getItem(String itemName) {
|
public Material getItem(String itemName) {
|
||||||
return Material.matchMaterial(itemName);
|
return Material.matchMaterial(itemName);
|
||||||
}
|
}
|
||||||
@ -36,18 +61,25 @@ public class EnumHandler {
|
|||||||
return itemNames;
|
return itemNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//checks whether the provided string is a valid entity
|
||||||
public boolean isEntityType(String entityName) {
|
public boolean isEntityType(String entityName) {
|
||||||
return entityTypeNames.contains(entityName.toLowerCase());
|
return entityTypeNames.contains(entityName.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
//returns EntityType enum constant (uppercase) if the input name is valid, otherwise null (param: entityName, not case sensitive)
|
//returns EntityType enum constant (uppercase) if the input name is valid, otherwise null (param: entityName, not case sensitive)
|
||||||
|
@Nullable
|
||||||
public EntityType getEntityType(String entityName) {
|
public EntityType getEntityType(String entityName) {
|
||||||
EntityType entityType = null;
|
EntityType entityType;
|
||||||
try {
|
try {
|
||||||
entityType = EntityType.valueOf(entityName.toUpperCase());
|
entityType = EntityType.valueOf(entityName.toUpperCase());
|
||||||
}
|
}
|
||||||
catch (IllegalArgumentException | NullPointerException exception) {
|
catch (IllegalArgumentException e) {
|
||||||
exception.printStackTrace();
|
plugin.getLogger().warning("IllegalArgumentException: " + entityName + " is not a valid statistic name!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (NullPointerException e) {
|
||||||
|
plugin.getLogger().warning("NullPointerException: please provide a statistic name!");
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return entityType;
|
return entityType;
|
||||||
}
|
}
|
||||||
@ -57,11 +89,13 @@ public class EnumHandler {
|
|||||||
return entityTypeNames;
|
return entityTypeNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//checks whether the provided string is a valid block
|
||||||
public boolean isBlock(String materialName) {
|
public boolean isBlock(String materialName) {
|
||||||
return blockNames.contains(materialName.toLowerCase());
|
return blockNames.contains(materialName.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
//returns corresponding block enum constant (uppercase), otherwise null (param: materialName, not case sensitive)
|
//returns corresponding block enum constant (uppercase), otherwise null (param: materialName, not case sensitive)
|
||||||
|
@Nullable
|
||||||
public Material getBlock(String materialName) {
|
public Material getBlock(String materialName) {
|
||||||
return Material.matchMaterial(materialName);
|
return Material.matchMaterial(materialName);
|
||||||
}
|
}
|
||||||
@ -71,4 +105,85 @@ public class EnumHandler {
|
|||||||
return blockNames;
|
return blockNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//returns the statistic enum constant, or null if non-existent (param: statName, not case sensitive)
|
||||||
|
public Statistic getStatEnum(String statName) {
|
||||||
|
try {
|
||||||
|
return Statistic.valueOf(statName.toUpperCase());
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
plugin.getLogger().warning("IllegalArgumentException: " + statName + " is not a valid statistic name!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (NullPointerException e) {
|
||||||
|
plugin.getLogger().warning("NullPointerException: please provide a statistic name!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//gets the type of the statistic from the string, otherwise returns null (param: statName, not case sensitive)
|
||||||
|
public Statistic.Type getStatType(String statName) {
|
||||||
|
try {
|
||||||
|
return Statistic.valueOf(statName.toUpperCase()).getType();
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException e) {
|
||||||
|
plugin.getLogger().warning("IllegalArgumentException: " + statName + " is not a valid statistic name!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (NullPointerException e) {
|
||||||
|
plugin.getLogger().warning("NullPointerException: please provide a statistic name!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//checks if string is a valid statistic (param: statName, not case sensitive)
|
||||||
|
public boolean isStatistic(String statName) {
|
||||||
|
return statNames.contains(statName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
//returns the names of all general statistics in lowercase
|
||||||
|
public List<String> getStatNames() {
|
||||||
|
return statNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
//returns all statistics that have type entities, in lowercase
|
||||||
|
public List<String> getEntityStatNames() {
|
||||||
|
return entityStatNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
//checks if this statistic is a subStatEntry, meaning it is a block, item or entity (param: statName, not case sensitive)
|
||||||
|
public boolean isSubStatEntry(String statName) {
|
||||||
|
return subStatEntryNames.contains(statName.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
//checks if string is a valid statistic (param: statName, not case sensitive)
|
||||||
|
public boolean isValidStatEntry(String statName) {
|
||||||
|
return isValidStatEntry(statName, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
//checks whether a subStatEntry is of the type that the statistic requires
|
||||||
|
public boolean isValidStatEntry(String statName, String subStatEntry) {
|
||||||
|
Statistic stat = getStatEnum(statName);
|
||||||
|
return (stat != null && isMatchingSubStatEntry(stat, subStatEntry));
|
||||||
|
}
|
||||||
|
|
||||||
|
//returns true if subStatEntry matches the type the stat requires, or if stat is untyped and subStatEntry is null
|
||||||
|
private boolean isMatchingSubStatEntry(@NotNull Statistic stat, String subStatEntry) {
|
||||||
|
switch (stat.getType()) {
|
||||||
|
case ENTITY -> {
|
||||||
|
return subStatEntry != null && isEntityType(subStatEntry);
|
||||||
|
}
|
||||||
|
case ITEM -> {
|
||||||
|
return subStatEntry != null && isItem(subStatEntry);
|
||||||
|
}
|
||||||
|
case BLOCK -> {
|
||||||
|
return subStatEntry != null && isBlock(subStatEntry);
|
||||||
|
}
|
||||||
|
case UNTYPED -> {
|
||||||
|
return subStatEntry==null;
|
||||||
|
}
|
||||||
|
default -> {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,51 +4,60 @@ import org.bukkit.Bukkit;
|
|||||||
import org.bukkit.OfflinePlayer;
|
import org.bukkit.OfflinePlayer;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class OfflinePlayerHandler {
|
public class OfflinePlayerHandler {
|
||||||
|
|
||||||
private static OfflinePlayerHandler instance;
|
private static HashMap<String, OfflinePlayer> offlinePlayerMap;
|
||||||
private List<OfflinePlayer> offlinePlayers;
|
private static List<String> offlinePlayerNames;
|
||||||
private List<String> offlinePlayerNames;
|
private static int totalOfflinePlayers;
|
||||||
private HashMap<String, OfflinePlayer> offlinePlayerMap;
|
|
||||||
|
|
||||||
private OfflinePlayerHandler() {
|
private OfflinePlayerHandler() {
|
||||||
updateOfflinePlayers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static OfflinePlayerHandler getInstance() {
|
public static boolean isOfflinePlayerName(String playerName) {
|
||||||
if (instance == null) {
|
|
||||||
instance = new OfflinePlayerHandler();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOfflinePlayerName(String playerName) {
|
|
||||||
return offlinePlayerNames.contains(playerName);
|
return offlinePlayerNames.contains(playerName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OfflinePlayer getOfflinePlayer(String playerName) {
|
public static OfflinePlayer getOfflinePlayer(String playerName) {
|
||||||
long time = System.currentTimeMillis();
|
return offlinePlayerMap.get(playerName);
|
||||||
|
|
||||||
OfflinePlayer player = offlinePlayerMap.get(playerName);
|
|
||||||
System.out.println(("OfflinePlayerHandler 35: " + (System.currentTimeMillis() - time)));
|
|
||||||
return player;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<OfflinePlayer> getAllOfflinePlayers() {
|
public static int getOfflinePlayerCount() {
|
||||||
return offlinePlayers;
|
return totalOfflinePlayers > 0 ? totalOfflinePlayers : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> getAllOfflinePlayerNames() {
|
public static List<String> getAllOfflinePlayerNames() {
|
||||||
return offlinePlayerNames;
|
return offlinePlayerNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateOfflinePlayers() {
|
//stores a private HashMap with keys:playerName and values:OfflinePlayer, and a private list of the names for easy access
|
||||||
offlinePlayerMap = new HashMap<>();
|
public static void updateOfflinePlayers() {
|
||||||
offlinePlayers = Arrays.stream(Bukkit.getOfflinePlayers()).filter(offlinePlayer ->
|
long totalTime = System.currentTimeMillis();
|
||||||
offlinePlayer.getName() != null && offlinePlayer.hasPlayedBefore()).collect(Collectors.toList());
|
long time = System.currentTimeMillis();
|
||||||
offlinePlayerNames = offlinePlayers.stream().map(OfflinePlayer::getName).collect(Collectors.toList());
|
if (offlinePlayerMap == null) offlinePlayerMap = new HashMap<>();
|
||||||
offlinePlayers.forEach(offlinePlayer -> offlinePlayerMap.put(offlinePlayer.getName(), offlinePlayer));
|
else if (!offlinePlayerMap.isEmpty()) {
|
||||||
|
offlinePlayerMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offlinePlayerNames == null) offlinePlayerNames = new ArrayList<>();
|
||||||
|
else if (!offlinePlayerNames.isEmpty()) {
|
||||||
|
offlinePlayerNames.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Arrays.stream(Bukkit.getOfflinePlayers()).filter(offlinePlayer ->
|
||||||
|
offlinePlayer.getName() != null && offlinePlayer.hasPlayedBefore()).forEach(offlinePlayer -> {
|
||||||
|
offlinePlayerNames.add(offlinePlayer.getName());
|
||||||
|
offlinePlayerMap.put(offlinePlayer.getName(), offlinePlayer);
|
||||||
|
});
|
||||||
|
System.out.println("OfflinePlayerHandler, making the HashMap and ArrayList: " + (System.currentTimeMillis() - time));
|
||||||
|
time = System.currentTimeMillis();
|
||||||
|
|
||||||
|
totalOfflinePlayers = offlinePlayerMap.size();
|
||||||
|
System.out.println("OfflinePlayerHandler, counting the HashMap: " + (System.currentTimeMillis() - time));
|
||||||
|
time = System.currentTimeMillis();
|
||||||
|
|
||||||
|
totalOfflinePlayers = offlinePlayerNames.size();
|
||||||
|
System.out.println("OfflinePlayerHandler, counting the ArrayList: " + (System.currentTimeMillis() - time));
|
||||||
|
System.out.println("updateOfflinePlayers total time: " + (System.currentTimeMillis() - totalTime));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,72 @@
|
|||||||
package com.gmail.artemis.the.gr8.playerstats.utils;
|
package com.gmail.artemis.the.gr8.playerstats.utils;
|
||||||
|
|
||||||
import com.gmail.artemis.the.gr8.playerstats.ConfigHandler;
|
import com.gmail.artemis.the.gr8.playerstats.filehandlers.ConfigHandler;
|
||||||
import com.gmail.artemis.the.gr8.playerstats.Main;
|
|
||||||
import org.bukkit.ChatColor;
|
import org.bukkit.ChatColor;
|
||||||
|
import org.bukkit.map.MinecraftFont;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.*;
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
|
|
||||||
public class OutputFormatter {
|
public class OutputFormatter {
|
||||||
|
|
||||||
//keys for the HashMap are:
|
//keys for the HashMap are the same as the config options (so e.g. player-names/player-names-ranked)
|
||||||
//playerNames(Ranked)
|
|
||||||
//statNames(Ranked)
|
|
||||||
//subStatNames(Ranked)
|
|
||||||
//numbers(Ranked)
|
|
||||||
private final ConfigHandler config;
|
private final ConfigHandler config;
|
||||||
private HashMap<String, ChatColor> chatColors;
|
private HashMap<String, ChatColor> chatColors;
|
||||||
|
private final String pluginPrefix;
|
||||||
|
|
||||||
public OutputFormatter(ConfigHandler c) {
|
public OutputFormatter(ConfigHandler c) {
|
||||||
config = c;
|
config = c;
|
||||||
|
pluginPrefix = ChatColor.GRAY + "[" + ChatColor.GOLD + "PlayerStats" + ChatColor.GRAY + "] " + ChatColor.RESET;
|
||||||
updateOutputColors();
|
updateOutputColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String formatTopStats(LinkedHashMap<String, Integer> topStats) {
|
public String formatExceptions(String exception) {
|
||||||
return "";
|
return pluginPrefix + exception;
|
||||||
}
|
|
||||||
|
|
||||||
public String formatPlayerStat(String playerName, String statName, int stat) {
|
|
||||||
return formatPlayerStat(playerName, statName, null, stat);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String formatPlayerStat(String playerName, String statName, String subStatEntryName, int stat) {
|
public String formatPlayerStat(String playerName, String statName, String subStatEntryName, int stat) {
|
||||||
long time = System.currentTimeMillis();
|
|
||||||
System.out.println("OutputFormatter 33: " + (System.currentTimeMillis() - time));
|
|
||||||
time = System.currentTimeMillis();
|
|
||||||
|
|
||||||
String subStat = subStatEntryName != null ?
|
String subStat = subStatEntryName != null ?
|
||||||
chatColors.get("subStatNames") + " (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
|
chatColors.get("sub-stat-names") + " (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
|
||||||
|
|
||||||
System.out.println("OutputFormatter 39: " + (System.currentTimeMillis() - time));
|
return chatColors.get("player-names") + playerName + chatColors.get("stat-numbers") + ": " + stat + " " +
|
||||||
time = System.currentTimeMillis();
|
chatColors.get("stat-names") + statName.toLowerCase().replace("_", " ") + subStat;
|
||||||
|
}
|
||||||
|
|
||||||
String msg = chatColors.get("playerNames") + playerName + chatColors.get("numbers") + ": " + stat + " " +
|
public String formatTopStats(LinkedHashMap<String, Integer> topStats, String statName, String subStatEntryName) {
|
||||||
chatColors.get("statNames") + statName.toLowerCase().replace("_", " ") + subStat;
|
String subStat = subStatEntryName != null ?
|
||||||
|
chatColors.get("sub-stat-names-ranked") + " (" + subStatEntryName.toLowerCase().replace("_", " ") + ")" : "";
|
||||||
|
String topCount = chatColors.get("list-numbers") + " " + topStats.size();
|
||||||
|
String title = "\n" + pluginPrefix + chatColors.get("list-title") + "Top" + topCount + chatColors.get("list-title") + " " +
|
||||||
|
statName.toLowerCase().replace("_", " ") + subStat;
|
||||||
|
|
||||||
System.out.println("OutputFormatter 45: " + (System.currentTimeMillis() - time));
|
boolean useDots = config.getUseDots();
|
||||||
return msg;
|
int count = 0;
|
||||||
|
Set<String> playerNames = topStats.keySet();
|
||||||
|
MinecraftFont font = new MinecraftFont();
|
||||||
|
|
||||||
|
StringBuilder rankList = new StringBuilder();
|
||||||
|
for (String playerName : playerNames) {
|
||||||
|
count = count+1;
|
||||||
|
|
||||||
|
rankList.append("\n")
|
||||||
|
.append(chatColors.get("list-numbers")).append(count).append(". ")
|
||||||
|
.append(chatColors.get("player-names-ranked")).append(playerName)
|
||||||
|
.append(chatColors.get("dots"));
|
||||||
|
|
||||||
|
if (useDots) {
|
||||||
|
rankList.append(" ");
|
||||||
|
int dots = (int) Math.round((125.0 - font.getWidth(count + ". " + playerName))/2);
|
||||||
|
if (dots >= 1) {
|
||||||
|
rankList.append(".".repeat(dots));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
rankList.append(":");
|
||||||
|
}
|
||||||
|
|
||||||
|
rankList.append(" ").append(chatColors.get("stat-numbers-ranked")).append(topStats.get(playerName).toString());
|
||||||
|
}
|
||||||
|
return title + rankList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateOutputColors() {
|
public void updateOutputColors() {
|
||||||
|
@ -1,15 +1,23 @@
|
|||||||
# PlayerStats Configuration
|
# PlayerStats Configuration
|
||||||
|
|
||||||
# --- Color Options ---
|
# --- General Options ---
|
||||||
# supports: all default Minecraft colors
|
|
||||||
|
|
||||||
|
# --- Format & Color Options ---
|
||||||
individual-statistics:
|
individual-statistics:
|
||||||
player-names: gold
|
player-names: gold
|
||||||
stat-names: yellow
|
stat-names: yellow
|
||||||
sub-stat-names: yellow
|
sub-stat-names: yellow
|
||||||
numbers: white
|
stat-numbers: white
|
||||||
|
|
||||||
ranked-list:
|
ranked-list:
|
||||||
player-names: gold
|
player-names: green
|
||||||
stat-names: yellow
|
list-title: yellow
|
||||||
sub-stat-names: yellow
|
sub-stat-names: yellow
|
||||||
numbers: white
|
stat-numbers: white
|
||||||
|
list-numbers: gold
|
||||||
|
|
||||||
|
# If true, the statistics will be aligned so that they are all underneath each other
|
||||||
|
use-dots: true
|
||||||
|
dots: dark_gray
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user