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: