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: