diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4788b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,113 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +target/ + +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next + +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar +.flattened-pom.xml + +# Common working directory +run/ diff --git a/dependency-reduced-pom.xml b/dependency-reduced-pom.xml index f3be600..ca2bb69 100644 --- a/dependency-reduced-pom.xml +++ b/dependency-reduced-pom.xml @@ -175,6 +175,10 @@ placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ + + william278.net + https://repo.william278.net/releases + @@ -209,6 +213,12 @@ 24.0.1 compile + + net.william278.husksync + husksync-bukkit + 3.8.7+1.21.8 + provided + diff --git a/pom.xml b/pom.xml index ef3b5dc..c0e1d4b 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,11 @@ placeholderapi https://repo.extendedclip.com/content/repositories/placeholderapi/ + + + william278.net + https://repo.william278.net/releases + @@ -142,6 +147,13 @@ annotations 24.0.1 + + + net.william278.husksync + husksync-bukkit + 3.8.7+1.21.8 + provided + diff --git a/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java b/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java index 33eae8c..59e6367 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/api/StatManager.java @@ -1,6 +1,7 @@ package com.artemis.the.gr8.playerstats.api; import java.util.LinkedHashMap; +import java.util.concurrent.CompletableFuture; public interface StatManager { @@ -31,7 +32,7 @@ public interface StatManager { * @see PlayerStats * @see StatResult */ - StatResult executePlayerStatRequest(StatRequest request); + CompletableFuture> executePlayerStatRequest(StatRequest request); /** Gets a RequestGenerator that can be used to create a ServerStatRequest. * This RequestGenerator will make sure all default settings @@ -49,7 +50,7 @@ public interface StatManager { * @see PlayerStats * @see StatResult */ - StatResult executeServerStatRequest(StatRequest request); + CompletableFuture> executeServerStatRequest(StatRequest request); /** Gets a RequestGenerator that can be used to create a TopStatRequest * for a top-list of the specified size. This RequestGenerator will @@ -76,5 +77,5 @@ public interface StatManager { * @see PlayerStats * @see StatResult */ - StatResult> executeTopRequest(StatRequest> request); + CompletableFuture>> executeTopRequest(StatRequest> request); } \ No newline at end of file diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java index f155a70..55cf2d1 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/multithreading/StatThread.java @@ -55,10 +55,11 @@ public void run() throws IllegalStateException { } try { - StatResult result = StatRequestManager.execute(statRequest); - outputManager.sendToCommandSender(statRequester, result.formattedComponent()); - } - catch (ConcurrentModificationException e) { + StatRequestManager.execute(statRequest).thenAccept(result -> { + StatResult statResult = (StatResult) result; // I really shouldn't do this. + outputManager.sendToCommandSender(statRequester, statResult.formattedComponent()); + }); + } catch (ConcurrentModificationException e) { if (!statRequest.getSettings().isConsoleSender()) { outputManager.sendFeedbackMsg(statRequester, StandardMessage.UNKNOWN_ERROR); } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/BukkitProcessor.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/BukkitProcessor.java index f4a3bff..b7f2bdc 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/BukkitProcessor.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/BukkitProcessor.java @@ -16,6 +16,7 @@ import org.jetbrains.annotations.NotNull; import java.util.*; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ForkJoinPool; import java.util.stream.Collectors; @@ -36,36 +37,36 @@ public BukkitProcessor(OutputManager outputManager) { } @Override - public @NotNull StatResult processPlayerRequest(StatRequest playerStatRequest) { + public @NotNull CompletableFuture> processPlayerRequest(StatRequest playerStatRequest) { StatRequest.Settings requestSettings = playerStatRequest.getSettings(); int stat = getPlayerStat(requestSettings); FormattingFunction formattingFunction = outputManager.formatPlayerStat(requestSettings, stat); TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); String resultAsString = outputManager.textComponentToString(formattedResult); - return new StatResult<>(stat, formattedResult, resultAsString); + return CompletableFuture.completedFuture(new StatResult<>(stat, formattedResult, resultAsString)); } @Override - public @NotNull StatResult processServerRequest(StatRequest serverStatRequest) { + public @NotNull CompletableFuture> processServerRequest(StatRequest serverStatRequest) { StatRequest.Settings requestSettings = serverStatRequest.getSettings(); long stat = getServerStat(requestSettings); FormattingFunction formattingFunction = outputManager.formatServerStat(requestSettings, stat); TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); String resultAsString = outputManager.textComponentToString(formattedResult); - return new StatResult<>(stat, formattedResult, resultAsString); + return CompletableFuture.completedFuture(new StatResult<>(stat, formattedResult, resultAsString)); } @Override - public @NotNull StatResult> processTopRequest(StatRequest topStatRequest) { + public @NotNull CompletableFuture>> processTopRequest(StatRequest topStatRequest) { StatRequest.Settings requestSettings = topStatRequest.getSettings(); LinkedHashMap stats = getTopStats(requestSettings); FormattingFunction formattingFunction = outputManager.formatTopStats(requestSettings, stats); TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); String resultAsString = outputManager.textComponentToString(formattedResult); - return new StatResult<>(stats, formattedResult, resultAsString); + return CompletableFuture.completedFuture(new StatResult<>(stats, formattedResult, resultAsString)); } private int getPlayerStat(@NotNull StatRequest.Settings requestSettings) { diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/HuskSyncProcessor.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/HuskSyncProcessor.java new file mode 100644 index 0000000..768d0e6 --- /dev/null +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/HuskSyncProcessor.java @@ -0,0 +1,204 @@ +package com.artemis.the.gr8.playerstats.core.statistic; + +import com.artemis.the.gr8.playerstats.api.StatRequest; +import com.artemis.the.gr8.playerstats.api.StatResult; +import com.artemis.the.gr8.playerstats.core.config.ConfigHandler; +import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.msg.msgutils.FormattingFunction; +import com.artemis.the.gr8.playerstats.core.multithreading.ThreadManager; +import com.artemis.the.gr8.playerstats.core.sharing.ShareManager; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; +import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; +import net.kyori.adventure.text.TextComponent; +import net.william278.husksync.api.HuskSyncAPI; +import net.william278.husksync.data.Data; +import net.william278.husksync.data.DataSnapshot; +import org.bukkit.NamespacedKey; +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.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ForkJoinPool; +import java.util.stream.Collectors; + +final class HuskSyncProcessor extends RequestProcessor { + + private final OutputManager outputManager; + private final ConfigHandler config; + private final ShareManager shareManager; + private final OfflinePlayerHandler offlinePlayerHandler; + + private final HuskSyncAPI huskSyncAPI; + + public HuskSyncProcessor(OutputManager outputManager) { + this.outputManager = outputManager; + + config = ConfigHandler.getInstance(); + shareManager = ShareManager.getInstance(); + offlinePlayerHandler = OfflinePlayerHandler.getInstance(); + huskSyncAPI = HuskSyncAPI.getInstance(); + } + + @Override + public @NotNull CompletableFuture> processPlayerRequest(StatRequest playerStatRequest) { + StatRequest.Settings requestSettings = playerStatRequest.getSettings(); + + CompletableFuture> future = new CompletableFuture<>(); + + getPlayerStatHs(requestSettings).thenAccept(stat -> { + FormattingFunction formattingFunction = outputManager.formatPlayerStat(requestSettings, stat); + TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); + String resultAsString = outputManager.textComponentToString(formattedResult); + + future.complete(new StatResult<>(stat, formattedResult, resultAsString)); + }); + + return future; + } + + @Override + public @NotNull CompletableFuture> processServerRequest(StatRequest serverStatRequest) { + StatRequest.Settings requestSettings = serverStatRequest.getSettings(); + long stat = getServerStat(requestSettings); + FormattingFunction formattingFunction = outputManager.formatServerStat(requestSettings, stat); + TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); + String resultAsString = outputManager.textComponentToString(formattedResult); + + return CompletableFuture.completedFuture(new StatResult<>(stat, formattedResult, resultAsString)); + } + + @Override + public @NotNull CompletableFuture>> processTopRequest(StatRequest topStatRequest) { + StatRequest.Settings requestSettings = topStatRequest.getSettings(); + LinkedHashMap stats = getTopStats(requestSettings); + FormattingFunction formattingFunction = outputManager.formatTopStats(requestSettings, stats); + TextComponent formattedResult = processFunction(requestSettings.getCommandSender(), formattingFunction); + String resultAsString = outputManager.textComponentToString(formattedResult); + + return CompletableFuture.completedFuture(new StatResult<>(stats, formattedResult, resultAsString)); + } + + private CompletableFuture getPlayerStatHs(@NotNull StatRequest.Settings requestSettings) { + OfflinePlayer player; + if (offlinePlayerHandler.isExcludedPlayer(requestSettings.getPlayerName()) && + config.allowPlayerLookupsForExcludedPlayers()) { + player = offlinePlayerHandler.getExcludedOfflinePlayer(requestSettings.getPlayerName()); + } else { + player = offlinePlayerHandler.getIncludedOfflinePlayer(requestSettings.getPlayerName()); + } + + UUID uuid = player.getUniqueId(); + + CompletableFuture completableFuture = new CompletableFuture<>(); + + huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> { + if (optionalUser.isEmpty()) { + completableFuture.complete(-1); + return; + } + + huskSyncAPI.getCurrentData(optionalUser.get()).thenAccept(optionalSnapshot -> { + if (optionalSnapshot.isEmpty()) { + completableFuture.complete(-1); + return; + } + + DataSnapshot.Unpacked snapshot = optionalSnapshot.get(); + Optional optionalStatistics = snapshot.getStatistics(); + if (optionalStatistics.isEmpty()) { + completableFuture.complete(-1); + return; + } + + Data.Statistics statistics = optionalStatistics.get(); + completableFuture.complete(huskStatProcessor(statistics, requestSettings)); + }); + + }); + + return completableFuture; + } + + + private int huskStatProcessor(Data.Statistics huskStats, StatRequest.Settings requestSettings) { + NamespacedKey nsKey = requestSettings.getStatistic().getKey(); + String key = nsKey.getKey(); + + MyLogger.logLowLevelMsg("Requested main-stat: " + key); + MyLogger.logLowLevelMsg("Statistic type: " + requestSettings.getStatistic().getType().name()); + + return switch (requestSettings.getStatistic().getType()) { + case UNTYPED -> huskStats.getGenericStatistics().get(key); + case ENTITY -> + getStatisticsSafetyValue(huskStats.getEntityStatistics().get(key), requestSettings.getEntity().getName()); + case BLOCK -> + getStatisticsSafetyValue(huskStats.getBlockStatistics().get(key), requestSettings.getBlock().name().toLowerCase(Locale.ROOT)); + case ITEM -> + getStatisticsSafetyValue(huskStats.getItemStatistics().get(key), requestSettings.getItem().name().toLowerCase(Locale.ROOT)); + }; + } + + private int getStatisticsSafetyValue(Map statistics, String nameKey) { + return statistics.getOrDefault(nameKey, 0); + } + + private long getServerStat(StatRequest.Settings requestSettings) { + List numbers = getAllStatsAsync(requestSettings) + .values() + .parallelStream() + .toList(); + return numbers.parallelStream().mapToLong(Integer::longValue).sum(); + } + + private LinkedHashMap getTopStats(StatRequest.Settings requestSettings) { + return getAllStatsAsync(requestSettings).entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())) + .limit(requestSettings.getTopListSize()) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); + } + + private TextComponent processFunction(CommandSender sender, FormattingFunction function) { + if (outputShouldBeStored(sender)) { + int shareCode = shareManager.saveStatResult(sender.getName(), function.getResultWithSharerName(sender)); + return function.getResultWithShareButton(shareCode); + } + return function.getDefaultResult(); + } + + private boolean outputShouldBeStored(CommandSender sender) { + return !(sender instanceof ConsoleCommandSender) && + shareManager.isEnabled() && + shareManager.senderHasPermission(sender); + } + + /** + * Invokes a bunch of worker pool threads to get the statistics for all players that are stored in the + * {@link OfflinePlayerHandler}). + */ + private @NotNull ConcurrentHashMap getAllStatsAsync(StatRequest.Settings requestSettings) { + long time = System.currentTimeMillis(); + + ForkJoinPool commonPool = ForkJoinPool.commonPool(); + ConcurrentHashMap allStats; + + try { + allStats = commonPool.invoke(ThreadManager.getStatAction(requestSettings)); + } catch (ConcurrentModificationException e) { + MyLogger.logWarning("The requestSettings 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!"); + throw new ConcurrentModificationException(e.toString()); + } + + MyLogger.actionFinished(); + ThreadManager.recordCalcTime(System.currentTimeMillis() - time); + MyLogger.logMediumLevelTask("Calculated all stats", time); + + return allStats; + } +} diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/RequestProcessor.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/RequestProcessor.java index 1bca449..b7158e8 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/RequestProcessor.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/RequestProcessor.java @@ -5,12 +5,13 @@ import org.jetbrains.annotations.NotNull; import java.util.LinkedHashMap; +import java.util.concurrent.CompletableFuture; public abstract class RequestProcessor { - abstract @NotNull StatResult processPlayerRequest(StatRequest playerStatRequest); + abstract @NotNull CompletableFuture> processPlayerRequest(StatRequest playerStatRequest); - abstract @NotNull StatResult processServerRequest(StatRequest serverStatRequest); + abstract @NotNull CompletableFuture> processServerRequest(StatRequest serverStatRequest); - abstract @NotNull StatResult> processTopRequest(StatRequest topStatRequest); + abstract @NotNull CompletableFuture>> processTopRequest(StatRequest topStatRequest); } diff --git a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/StatRequestManager.java b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/StatRequestManager.java index 195bdd7..164bb9f 100644 --- a/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/StatRequestManager.java +++ b/src/main/java/com/artemis/the/gr8/playerstats/core/statistic/StatRequestManager.java @@ -6,12 +6,15 @@ import com.artemis.the.gr8.playerstats.api.StatResult; import com.artemis.the.gr8.playerstats.core.Main; import com.artemis.the.gr8.playerstats.core.msg.OutputManager; +import com.artemis.the.gr8.playerstats.core.utils.MyLogger; import com.artemis.the.gr8.playerstats.core.utils.OfflinePlayerHandler; import com.artemis.the.gr8.playerstats.core.utils.Reloadable; +import org.bukkit.Bukkit; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import java.util.*; +import java.util.concurrent.CompletableFuture; /** * Turns user input into a {@link StatRequest} that can be @@ -35,10 +38,20 @@ public void reload() { private @NotNull RequestProcessor getProcessor() { OutputManager outputManager = OutputManager.getInstance(); + + boolean huskSyncAvailable = Bukkit.getPluginManager().getPlugin("HuskSync") != null; + + if (huskSyncAvailable) { + MyLogger.logLowLevelMsg("HuskSync detected, using HuskSync for player data retrieval."); + return new HuskSyncProcessor(outputManager); + } + + MyLogger.logLowLevelMsg("Using Bukkit API for player data retrieval."); return new BukkitProcessor(outputManager); } - public static StatResult execute(@NotNull StatRequest request) { + // TODO: This is not a good type declaration. Change it later. + public static @NotNull CompletableFuture execute(@NotNull StatRequest request) { return switch (request.getSettings().getTarget()) { case PLAYER -> processor.processPlayerRequest(request); case SERVER -> processor.processServerRequest(request); @@ -58,7 +71,7 @@ public boolean isExcludedPlayer(String playerName) { } @Override - public @NotNull StatResult executePlayerStatRequest(@NotNull StatRequest request) { + public @NotNull CompletableFuture> executePlayerStatRequest(@NotNull StatRequest request) { return processor.processPlayerRequest(request); } @@ -69,7 +82,7 @@ public boolean isExcludedPlayer(String playerName) { } @Override - public @NotNull StatResult executeServerStatRequest(@NotNull StatRequest request) { + public @NotNull CompletableFuture> executeServerStatRequest(@NotNull StatRequest request) { return processor.processServerRequest(request); } @@ -86,7 +99,7 @@ public boolean isExcludedPlayer(String playerName) { } @Override - public @NotNull StatResult> executeTopRequest(@NotNull StatRequest> request) { + public @NotNull CompletableFuture>> executeTopRequest(@NotNull StatRequest> request) { return processor.processTopRequest(request); } } \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index c234311..4d6d109 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -7,6 +7,7 @@ description: adds commands to view player statistics in chat author: Artemis_the_gr8 softdepend: - PlaceholderAPI + - HuskSync commands: statistic: aliases: