diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/Main.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/Main.java index 0c43767..660bccb 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/Main.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/Main.java @@ -47,7 +47,7 @@ public class Main extends JavaPlugin { PluginCommand reloadcmd = this.getCommand("statisticreload"); if (reloadcmd != null) reloadcmd.setExecutor(new ReloadCommand(threadManager)); PluginCommand sharecmd = this.getCommand("statisticshare"); - if (sharecmd != null) sharecmd.setExecutor(new ShareCommand(shareManager)); + if (sharecmd != null) sharecmd.setExecutor(new ShareCommand(shareManager, messageWriter)); //register the listener Bukkit.getPluginManager().registerEvents(new JoinListener(threadManager), this); diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/ShareManager.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/ShareManager.java index 21f8e01..590eaef 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/ShareManager.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/ShareManager.java @@ -8,9 +8,11 @@ import net.kyori.adventure.text.TextComponent; import javax.annotation.Nullable; import java.time.Instant; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; @@ -23,18 +25,20 @@ public final class ShareManager { private static boolean isEnabled; private static int waitingTime; - private volatile AtomicInteger resultID; //always starts with value 0 - private ConcurrentHashMap statResults = null; - private ConcurrentHashMap shareTimeStamp = null; + private volatile AtomicInteger resultID; + private ConcurrentHashMap statResultQueue; + private ConcurrentHashMap shareTimeStamp; + private ArrayBlockingQueue sharedResults; private ShareManager(ConfigHandler config) { isEnabled = config.enableStatSharing(); waitingTime = config.getStatShareWaitingTime(); if (isEnabled) { - resultID = new AtomicInteger(); - statResults = new ConcurrentHashMap<>(); + resultID = new AtomicInteger(); //always starts with value 0 + statResultQueue = new ConcurrentHashMap<>(); shareTimeStamp = new ConcurrentHashMap<>(); + sharedResults = new ArrayBlockingQueue<>(500); } } @@ -55,15 +59,19 @@ public final class ShareManager { isEnabled = config.enableStatSharing(); waitingTime = config.getStatShareWaitingTime(); - //if we went from disabled to enabled, initialize the HashMaps - if (isEnabled && statResults == null) { - statResults = new ConcurrentHashMap<>(); - shareTimeStamp = new ConcurrentHashMap<>(); + + if (isEnabled) { //reset the sharedResultsQueue + sharedResults = new ArrayBlockingQueue<>(500); + if (statResultQueue == null) { //if we went from disabled to enabled, initialize the HashMaps + statResultQueue = new ConcurrentHashMap<>(); + shareTimeStamp = new ConcurrentHashMap<>(); + } } //if we went from enabled to disabled, purge the existing data - else if (!isEnabled && statResults != null) { - statResults = null; + else if (statResultQueue != null) { + statResultQueue = null; shareTimeStamp = null; + sharedResults = null; } } @@ -77,22 +85,38 @@ public final class ShareManager { int ID = getNextIDNumber(); UUID identifier = UUID.randomUUID(); - statResults.put(identifier, new StatResult(playerName, statResult, ID, identifier)); + statResultQueue.put(identifier, new StatResult(playerName, statResult, ID, identifier)); MyLogger.logMsg("Saving statResults with no. " + ID, DebugLevel.MEDIUM); return identifier; } - public @Nullable TextComponent getResultMessage(String playerName, UUID identifier) { - if (statResults.containsKey(identifier) && playerCanShare(playerName)) { + /** Takes a statResult from the internal ConcurrentHashmap, + puts the current time in the shareTimeStamp (ConcurrentHashMap), + puts the shareCode (UUID) in the sharedResults (ArrayBlockingQueue), + and returns the statResult. If no statResult was found, returns null.*/ + public @Nullable TextComponent getStatResult(String playerName, UUID identifier) { + if (statResultQueue.containsKey(identifier)) { shareTimeStamp.put(playerName, Instant.now()); - return statResults.remove(identifier).statResult(); + + if (!sharedResults.offer(identifier)) { //create a new ArrayBlockingQueue if our queue is full + ArrayBlockingQueue newQueue = new ArrayBlockingQueue<>(500); + + synchronized (this) { //put the last 50 values in the new Queue + UUID[] lastValues = sharedResults.toArray(new UUID[0]); + Arrays.stream(Arrays.copyOfRange(lastValues, 450, 499)) + .parallel().iterator() + .forEachRemaining(newQueue::offer); + sharedResults = newQueue; + } + sharedResults.offer(identifier); + } + return statResultQueue.remove(identifier).statResult(); } else { - //TODO send error-message if on time-out, and error-message if request is already shared return null; } } - public boolean playerCanShare(String playerName) { + public boolean isOnCoolDown(String playerName) { if (waitingTime == 0 || !shareTimeStamp.containsKey(playerName)) { return true; } else { @@ -101,10 +125,14 @@ public final class ShareManager { } } + public boolean requestAlreadyShared(UUID shareCode) { + return sharedResults.contains(shareCode); + } + /** If the given player already has more than x (in this case 25) StatResults saved, remove the oldest one.*/ private void removeExcessResults(String playerName) { - List alreadySavedResults = statResults.values() + List alreadySavedResults = statResultQueue.values() .parallelStream() .filter(result -> result.playerName().equalsIgnoreCase(playerName)) .toList(); @@ -114,8 +142,8 @@ public final class ShareManager { .parallelStream() .min(Comparator.comparing(StatResult::ID)) .orElseThrow().uuid(); - MyLogger.logMsg("Removing old stat no. " + statResults.get(uuid).ID() + " for player " + playerName, DebugLevel.MEDIUM); - statResults.remove(uuid); + MyLogger.logMsg("Removing old stat no. " + statResultQueue.get(uuid).ID() + " for player " + playerName, DebugLevel.MEDIUM); + statResultQueue.remove(uuid); } } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/commands/ShareCommand.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/commands/ShareCommand.java index 21441cc..8a59e0a 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/commands/ShareCommand.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/commands/ShareCommand.java @@ -2,7 +2,10 @@ package com.gmail.artemis.the.gr8.playerstats.commands; import com.gmail.artemis.the.gr8.playerstats.Main; import com.gmail.artemis.the.gr8.playerstats.ShareManager; +import com.gmail.artemis.the.gr8.playerstats.msg.MessageWriter; +import com.gmail.artemis.the.gr8.playerstats.utils.MyLogger; import net.kyori.adventure.platform.bukkit.BukkitAudiences; +import net.kyori.adventure.text.TextComponent; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; @@ -14,18 +17,39 @@ public class ShareCommand implements CommandExecutor { private static BukkitAudiences adventure; private static ShareManager shareManager; + private final MessageWriter messageWriter; - public ShareCommand(ShareManager s) { + public ShareCommand(ShareManager s, MessageWriter m) { adventure = Main.adventure(); shareManager = s; + messageWriter = m; } @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, String label, String[] args) { - adventure.all().sendMessage(shareManager.getResultMessage(sender.getName(), UUID.fromString(args[0]))); - //TODO send feedback if stat-result is null: - //can't share again yet (time-out) - //already shared this result (not in statResult list anymore) + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command cmd, @NotNull String label, String[] args) { + if (args.length == 1 && shareManager.isEnabled()) { + UUID shareCode; + try { + shareCode = UUID.fromString(args[0]); + } catch (IllegalArgumentException e) { + MyLogger.logException(e, "ShareCommand", "/statshare is being called without a valid UUID argument"); + return false; + } + if (shareManager.isOnCoolDown(sender.getName())) { + adventure.sender(sender).sendMessage(messageWriter.stillOnShareCoolDown()); + } + else if (shareManager.requestAlreadyShared(shareCode)) { + adventure.sender(sender).sendMessage(messageWriter.resultsAlreadyShared()); + } + else { + TextComponent result = shareManager.getStatResult(sender.getName(), shareCode); + if (result == null) { //at this point the only possible cause of statResult being null is the request being older than 25 player-requests ago + adventure.sender(sender).sendMessage(messageWriter.statResultsTooOld()); + } else { + adventure.all().sendMessage(result); + } + } + } return true; } } diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/ComponentFactory.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/ComponentFactory.java index 774f7cd..42daa31 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/ComponentFactory.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/ComponentFactory.java @@ -62,6 +62,10 @@ public class ComponentFactory { return text().color(PluginColor.MEDIUM_BLUE.getColor()).build(); } + public TextComponent messageAccentComponent() { + return text().color(PluginColor.LIGHT_GOLD.getColor()).build(); + } + public TextComponent.Builder playerNameBuilder(String playerName, Target selection) { return getComponentBuilder(playerName, getColorFromString(config.getPlayerNameDecoration(selection, false)), diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/MessageWriter.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/MessageWriter.java index fe42af5..98cd700 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/MessageWriter.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/msg/MessageWriter.java @@ -108,6 +108,29 @@ public class MessageWriter { "Please wait for your previous lookup to finish!")); } + public TextComponent stillOnShareCoolDown() { + return componentFactory.pluginPrefixComponent(false) + .append(space()) + .append(componentFactory.messageComponent().content("You need to wait") + .append(space()) + .append(componentFactory.messageAccentComponent().content(config.getStatShareWaitingTime() + "")) + .append(space())) + .append(text("minutes before you are able to share again!")); + } + + public TextComponent resultsAlreadyShared() { + return componentFactory.pluginPrefixComponent(false) + .append(space()) + .append(componentFactory.messageComponent().content("You already shared these results!")); + } + + public TextComponent statResultsTooOld() { + return componentFactory.pluginPrefixComponent(false) + .append(space()) + .append(componentFactory.messageComponent().content( + "It has been too long since you looked up this statistic, please repeat the original look-up if you want to share it!")); + } + public TextComponent unknownError(boolean isBukkitConsole) { return componentFactory.pluginPrefixComponent(isBukkitConsole) .append(space()) diff --git a/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java b/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java index 758f45e..f1b28f7 100644 --- a/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java +++ b/src/main/java/com/gmail/artemis/the/gr8/playerstats/statistic/StatThread.java @@ -85,7 +85,7 @@ public class StatThread extends Thread { case SERVER -> messageWriter.formatServerStat(getServerTotal(), request); }; - if (shareManager.isEnabled()) { + if (shareManager.isEnabled() && request.getCommandSender().hasPermission("playerstats.share")) { UUID shareCode = shareManager.saveStatResult(request.getCommandSender().getName(), statResult); statResult = messageWriter.addShareButton(statResult, shareCode); } diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 2d27198..38d1e59 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -31,7 +31,7 @@ permissions: default: true playerstats.share: description: allows sharing stats in chat - default: true + default: op playerstats.reload: description: allows usage of /statreload default: op \ No newline at end of file