diff --git a/pom.xml b/pom.xml
index c02058c..40282a4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -80,7 +80,7 @@
com.github.TheSilentPro
Warehouse
- f40f72cb19
+ 882b42fc75
net.wesjd
diff --git a/src/main/java/tsp/headdb/HeadDB.java b/src/main/java/tsp/headdb/HeadDB.java
index 732acff..edab91c 100644
--- a/src/main/java/tsp/headdb/HeadDB.java
+++ b/src/main/java/tsp/headdb/HeadDB.java
@@ -1,5 +1,6 @@
package tsp.headdb;
+import org.bukkit.command.PluginCommand;
import tsp.headdb.core.command.CommandCategory;
import tsp.headdb.core.command.CommandGive;
import tsp.headdb.core.command.CommandHelp;
@@ -15,6 +16,7 @@ import tsp.headdb.core.command.CommandUpdate;
import tsp.headdb.core.economy.BasicEconomyProvider;
import tsp.headdb.core.economy.VaultProvider;
import tsp.headdb.core.listener.PlayerJoinListener;
+import tsp.headdb.core.storage.Storage;
import tsp.headdb.core.task.UpdateTask;
import tsp.headdb.core.util.BuildProperties;
@@ -35,6 +37,7 @@ public class HeadDB extends SmartPlugin {
private PluginLogger logger;
private BuildProperties buildProperties;
private TranslatableLocalization localization;
+ private Storage storage;
private BasicEconomyProvider economyProvider;
private CommandManager commandManager;
@@ -49,15 +52,14 @@ public class HeadDB extends SmartPlugin {
new UpdateTask(getConfig().getLong("refresh", 86400L)).schedule(this);
instance.logger.info("Loaded " + loadLocalization() + " languages!");
+ instance.initStorage();
instance.initEconomy();
new PaneListener(this);
- new PlayerJoinListener();
+ //new PlayerJoinListener();
instance.commandManager = new CommandManager();
loadCommands();
- //noinspection ConstantConditions
- instance.getCommand("headdb").setExecutor(new CommandMain());
new Metrics(this, 9152);
ensureLatestVersion();
@@ -66,7 +68,9 @@ public class HeadDB extends SmartPlugin {
@Override
public void onDisable() {
- // todo: save storage
+ if (storage != null) {
+ storage.getPlayerStorage().suspend();
+ }
}
private void ensureLatestVersion() {
@@ -78,6 +82,13 @@ public class HeadDB extends SmartPlugin {
});
}
+ // Loaders
+
+ private void initStorage() {
+ storage = new Storage(getConfig().getInt("storage.threads"));
+ storage.getPlayerStorage().init();
+ }
+
private int loadLocalization() {
instance.localization = new TranslatableLocalization(this, "messages");
try {
@@ -108,6 +119,16 @@ public class HeadDB extends SmartPlugin {
}
private void loadCommands() {
+ PluginCommand main = getCommand("headdb");
+ if (main != null) {
+ main.setExecutor(new CommandMain());
+ main.setTabCompleter(new CommandMain());
+ } else {
+ instance.logger.error("Could not find main 'headdb' command!");
+ this.setEnabled(false);
+ return;
+ }
+
new CommandHelp().register();
new CommandCategory().register();
new CommandSearch().register();
@@ -119,6 +140,12 @@ public class HeadDB extends SmartPlugin {
new CommandInfo().register();
}
+ // Getters
+
+ public Storage getStorage() {
+ return storage;
+ }
+
public CommandManager getCommandManager() {
return commandManager;
}
diff --git a/src/main/java/tsp/headdb/core/api/HeadAPI.java b/src/main/java/tsp/headdb/core/api/HeadAPI.java
index ef20d95..37396ec 100644
--- a/src/main/java/tsp/headdb/core/api/HeadAPI.java
+++ b/src/main/java/tsp/headdb/core/api/HeadAPI.java
@@ -3,6 +3,7 @@ package tsp.headdb.core.api;
import org.bukkit.Bukkit;
import tsp.headdb.HeadDB;
+import tsp.headdb.core.storage.PlayerData;
import tsp.headdb.core.util.Utils;
import tsp.headdb.implementation.category.Category;
import tsp.headdb.implementation.head.Head;
@@ -18,6 +19,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import java.util.stream.Collectors;
/**
@@ -152,7 +154,7 @@ public final class HeadAPI {
/**
* Retrieve a {@link Set} of local heads.
- * Note that this calculates the heads on every try.
+ * Note that this calculates the heads on every call.
*
* @return {@link Set Local Heads}
*/
@@ -161,6 +163,24 @@ public final class HeadAPI {
return Arrays.stream(Bukkit.getOfflinePlayers()).map(player -> new LocalHead(player.getUniqueId(), player.getName())).collect(Collectors.toSet());
}
+ /**
+ * Retrieve a {@link Set} of favorite heads for the specified {@link UUID player id}.
+ * Note that this calculates the heads on every call.
+ *
+ * @param player The players id
+ * @return {@link Set Favorite Heads}
+ */
+ @Nonnull
+ public static List getFavoriteHeads(UUID player) {
+ List result = new ArrayList<>();
+ Optional data = HeadDB.getInstance().getStorage().getPlayerStorage().get(player);
+ data.ifPresent(playerData -> playerData.favorites()
+ .forEach(texture -> getHeadByTexture(texture)
+ .ifPresent(result::add))
+ );
+ return result;
+ }
+
/**
* Retrieve the main {@link HeadDatabase} used by the plugin.
*
diff --git a/src/main/java/tsp/headdb/core/command/CommandMain.java b/src/main/java/tsp/headdb/core/command/CommandMain.java
index 0a07d89..7494c72 100644
--- a/src/main/java/tsp/headdb/core/command/CommandMain.java
+++ b/src/main/java/tsp/headdb/core/command/CommandMain.java
@@ -80,7 +80,7 @@ public class CommandMain extends HeadDBCommand implements CommandExecutor, TabCo
}
})
.text("Query")
- .title(getLocalization().getMessage(player.getUniqueId(), "menu.main.category.page.name").orElse("Enter page"))
+ .title(StringUtils.colorize(getLocalization().getMessage(player.getUniqueId(), "menu.main.category.page.name").orElse("Enter page")))
.plugin(getInstance())
.open(player);
}
@@ -90,7 +90,20 @@ public class CommandMain extends HeadDBCommand implements CommandExecutor, TabCo
// Set meta buttons
pane.setButton(getInstance().getConfig().getInt("gui.main.meta.favorites.slot"), new Button(Utils.getItemFromConfig("gui.main.meta.favorites.item", Material.BOOK), e -> {
e.setCancelled(true);
- // TODO: favorites
+ List heads = HeadAPI.getFavoriteHeads(player.getUniqueId());
+ PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.main.favorites").orElse("Favorites"), heads.size(), "Favorites"));
+ for (Head head : heads) {
+ main.addButton(new Button(head.getItem(player.getUniqueId()), fe -> {
+ if (fe.isLeftClick()) {
+ ItemStack favoriteItem = head.getItem(player.getUniqueId());
+ if (fe.isShiftClick()) {
+ favoriteItem.setAmount(64);
+ }
+
+ player.getInventory().addItem(favoriteItem);
+ }
+ }));
+ }
}));
pane.setButton(getInstance().getConfig().getInt("gui.main.meta.search.slot"), new Button(Utils.getItemFromConfig("gui.main.meta.search.item", Material.DARK_OAK_SIGN), e -> {
@@ -140,8 +153,6 @@ public class CommandMain extends HeadDBCommand implements CommandExecutor, TabCo
}
player.getInventory().addItem(localItem);
- } else if (le.isRightClick()) {
- // todo: remove from favorites
}
}));
}
diff --git a/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java b/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java
index c27ef56..187f522 100644
--- a/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java
+++ b/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java
@@ -14,7 +14,6 @@ public final class PlayerJoinListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
- // todo: favorites
//HeadDB.getInstance().getStorage().getPlayerStorage().register(new PlayerData(event.getPlayer().getUniqueId(), ""));
}
diff --git a/src/main/java/tsp/headdb/core/storage/HeadDBThreadFactory.java b/src/main/java/tsp/headdb/core/storage/HeadDBThreadFactory.java
new file mode 100644
index 0000000..1fe7fa7
--- /dev/null
+++ b/src/main/java/tsp/headdb/core/storage/HeadDBThreadFactory.java
@@ -0,0 +1,20 @@
+package tsp.headdb.core.storage;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class HeadDBThreadFactory implements ThreadFactory {
+
+ private HeadDBThreadFactory() {}
+
+ public static final HeadDBThreadFactory FACTORY = new HeadDBThreadFactory();
+ private final AtomicInteger ID = new AtomicInteger(1);
+
+ @Override
+ public Thread newThread(@NotNull Runnable r) {
+ return new Thread(r, "headdb-thread-" + ID.getAndIncrement());
+ }
+
+}
diff --git a/src/main/java/tsp/headdb/core/storage/PlayerData.java b/src/main/java/tsp/headdb/core/storage/PlayerData.java
new file mode 100644
index 0000000..83953a5
--- /dev/null
+++ b/src/main/java/tsp/headdb/core/storage/PlayerData.java
@@ -0,0 +1,7 @@
+package tsp.headdb.core.storage;
+
+import java.io.Serializable;
+import java.util.Set;
+import java.util.UUID;
+
+public record PlayerData(UUID uniqueId, Set favorites) implements Serializable {}
\ No newline at end of file
diff --git a/src/main/java/tsp/headdb/core/storage/PlayerStorage.java b/src/main/java/tsp/headdb/core/storage/PlayerStorage.java
new file mode 100644
index 0000000..14dd0b5
--- /dev/null
+++ b/src/main/java/tsp/headdb/core/storage/PlayerStorage.java
@@ -0,0 +1,62 @@
+package tsp.headdb.core.storage;
+
+import tsp.headdb.HeadDB;
+import tsp.warehouse.storage.file.SerializableFileDataManager;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+public class PlayerStorage extends SerializableFileDataManager> {
+
+ private final Map players = new HashMap<>();
+
+ public PlayerStorage(Storage storage) {
+ super(new File("data/players.data"), storage.getExecutor());
+ }
+
+ public void set(PlayerData data) {
+ this.players.put(data.uniqueId(), data);
+ }
+
+ public Optional get(UUID uuid) {
+ return Optional.ofNullable(players.get(uuid));
+ }
+
+ public Map getPlayersMap() {
+ return Collections.unmodifiableMap(players);
+ }
+
+ public void init() {
+ load().whenComplete((data, ex) -> {
+ for (PlayerData entry : data) {
+ players.put(entry.uniqueId(), entry);
+ }
+
+ HeadDB.getInstance().getLog().debug("Loaded " + players.values().size() + " player data!");
+ });
+ }
+
+ public void backup() {
+ save(players.values()).whenComplete((success, ex) -> HeadDB.getInstance().getLog().debug("Saved " + players.values().size() + " player data!"));
+ }
+
+ public void suspend() {
+ Boolean saved = save(players.values())
+ .exceptionally(ex -> {
+ HeadDB.getInstance().getLog().error("Failed to save player data! | Stack Trace: ");
+ ex.printStackTrace();
+ return false;
+ })
+ .join();
+
+ if (Boolean.TRUE.equals(saved)) {
+ HeadDB.getInstance().getLog().debug("Saved " + players.values().size() + " player data!");
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/tsp/headdb/core/storage/Storage.java b/src/main/java/tsp/headdb/core/storage/Storage.java
new file mode 100644
index 0000000..1cb46d1
--- /dev/null
+++ b/src/main/java/tsp/headdb/core/storage/Storage.java
@@ -0,0 +1,24 @@
+package tsp.headdb.core.storage;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class Storage {
+
+ private final Executor executor;
+ private final PlayerStorage playerStorage;
+
+ public Storage(int threads) {
+ executor = Executors.newFixedThreadPool(threads, HeadDBThreadFactory.FACTORY);
+ playerStorage = new PlayerStorage(this);
+ }
+
+ public PlayerStorage getPlayerStorage() {
+ return playerStorage;
+ }
+
+ public Executor getExecutor() {
+ return executor;
+ }
+
+}
diff --git a/src/main/java/tsp/headdb/core/util/Utils.java b/src/main/java/tsp/headdb/core/util/Utils.java
index 60c17d4..626431d 100644
--- a/src/main/java/tsp/headdb/core/util/Utils.java
+++ b/src/main/java/tsp/headdb/core/util/Utils.java
@@ -15,6 +15,8 @@ import tsp.headdb.core.api.HeadAPI;
import tsp.headdb.core.api.event.HeadPurchaseEvent;
import tsp.headdb.core.economy.BasicEconomyProvider;
import tsp.headdb.core.hook.Hooks;
+import tsp.headdb.core.storage.PlayerData;
+import tsp.headdb.core.storage.PlayerStorage;
import tsp.headdb.implementation.category.Category;
import tsp.headdb.implementation.head.Head;
import tsp.smartplugin.inventory.Button;
@@ -33,6 +35,7 @@ import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
@@ -121,7 +124,7 @@ public class Utils {
purchase(player, head, amount);
} else if (e.isRightClick()) {
- // todo: favorites
+ HeadDB.getInstance().getStorage().getPlayerStorage().get(player.getUniqueId()).orElse(new PlayerData(player.getUniqueId(), new HashSet<>())).favorites().add(head.getTexture());
}
}));
}
diff --git a/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java b/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java
index 437a214..3f488ad 100644
--- a/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java
+++ b/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java
@@ -9,6 +9,7 @@ import tsp.headdb.implementation.requester.Requester;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
public class HeadDatabase {
@@ -16,14 +17,14 @@ public class HeadDatabase {
private final JavaPlugin plugin;
private final BukkitScheduler scheduler;
private final Requester requester;
- private final Map> heads;
+ private final ConcurrentHashMap> heads;
private long timestamp;
public HeadDatabase(JavaPlugin plugin, HeadProvider provider) {
this.plugin = plugin;
this.scheduler = plugin.getServer().getScheduler();
this.requester = new Requester(plugin, provider);
- this.heads = new HashMap<>();
+ this.heads = new ConcurrentHashMap<>();
}
public Map> getHeads() {
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 5d7dfd9..e784093 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,14 +1,15 @@
# How often the database should be updated in seconds.
refresh: 86400
-# If local heads should be enabled. Only starts keeping track of joined players when enabled!
+# If local heads should be enabled.
+# Local heads are heads from players that have joined your server at least once.
localHeads: true
-# If enabled categories will require a permission to be used
+# If enabled categories will require a permission to be used.
# Permission: headdb.category.
requireCategoryPermission: false
-# If enabled, the menu will close after purchasing a head (even if the purchase fails)
+# If enabled, the menu will close after purchasing a head (even if the purchase fails).
closeOnPurchase: false
# Economy Options
@@ -28,7 +29,7 @@ economy:
monsters: 100
plants: 100
-# Command Configuration. Supports PlaceholderAPI
+# Command Configuration. Supports PlaceholderAPI.
commands:
# Commands to run ONLY if the purchase is successful.
# They are run as CONSOLE after the player has receiver the head in their inventory.
@@ -83,8 +84,13 @@ gui:
# The archive is static so some heads may be missing, this will only be used when all else fails.
fallback: true
-# Shows more plugin information in /hdb info
+# Shows more plugin information. (/hdb info)
showAdvancedPluginInfo: true
+# Storage Options
+storage:
+ # Amount of threads in the executor pool used for storage.
+ threads: 2
+
# Debug Mode
debug: false
\ No newline at end of file
diff --git a/src/main/resources/messages/en.yml b/src/main/resources/messages/en.yml
index 31e3959..57ea293 100644
--- a/src/main/resources/messages/en.yml
+++ b/src/main/resources/messages/en.yml
@@ -17,7 +17,7 @@ itemTexture: "&7Texture: &6%texture%"
itemNoTexture: "&cThis item does not have a texture!"
copyTexture: "&6Click to copy texture!"
-#
+# Only shown if economy is enabled
processPayment: "&7Purchasing &6%name% &7for &6%cost%&7! Please wait..."
completePayment: "&7Received &6%name% &7for &6%cost%"
@@ -34,6 +34,8 @@ menu:
lore:
- "&7Left-Click to open"
- "&7Right-Click to open specific page"
+ favorites:
+ name: "&6Favorites"
search:
name: "&6Search"
local: