diff --git a/.gitignore b/.gitignore index 438e9c1..50c47b2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ target/ src/test/ dependency-reduced-pom.xml +build.properties +build/ .classpath .project diff --git a/LICENSE b/LICENSE index 4012c74..54f3fb5 100644 --- a/LICENSE +++ b/LICENSE @@ -14,7 +14,7 @@ copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE diff --git a/README.md b/README.md index e553736..5ef191f 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,4 @@ You may report issues on the [Issue Tracker](https://github.com/TheSilentPro/Hea # API All API methods can be found in the [HeadAPI](https://github.com/TheSilentPro/HeadDB/blob/master/src/main/java/tsp/headdb/api/HeadAPI.java) class.
-Alternatevly you may view the [javadocs](https://javadocs.pages.dev/headdb/4.0.0/tsp/headdb/api/HeadAPI). +Alternatively you may view the [javadocs](https://javadocs.pages.dev/headdb/4.0.0/tsp/headdb/api/HeadAPI). diff --git a/pom.xml b/pom.xml index d42eee7..40282a4 100644 --- a/pom.xml +++ b/pom.xml @@ -6,38 +6,40 @@ tsp.headdb HeadDB - 4.4.4 + 5.0.0-rc.1 jar - - TheSilentPro_HeadDB - silent - https://sonarcloud.io - src/main/java/tsp/headdb/Metrics.java - * - 84967 - - HeadDB Database with thousands of heads + + UTF-8 + 17 + 17 + TheSilentPro (Silent) + dd-MM-yyyy HH:mm:ss + ${maven.build.timestamp} + + + + scm:git:git@github.com:TheSilentPro/HeadDB.git + scm:git:git@github.com:TheSilentPro/HeadDB.git + git@github.com:TheSilentPro/HeadDB.git + + - spigot-repo https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - mojang-repo https://libraries.minecraft.net/ - jitpack.io https://jitpack.io - CodeMC https://repo.codemc.org/repository/maven-public @@ -49,40 +51,50 @@ - + org.spigotmc spigot-api - 1.19-R0.1-SNAPSHOT + 1.19.2-R0.1-SNAPSHOT provided - com.mojang authlib 1.5.21 provided - + + com.google.code.gson + gson + 2.10 + provided + + + + + com.github.TheSilentPro + SmartPlugin + 737fc7b893 + + + com.github.TheSilentPro + Warehouse + 882b42fc75 + net.wesjd anvilgui 1.5.3-SNAPSHOT - + + com.github.MilkBowl VaultAPI 1.7 provided - - - me.lokka30 - treasury-api - 1.1.0 - provided - me.clip placeholderapi @@ -92,6 +104,8 @@ + ${project.name} + src/main/resources @@ -104,10 +118,10 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.10.1 - 16 - 16 + ${maven.compiler.source} + ${maven.compiler.target} @@ -137,16 +151,26 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.3.2 - - - - - org.codehaus.mojo - sonar-maven-plugin - 3.9.1.2184 + 3.4.1 + + + javadoc-generate + package + + javadoc + + + - sonar + public + none + + + org.spigotmc + spigot-api + 1.19.2-R0.1-SNAPSHOT + + diff --git a/src/main/java/tsp/headdb/HeadDB.java b/src/main/java/tsp/headdb/HeadDB.java index 3d81696..0316dbc 100644 --- a/src/main/java/tsp/headdb/HeadDB.java +++ b/src/main/java/tsp/headdb/HeadDB.java @@ -1,126 +1,179 @@ package tsp.headdb; -import org.bukkit.Bukkit; -import org.bukkit.plugin.java.JavaPlugin; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.command.HeadDBCommand; -import tsp.headdb.economy.TreasuryProvider; -import tsp.headdb.implementation.DataSaveTask; -import tsp.headdb.implementation.DatabaseUpdateTask; -import tsp.headdb.economy.BasicEconomyProvider; -import tsp.headdb.economy.VaultProvider; -import tsp.headdb.listener.JoinListener; -import tsp.headdb.listener.MenuListener; -import tsp.headdb.listener.PagedPaneListener; -import tsp.headdb.storage.PlayerDataFile; -import tsp.headdb.util.Localization; -import tsp.headdb.util.Log; -import tsp.headdb.util.Utils; +import org.bukkit.command.PluginCommand; +import tsp.headdb.core.command.CommandCategory; +import tsp.headdb.core.command.CommandGive; +import tsp.headdb.core.command.CommandHelp; +import tsp.headdb.core.command.CommandInfo; +import tsp.headdb.core.command.CommandLanguage; +import tsp.headdb.core.command.CommandMain; +import tsp.headdb.core.command.CommandManager; +import tsp.headdb.core.command.CommandSearch; -import javax.annotation.Nullable; -import java.io.File; +import tsp.headdb.core.command.CommandSettings; +import tsp.headdb.core.command.CommandTexture; +import tsp.headdb.core.command.CommandUpdate; +import tsp.headdb.core.economy.BasicEconomyProvider; +import tsp.headdb.core.economy.VaultProvider; +import tsp.headdb.core.storage.Storage; +import tsp.headdb.core.task.UpdateTask; -/** - * Main class of HeadDB - */ -public class HeadDB extends JavaPlugin { +import tsp.headdb.core.util.BuildProperties; +import tsp.smartplugin.SmartPlugin; +import tsp.smartplugin.inventory.PaneListener; +import tsp.smartplugin.localization.TranslatableLocalization; +import tsp.smartplugin.logger.PluginLogger; +import tsp.smartplugin.utils.PluginUtils; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.text.DecimalFormat; +import java.util.Optional; + +public class HeadDB extends SmartPlugin { private static HeadDB instance; + private PluginLogger logger; + private BuildProperties buildProperties; + private TranslatableLocalization localization; + private Storage storage; private BasicEconomyProvider economyProvider; - private PlayerDataFile playerData; - private Localization localization; + private CommandManager commandManager; @Override - public void onEnable() { + public void onStart() { instance = this; - Log.info("Loading HeadDB - " + getDescription().getVersion()); - saveDefaultConfig(); - createLocalizationFile(); + instance.saveDefaultConfig(); + instance.logger = new PluginLogger(this, getConfig().getBoolean("debug")); + instance.logger.info("Loading HeadDB - " + instance.getDescription().getVersion()); + instance.buildProperties = new BuildProperties(this); - this.playerData = new PlayerDataFile("player_data.json"); - this.playerData.load(); + new UpdateTask(getConfig().getLong("refresh", 86400L)).schedule(this); + instance.logger.info("Loaded " + loadLocalization() + " languages!"); - if (getConfig().getBoolean("economy.enable")) { - String rawProvider = getConfig().getString("economy.provider", "VAULT"); - Log.debug("Starting economy with provider: " + rawProvider); - if (rawProvider.equalsIgnoreCase("vault")) { - economyProvider = new VaultProvider(); - economyProvider.initProvider(); - } else if (rawProvider.equalsIgnoreCase("treasury")) { - economyProvider = new TreasuryProvider(); - economyProvider.initProvider(); - } - } + instance.initStorage(); + instance.initEconomy(); - long refresh = getConfig().getLong("refresh") * 20; - HeadAPI.getDatabase().setRefresh(refresh); - Bukkit.getScheduler().runTaskTimerAsynchronously(this, new DatabaseUpdateTask(), 0, refresh); - Bukkit.getScheduler().runTaskTimerAsynchronously(this, new DataSaveTask(), refresh, refresh); + new PaneListener(this); + //new PlayerJoinListener(); - new JoinListener(this); - new MenuListener(this); - new PagedPaneListener(this); + instance.commandManager = new CommandManager(); + loadCommands(); - getCommand("headdb").setExecutor(new HeadDBCommand()); - - Log.debug("Starting metrics..."); - initMetrics(); - - Utils.isLatestVersion(this, 84967, latest -> { - if (!Boolean.TRUE.equals(latest)) { - Log.warning("There is a new update available for HeadDB on spigot!"); - Log.warning("Download: https://www.spigotmc.org/resources/84967"); - } - }); - - Log.info("Done!"); + new Metrics(this, 9152); + ensureLatestVersion(); + instance.logger.info("Done!"); } @Override public void onDisable() { - Bukkit.getScheduler().cancelTasks(this); - this.playerData.save(); + if (storage != null) { + storage.getPlayerStorage().suspend(); + } } - public Localization getLocalization() { + private void ensureLatestVersion() { + PluginUtils.isLatestVersion(this, 84967, latest -> { + if (Boolean.FALSE.equals(latest)) { + instance.logger.warning("There is a new update available for HeadDB on spigot!"); + instance.logger.warning("Download: https://www.spigotmc.org/resources/84967"); + } + }); + } + + // Loaders + + private void initStorage() { + storage = new Storage(getConfig().getInt("storage.threads")); + storage.getPlayerStorage().init(); + } + + private int loadLocalization() { + instance.localization = new TranslatableLocalization(this, "messages"); + try { + instance.localization.createDefaults(); + return instance.localization.load(); + } catch (URISyntaxException | IOException ex) { + instance.logger.error("Failed to load localization!"); + ex.printStackTrace(); + this.setEnabled(false); + return 0; + } + } + + private void initEconomy() { + if (!getConfig().getBoolean("economy.enabled")) { + instance.logger.debug("Economy disabled by config.yml!"); + instance.economyProvider = null; + return; + } + + String raw = getConfig().getString("economy.provider", "VAULT"); + if (raw.equalsIgnoreCase("VAULT")) { + economyProvider = new VaultProvider(); + } + + economyProvider.init(); + instance.logger.info("Economy Provider: " + raw); + } + + 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(); + new CommandGive().register(); + new CommandUpdate().register(); + new CommandTexture().register(); + new CommandLanguage().register(); + new CommandSettings().register(); + new CommandInfo().register(); + } + + // Getters + + public Storage getStorage() { + return storage; + } + + public CommandManager getCommandManager() { + return commandManager; + } + + public Optional getEconomyProvider() { + return Optional.ofNullable(economyProvider); + } + + @SuppressWarnings("ConstantConditions") + private DecimalFormat decimalFormat = new DecimalFormat(getConfig().getString("economy.format")); + + public DecimalFormat getDecimalFormat() { + return decimalFormat != null ? decimalFormat : (decimalFormat = new DecimalFormat("##.##")); + } + + public TranslatableLocalization getLocalization() { return localization; } - public PlayerDataFile getPlayerData() { - return playerData; + public BuildProperties getBuildProperties() { + return buildProperties; } - @Nullable - public BasicEconomyProvider getEconomyProvider() { - return economyProvider; + public PluginLogger getLog() { + return logger; } public static HeadDB getInstance() { return instance; } - private void createLocalizationFile() { - File messagesFile = new File(getDataFolder().getAbsolutePath() + "/messages.yml"); - if (!messagesFile.exists()) { - saveResource("messages.yml", false); - Log.debug("Localization loaded from jar file."); - } - - this.localization = new Localization(messagesFile); - this.localization.load(); - } - - private void initMetrics() { - Metrics metrics = new Metrics(this, 9152); - - metrics.addCustomChart(new Metrics.SimplePie("economy_provider", () -> { - if (this.getEconomyProvider() != null) { - return this.getConfig().getString("economy.provider"); - } - - return "None"; - })); - } - - } diff --git a/src/main/java/tsp/headdb/Metrics.java b/src/main/java/tsp/headdb/Metrics.java index 2016398..26436a4 100644 --- a/src/main/java/tsp/headdb/Metrics.java +++ b/src/main/java/tsp/headdb/Metrics.java @@ -1,17 +1,3 @@ -/* - * This Metrics class was auto-generated and can be copied into your project if you are - * not using a build tool like Gradle or Maven for dependency management. - * - * IMPORTANT: You are not allowed to modify this class, except changing the package. - * - * Unallowed modifications include but are not limited to: - * - Remove the option for users to opt-out - * - Change the frequency for data submission - * - Obfuscate the code (every obfucator should allow you to make an exception for specific files) - * - Reformat the code (if you use a linter, add an exception) - * - * Violations will result in a ban of your plugin and account from bStats. - */ package tsp.headdb; import java.io.BufferedReader; @@ -47,7 +33,8 @@ import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; -public class Metrics { +@SuppressWarnings({"all", "deprecation"}) // Class is from bstats, can't modify it. +class Metrics { private final Plugin plugin; diff --git a/src/main/java/tsp/headdb/api/HeadAPI.java b/src/main/java/tsp/headdb/api/HeadAPI.java deleted file mode 100644 index a728f00..0000000 --- a/src/main/java/tsp/headdb/api/HeadAPI.java +++ /dev/null @@ -1,259 +0,0 @@ -package tsp.headdb.api; - -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import tsp.headdb.HeadDB; -import tsp.headdb.implementation.Category; -import tsp.headdb.implementation.Head; -import tsp.headdb.implementation.HeadDatabase; -import tsp.headdb.implementation.LocalHead; -import tsp.headdb.inventory.InventoryUtils; -import tsp.headdb.storage.PlayerDataFile; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.List; -import java.util.Objects; -import java.util.UUID; - -/** - * This class provides simple methods - * for interacting with the HeadDB plugin - * - * @author TheSilentPro - */ -// TODO: Optional instead of null. -// TODO: Remove stream, use loop. -public final class HeadAPI { - - private HeadAPI() {} - - private static final String VALIDATION_PLAYER_NULL = "Player can not be null!"; - private static final String VALIDATION_CATEGORY_NULL = "Category can not be null!"; - private static final String VALIDATION_UUID_NULL = "UUID can not be null!"; - private static final String VALIDATION_VALUE_NULL = "Value can not be null!"; - - /** - * Main {@link HeadDatabase} that he HeadDB plugin uses. - */ - private static final HeadDatabase database = new HeadDatabase(HeadDB.getInstance()); - - /** - * Retrieves the main {@link HeadDatabase} - * - * @return Head Database - */ - public static HeadDatabase getDatabase() { - return database; - } - - /** - * Opens the database for a player - * - * @param player Target player - */ - public static void openDatabase(@Nonnull Player player) { - Validate.notNull(player, VALIDATION_PLAYER_NULL); - - InventoryUtils.openDatabase(player); - } - - /** - * Opens a specific category of the database for a player - * - * @param player Target player - * @param category Category to open - */ - public static void openCategoryDatabase(@Nonnull Player player, @Nonnull Category category) { - Validate.notNull(player, VALIDATION_PLAYER_NULL); - Validate.notNull(category, VALIDATION_CATEGORY_NULL); - - InventoryUtils.openCategoryDatabase(player, category); - } - - /** - * Opens the database with results of a specific search term - * - * @param player Target player - * @param search Search term - */ - public static void openSearchDatabase(@Nonnull Player player, @Nonnull String search) { - Validate.notNull(player, VALIDATION_PLAYER_NULL); - Validate.notNull(search, "Search can not be null!"); - - InventoryUtils.openSearchDatabase(player, search); - } - - /** - * Opens the database with results of a specific tag search term - * - * @param player Target player - * @param tag Tag search term - */ - public static void openTagSearchDatabase(@Nonnull Player player, @Nonnull String tag) { - Validate.notNull(player, VALIDATION_PLAYER_NULL); - Validate.notNull(tag, "Tag can not be null!"); - - InventoryUtils.openTagSearchDatabase(player, tag); - } - - /** - * Retrieve a {@link Head} by it's ID - * - * @param id The ID of the head - * @return The head - */ - @Nullable - public static Head getHeadByID(int id) { - return database.getHeadByID(id); - } - - /** - * Retrieve a {@link Head} by it's UUID - * - * @param uuid The UUID of the head - * @return The head - */ - @Nullable - public static Head getHeadByUniqueId(@Nonnull UUID uuid) { - Validate.notNull(uuid, VALIDATION_UUID_NULL); - - return database.getHeadByUniqueId(uuid); - } - - /** - * Retrieve a {@link List} of {@link Head}'s by their tag - * - * @param tag The tag - * @return List of heads - */ - @Nonnull - public static List getHeadsByTag(@Nonnull String tag) { - Validate.notNull(tag, "Tag can not be null!"); - - return database.getHeadsByTag(tag); - } - - /** - * Retrieves a {@link List} of {@link Head}'s matching a name - * - * @param name The name to match for - * @return List of heads - */ - @Nonnull - public static List getHeadsByName(@Nonnull String name) { - Validate.notNull(name, "Name can not be null!"); - - return database.getHeadsByName(name); - } - - /** - * Retrieves a {@link List} of {@link Head}'s in a {@link Category} matching a name - * - * @param category The category to search in - * @param name The name to match for - * @return List of heads - */ - @Nonnull - public static List getHeadsByName(@Nonnull Category category, @Nonnull String name) { - Validate.notNull(category, VALIDATION_CATEGORY_NULL); - Validate.notNull(name, "Name can not be null!"); - - return database.getHeadsByName(category, name); - } - - /** - * Retrieve a {@link Head} by it's value - * - * @param value The texture value - * @return The head - */ - @Nullable - public static Head getHeadByValue(@Nonnull String value) { - Validate.notNull(value, VALIDATION_VALUE_NULL); - - return database.getHeadByValue(value); - } - - /** - * Retrieve a {@link List} of {@link Head}'s in a specific {@link Category} - * - * @param category The category to search in - * @return List of heads - */ - @Nonnull - public static List getHeads(@Nonnull Category category) { - Validate.notNull(category, VALIDATION_CATEGORY_NULL); - - return database.getHeads(category); - } - - /** - * Retrieve a {@link List} of all {@link Head}'s - * - * @return List of all heads - */ - @Nonnull - public static List getHeads() { - return database.getHeads(); - } - - /** - * Add a favorite {@link Head} to the player - * - * @param uuid The player's unique id - * @param textureValue The head's texture value - */ - public static void addFavoriteHead(@Nonnull UUID uuid, @Nonnull String textureValue) { - Validate.notNull(uuid, VALIDATION_UUID_NULL); - Validate.notNull(textureValue, VALIDATION_VALUE_NULL); - - HeadDB.getInstance().getPlayerData().modifyFavorite(uuid, textureValue, PlayerDataFile.ModificationType.SET); - } - - /** - * Remove a favorite {@link Head} from the player - * - * @param uuid The player's unique id - * @param textureValue The head's texture value - */ - public static void removeFavoriteHead(@Nonnull UUID uuid, @Nonnull String textureValue) { - Validate.notNull(uuid, VALIDATION_UUID_NULL); - Validate.notNull(textureValue, VALIDATION_VALUE_NULL); - - HeadDB.getInstance().getPlayerData().modifyFavorite(uuid, textureValue, PlayerDataFile.ModificationType.REMOVE); - } - - /** - * Retrieve a {@link List} of favorite {@link Head}'s for the player - * - * @param uuid The player's unique id - * @return List of favorite {@link Head}'s for the player - */ - @Nonnull - public static List getFavoriteHeads(@Nonnull UUID uuid) { - Validate.notNull(uuid, VALIDATION_UUID_NULL); - - return HeadDB.getInstance().getPlayerData().getFavoriteHeadsByTexture(uuid).stream() - .map(HeadAPI::getHeadByValue) - .filter(Objects::nonNull) - .toList(); - } - - /** - * Retrieve a list of {@link LocalHead}'s. - * These are heads from players that have joined the server at least once. - * Requires config option localHeads = true - * - * @return List of {@link LocalHead}'s - */ - @Nonnull - public static List getLocalHeads() { - return HeadDB.getInstance().getPlayerData().getEntries().stream() - .map(entry -> Bukkit.getOfflinePlayer(UUID.fromString(entry))) - .map(player -> new LocalHead(player.getUniqueId()).name(player.getName())) - .toList(); - } - -} diff --git a/src/main/java/tsp/headdb/api/event/DatabaseUpdateEvent.java b/src/main/java/tsp/headdb/api/event/DatabaseUpdateEvent.java deleted file mode 100644 index f46f54f..0000000 --- a/src/main/java/tsp/headdb/api/event/DatabaseUpdateEvent.java +++ /dev/null @@ -1,54 +0,0 @@ -package tsp.headdb.api.event; - -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import tsp.headdb.implementation.Head; -import tsp.headdb.implementation.Category; -import tsp.headdb.implementation.HeadDatabase; - -import java.util.List; -import java.util.Map; - -/** - * This event is called AFTER a {@link HeadDatabase} updates. - * The event is called asynchronously and can not be cancelled. - * - * @author TheSilentPro - */ -public class DatabaseUpdateEvent extends Event { - - private final HandlerList handlerList = new HandlerList(); - private final HeadDatabase database; - private final Map> heads; - - public DatabaseUpdateEvent(HeadDatabase database, Map> heads) { - super(true); - - this.database = database; - this.heads = heads; - } - - /** - * Retrieve the {@link HeadDatabase} associated with this event - * - * @return The database - */ - public HeadDatabase getDatabase() { - return database; - } - - /** - * Retrieve the result of the update - * - * @return The heads fetched from the update - */ - public Map> getHeads() { - return heads; - } - - @Override - public HandlerList getHandlers() { - return handlerList; - } - -} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/event/PlayerHeadPurchaseEvent.java b/src/main/java/tsp/headdb/api/event/PlayerHeadPurchaseEvent.java deleted file mode 100644 index a9faf87..0000000 --- a/src/main/java/tsp/headdb/api/event/PlayerHeadPurchaseEvent.java +++ /dev/null @@ -1,69 +0,0 @@ -package tsp.headdb.api.event; - -import org.bukkit.entity.Player; -import org.bukkit.event.Cancellable; -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import tsp.headdb.implementation.Head; - -/** - * This event is called when a player purchases a {@link Head} - * - * @author TheSilentPro - * @see tsp.headdb.inventory.InventoryUtils#purchaseHead(Player, Head, int, String, String) - */ -public class PlayerHeadPurchaseEvent extends Event implements Cancellable { - - private final HandlerList handlerList = new HandlerList(); - private boolean cancelled; - private Player player; - private Head head; - private double cost; - - public PlayerHeadPurchaseEvent(Player player, Head head, double cost) { - super(true); - this.player = player; - this.head = head; - this.cost = cost; - } - - public Player getPlayer() { - return player; - } - - public Head getHead() { - return head; - } - - public double getCost() { - return cost; - } - - public void setPlayer(Player player) { - this.player = player; - } - - public void setHead(Head head) { - this.head = head; - } - - public void setCost(double cost) { - this.cost = cost; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public void setCancelled(boolean b) { - this.cancelled = b; - } - - @Override - public HandlerList getHandlers() { - return handlerList; - } - -} diff --git a/src/main/java/tsp/headdb/command/GiveCommand.java b/src/main/java/tsp/headdb/command/GiveCommand.java deleted file mode 100644 index 99cd4f8..0000000 --- a/src/main/java/tsp/headdb/command/GiveCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.implementation.Head; -import tsp.headdb.util.Utils; - -public class GiveCommand implements HeadSubCommand { - - @Override - public void handle(CommandSender sender, String[] args) { - if (!sender.hasPermission("headdb.give")) { - Utils.sendMessage(sender, getLocalization().getMessage("noPermission")); - return; - } - - if (args.length < 3) { - Utils.sendMessage(sender, "&c/hdb give &6[amount]"); - return; - } - - try { - int id = Integer.parseInt(args[1]); - Player target = Bukkit.getPlayer(args[2]); - if (target == null) { - Utils.sendMessage(sender, getLocalization().getMessage("invalidPlayer")); - return; - } - - int amount = 1; - if (args.length > 3) { - amount = Integer.parseInt(args[3]); - } - - Head head = HeadAPI.getHeadByID(id); - if (head == null) { - Utils.sendMessage(sender, "&cCould not find head with id &e" + id); - return; - } - - ItemStack item = head.getMenuItem(); - item.setAmount(amount); - target.getInventory().addItem(item); - Utils.sendMessage(sender, "&7Gave &6" + target.getName() + " &ex" + amount + " " + head.getName()); - } catch (NumberFormatException nfe) { - Utils.sendMessage(sender, "&cID/Amount must be a number!"); - } - } - -} diff --git a/src/main/java/tsp/headdb/command/HeadDBCommand.java b/src/main/java/tsp/headdb/command/HeadDBCommand.java deleted file mode 100644 index da0f779..0000000 --- a/src/main/java/tsp/headdb/command/HeadDBCommand.java +++ /dev/null @@ -1,71 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import tsp.headdb.HeadDB; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Localization; -import tsp.headdb.util.Utils; - -public class HeadDBCommand implements CommandExecutor { - - private void handle(CommandSender sender, String[] args) { - Localization localization = HeadDB.getInstance().getLocalization(); - if (args.length == 0) { - if (!sender.hasPermission("headdb.open")) { - Utils.sendMessage(sender, localization.getMessage("noPermission")); - return; - } - if (!(sender instanceof Player player)) { - Utils.sendMessage(sender, localization.getMessage("onlyPlayers")); - return; - } - - Utils.sendMessage(player, localization.getMessage("databaseOpen")); - HeadAPI.openDatabase(player); - return; - } - - String sub = args[0]; - HeadSubCommand subCommand = null; - if (sub.equalsIgnoreCase("info") || sub.equalsIgnoreCase("i")) { - subCommand = new InfoCommand(); - } else if (sub.equalsIgnoreCase("reload") || sub.equalsIgnoreCase("r")) { - subCommand = new ReloadCommand(); - } else if (sub.equalsIgnoreCase("search") || sub.equalsIgnoreCase("s")) { - subCommand = new SearchCommand(); - } else if (sub.equalsIgnoreCase("tagsearch") || sub.equalsIgnoreCase("ts")) { - subCommand = new TagSearchCommand(); - } else if (sub.equalsIgnoreCase("give") || sub.equalsIgnoreCase("g")) { - subCommand = new GiveCommand(); - } else if (sub.equalsIgnoreCase("update") || sub.equalsIgnoreCase("u")) { - subCommand = new UpdateCommand(); - } - - if (subCommand != null) { - subCommand.handle(sender, args); - return; - } - - Utils.sendMessage(sender, " "); - Utils.sendMessage(sender, "&c&lHeadDB &c- &5Commands"); - Utils.sendMessage(sender, "&7&oParameters:&c command &9(aliases)&c arguments... &7- Description"); - Utils.sendMessage(sender, "&7 > &c/hdb &7- Opens the database"); - Utils.sendMessage(sender, "&7 > &c/hdb info &9(i) &7- Plugin Information"); - Utils.sendMessage(sender, "&7 > &c/hdb reload &9(r) &7- Reloads the messages file"); - Utils.sendMessage(sender, "&7 > &c/hdb search &9(s) &c &7- Search for heads matching a name"); - Utils.sendMessage(sender, "&7 > &c/hdb tagsearch &9(ts) &c &7- Search for heads matching a tag"); - Utils.sendMessage(sender, "&7 > &c/hdb update &9(u) &7- Forcefully update the database"); - Utils.sendMessage(sender, "&7 > &c/hdb give &9(g) &c &6[amount] &7- Give player a head"); - Utils.sendMessage(sender, " "); - } - - @Override - public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { - handle(sender, args); - return true; - } - -} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/command/HeadSubCommand.java b/src/main/java/tsp/headdb/command/HeadSubCommand.java deleted file mode 100644 index 020f84f..0000000 --- a/src/main/java/tsp/headdb/command/HeadSubCommand.java +++ /dev/null @@ -1,21 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; -import tsp.headdb.util.Localization; - -/** - * An interface for a HeadDB sub-command - * - * @author TheSilentPro - * @since 4.0.0 - */ -public interface HeadSubCommand { - - void handle(CommandSender sender, String[] args); - - default Localization getLocalization() { - return HeadDB.getInstance().getLocalization(); - } - -} diff --git a/src/main/java/tsp/headdb/command/InfoCommand.java b/src/main/java/tsp/headdb/command/InfoCommand.java deleted file mode 100644 index 18a5694..0000000 --- a/src/main/java/tsp/headdb/command/InfoCommand.java +++ /dev/null @@ -1,18 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Utils; - -public class InfoCommand implements HeadSubCommand { - - @Override - public void handle(CommandSender sender, String[] args) { - // These should not be customizable - Utils.sendMessage(sender, "&7Running &cHeadDB - " + HeadDB.getInstance().getDescription().getVersion()); - Utils.sendMessage(sender, "&7Created by &c" + HeadDB.getInstance().getDescription().getAuthors()); - Utils.sendMessage(sender, "&7There are currently &c" + HeadAPI.getHeads().size() + " &7heads in the database."); - } - -} diff --git a/src/main/java/tsp/headdb/command/ReloadCommand.java b/src/main/java/tsp/headdb/command/ReloadCommand.java deleted file mode 100644 index 0665d6a..0000000 --- a/src/main/java/tsp/headdb/command/ReloadCommand.java +++ /dev/null @@ -1,20 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; -import tsp.headdb.util.Utils; - -public class ReloadCommand implements HeadSubCommand { - - @Override - public void handle(CommandSender sender, String[] args) { - if (!sender.hasPermission("headdb.reload")) { - Utils.sendMessage(sender, getLocalization().getMessage("noPermission")); - return; - } - - HeadDB.getInstance().getLocalization().load(); - Utils.sendMessage(sender, getLocalization().getMessage("reloadMessages")); - } - -} diff --git a/src/main/java/tsp/headdb/command/SearchCommand.java b/src/main/java/tsp/headdb/command/SearchCommand.java deleted file mode 100644 index 792e276..0000000 --- a/src/main/java/tsp/headdb/command/SearchCommand.java +++ /dev/null @@ -1,38 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Utils; - -public class SearchCommand implements HeadSubCommand { - - @Override - public void handle(CommandSender sender, String[] args) { - if (!sender.hasPermission("headdb.search")) { - Utils.sendMessage(sender, getLocalization().getMessage("noPermission")); - return; - } - if (!(sender instanceof Player player)) { - Utils.sendMessage(sender, getLocalization().getMessage("onlyPlayers")); - return; - } - - if (args.length < 2) { - Utils.sendMessage(player, "&c/hdb search "); - return; - } - - StringBuilder builder = new StringBuilder(); - for (int i = 1; i < args.length; i++) { - builder.append(args[i]); - if (i != args.length - 1) { - builder.append(" "); - } - } - String name = builder.toString(); - Utils.sendMessage(sender, "&7Searching for &e" + name); - HeadAPI.openSearchDatabase(player, name); - } - -} diff --git a/src/main/java/tsp/headdb/command/TagSearchCommand.java b/src/main/java/tsp/headdb/command/TagSearchCommand.java deleted file mode 100644 index bf601e2..0000000 --- a/src/main/java/tsp/headdb/command/TagSearchCommand.java +++ /dev/null @@ -1,32 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Utils; - -public class TagSearchCommand implements HeadSubCommand { - - @Override - public void handle(CommandSender sender, String[] args) { - if (!sender.hasPermission("headdb.tagsearch")) { - Utils.sendMessage(sender, getLocalization().getMessage("noPermission")); - return; - } - - if (args.length < 2) { - Utils.sendMessage(sender, "&c/hdb tagsearch "); - return; - } - - if (!(sender instanceof Player player)) { - Utils.sendMessage(sender, getLocalization().getMessage("onlyPlayers")); - return; - } - - String tag = args[1]; - Utils.sendMessage(sender, "&7Searching for heads with tag &e" + tag); - HeadAPI.openTagSearchDatabase(player, tag); - } - -} diff --git a/src/main/java/tsp/headdb/command/UpdateCommand.java b/src/main/java/tsp/headdb/command/UpdateCommand.java deleted file mode 100644 index d3765ff..0000000 --- a/src/main/java/tsp/headdb/command/UpdateCommand.java +++ /dev/null @@ -1,26 +0,0 @@ -package tsp.headdb.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Utils; - -import java.util.concurrent.TimeUnit; - -public class UpdateCommand implements HeadSubCommand { - - @Override - public void handle(CommandSender sender, String[] args) { - if (!sender.hasPermission("headdb.update")) { - Utils.sendMessage(sender, getLocalization().getMessage("noPermission")); - return; - } - - Utils.sendMessage(sender, "&7Updating..."); - long start = System.currentTimeMillis(); - HeadAPI.getDatabase().update(heads -> { - Utils.sendMessage(sender, "&7Done! Took: &a" + TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - start) + " &7seconds"); - Utils.sendMessage(sender, "&7There are &a" + HeadAPI.getHeads().size() + " &7heads in the database!"); - }); - } - -} diff --git a/src/main/java/tsp/headdb/core/api/HeadAPI.java b/src/main/java/tsp/headdb/core/api/HeadAPI.java new file mode 100644 index 0000000..37396ec --- /dev/null +++ b/src/main/java/tsp/headdb/core/api/HeadAPI.java @@ -0,0 +1,194 @@ +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; +import tsp.headdb.implementation.head.HeadDatabase; +import tsp.headdb.implementation.head.LocalHead; +import tsp.headdb.implementation.requester.HeadProvider; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +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; + +/** + * Head API for interacting with the main {@link HeadDatabase}. + * + * @author TheSilentPro (Silent) + * @see HeadDatabase + */ +public final class HeadAPI { + + /** + * Utility class. No initialization nor extension. + */ + private HeadAPI() {} + + /** + * The main {@link HeadDatabase}. + */ + private static final HeadDatabase database = new HeadDatabase(HeadDB.getInstance(), HeadProvider.HEAD_STORAGE); + + /** + * Retrieve a {@link List} of {@link Head} matching the name. + * + * @param name The name to match against + * @param lenient Whether the filter should be lenient when matching + * @return {@link List Heads} + */ + @Nonnull + public static List getHeadsByName(String name, boolean lenient) { + return getHeads().stream().filter(head -> (lenient ? Utils.matches(head.getName(), name) : head.getName().equalsIgnoreCase(name))).collect(Collectors.toList()); + } + + /** + * Retrieve a {@link List} of {@link Head} matching the name. + * + * @param name The name to match against + * @return {@link List Heads} + */ + @Nonnull + public static List getHeadsByName(String name) { + return getHeadsByName(name, true); + } + + /** + * Retrieve a {@link Head} by its exact name. + * + * @param name The name to look for + * @param lenient Whether the filter to be lenient when matching + * @return The {@link Head}, else empty + */ + public static Optional getHeadByExactName(String name, boolean lenient) { + return getHeads().stream().filter(head -> (lenient ? Utils.matches(head.getName(), name) : head.getName().equalsIgnoreCase(name))).findAny(); + } + + /** + * Retrieve a {@link Head} by its exact name. + * + * @param name The name to look for + * @return The {@link Head}, else empty + */ + @Nonnull + public static Optional getHeadByExactName(String name) { + return getHeadByExactName(name, false); + } + + /** + * Retrieve a {@link Head} by its id. + * + * @param id The id to look for + * @return The {@link Head}, else empty + */ + @Nonnull + public static Optional getHeadById(int id) { + return getHeads().stream().filter(head -> head.getId() == id).findAny(); + } + + /** + * Retrieve a {@link Head} by its texture value. + * + * @param texture The texture to look for + * @return The {@link Head}, else empty + */ + @Nonnull + public static Optional getHeadByTexture(String texture) { + return getHeads().stream().filter(head -> head.getTexture().equals(texture)).findAny(); + } + + /** + * Retrieve a {@link List} of {@link Head} within the main {@link HeadDatabase}. + * + * @return {@link List Heads} + */ + @Nonnull + public static List getHeads() { + List result = new ArrayList<>(); + for (Category category : getHeadsMap().keySet()) { + result.addAll(getHeads(category)); + } + + return result; + } + + /** + * Retrieve a {@link List} of {@link Head} within a {@link Category}. + * + * @param category The category to retrieve the heads from + * @return {@link List Heads} + */ + @Nonnull + public static List getHeads(Category category) { + return getHeadsMap().get(category); + } + + /** + * Retrieve an unmodifiable view of the database head map. + * + * @return The map + */ + @Nonnull + public static Map> getHeadsMap() { + return Collections.unmodifiableMap(database.getHeads()); + } + + /** + * Retrieve the total amount of {@link Head heads} present in the main {@link HeadDatabase}. + * + * @return Amount of heads + */ + public static int getTotalHeads() { + return getHeads().size(); + } + + /** + * Retrieve a {@link Set} of local heads. + * Note that this calculates the heads on every call. + * + * @return {@link Set Local Heads} + */ + @Nonnull + public static Set getLocalHeads() { + 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. + * + * @return {@link HeadDatabase Database} + */ + @Nonnull + public static HeadDatabase getDatabase() { + return database; + } + +} diff --git a/src/main/java/tsp/headdb/core/api/event/HeadPurchaseEvent.java b/src/main/java/tsp/headdb/core/api/event/HeadPurchaseEvent.java new file mode 100644 index 0000000..ffc01f8 --- /dev/null +++ b/src/main/java/tsp/headdb/core/api/event/HeadPurchaseEvent.java @@ -0,0 +1,80 @@ +package tsp.headdb.core.api.event; + +import org.bukkit.entity.Player; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import tsp.headdb.implementation.head.Head; + +import java.math.BigDecimal; + +/** + * Called BEFORE a head is added to the inventory but AFTER the transaction is complete. + * This gives you the chance to cancel and refund the money. + * This event is fired asynchronously! + * + * @author TheSilentPro (Silent) + * @see Event#isAsynchronous() + */ +public class HeadPurchaseEvent extends Event implements Cancellable { + + private static final HandlerList HANDLER_LIST = new HandlerList(); + private Player player; + private Head head; + private BigDecimal cost; + private final boolean success; + private boolean cancelled; + + public HeadPurchaseEvent(Player player, Head head, BigDecimal cost, boolean success) { + super(true); + this.player = player; + this.head = head; + this.cost = cost; + this.success = success; + } + + public boolean isSuccessful() { + return success; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @NotNull + @Override + public HandlerList getHandlers() { + return HANDLER_LIST; + } + + public Player getPlayer() { + return player; + } + + public void setPlayer(Player player) { + this.player = player; + } + + public Head getHead() { + return head; + } + + public void setHead(Head head) { + this.head = head; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/command/CommandCategory.java b/src/main/java/tsp/headdb/core/command/CommandCategory.java new file mode 100644 index 0000000..cefc6c6 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandCategory.java @@ -0,0 +1,61 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.HeadDB; +import tsp.headdb.core.api.HeadAPI; +import tsp.headdb.core.util.Utils; +import tsp.headdb.implementation.category.Category; +import tsp.headdb.implementation.head.Head; +import tsp.smartplugin.inventory.PagedPane; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class CommandCategory extends SubCommand { + + public CommandCategory() { + super("open", Arrays.stream(Category.VALUES).map(Category::getName).collect(Collectors.toList()), "o"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + getLocalization().sendConsoleMessage("noConsole"); + return; + } + + if (args.length < 2) { + getLocalization().sendMessage(player.getUniqueId(), "invalidArguments"); + return; + } + + Category.getByName(args[1]).ifPresentOrElse(category -> { + boolean requirePermission = HeadDB.getInstance().getConfig().getBoolean("requireCategoryPermission"); + if (requirePermission + && !player.hasPermission("headdb.category." + category.getName()) + && !player.hasPermission("headdb.category.*")) { + getLocalization().sendMessage(player.getUniqueId(), "noPermission"); + return; + } + + int page = 0; + if (args.length >= 3) { + page = Utils.resolveInt(args[2]) - 1; + } + + List heads = HeadAPI.getHeads(category); + PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.category.name").orElse(category.getName()), heads.size(), category.getName())); + Utils.addHeads(player, category, main, heads); + if (page < 0 || page > main.getPageAmount()) { + getLocalization().sendMessage(player.getUniqueId(), "invalidPageIndex", msg -> msg.replace("%pages%", String.valueOf(main.getPageAmount()))); + return; + } + + main.selectPage(page); + main.open(player); + }, () -> getLocalization().sendMessage(player.getUniqueId(), "invalidCategory")); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandGive.java b/src/main/java/tsp/headdb/core/command/CommandGive.java new file mode 100644 index 0000000..522e332 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandGive.java @@ -0,0 +1,57 @@ +package tsp.headdb.core.command; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.core.api.HeadAPI; +import tsp.headdb.core.util.Utils; +import tsp.headdb.implementation.head.Head; + +import java.util.Optional; +import java.util.stream.Collectors; + +public class CommandGive extends SubCommand { + + public CommandGive() { + super("give", HeadAPI.getHeads().stream().map(Head::getName).collect(Collectors.toList()), "g"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + // /hdb give [amount] + if (args.length < 3) { + getLocalization().sendMessage(sender, "invalidArguments"); + return; + } + + int amount = args.length >= 4 ? Utils.resolveInt(args[3]) : 1; + + Optional head; + String id = args[1]; + if (id.startsWith("t:")) { + head = HeadAPI.getHeadByTexture(id.substring(2)); + } else { + try { + head = HeadAPI.getHeadById(Integer.parseInt(id)); + } catch (NumberFormatException nfe) { + // Attempt to find it by exact name (useful for tab completions) + head = HeadAPI.getHeadByExactName(id); + } + } + + Player player = Bukkit.getPlayer(args[2]); + if (player == null) { + getLocalization().sendMessage(sender, "invalidTarget", msg -> msg.replace("%name%", args[2])); + return; + } + + head.ifPresentOrElse( + value -> { + player.getInventory().addItem(value.getItem(player.getUniqueId())); + getLocalization().sendMessage(sender, "giveCommand", msg -> msg.replace("%size%", String.valueOf(amount)).replace("%name%", value.getName()).replace("%receiver%", player.getName())); + }, + () -> getLocalization().sendMessage(sender, "giveCommandInvalid", msg -> msg.replace("%name%", id)) + ); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandHelp.java b/src/main/java/tsp/headdb/core/command/CommandHelp.java new file mode 100644 index 0000000..497282c --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandHelp.java @@ -0,0 +1,29 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import tsp.smartplugin.player.PlayerUtils; + +public class CommandHelp extends SubCommand { + + public CommandHelp() { + super("help", "h"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + PlayerUtils.sendMessage(sender, "&7<==================== [ &cHeadDB &7| &5Commands ] &7====================>"); + PlayerUtils.sendMessage(sender, "&7Format: /hdb &9(aliases) &c &7- Description"); + PlayerUtils.sendMessage(sender, "&7Required: &c<> &7| Optional: &b[]"); + PlayerUtils.sendMessage(sender, " "); + PlayerUtils.sendMessage(sender, "&7/hdb &9info(i) &7- Show plugin information."); + PlayerUtils.sendMessage(sender, "&7/hdb &9open(o) &c &b[page] &7- Open a specific category."); + PlayerUtils.sendMessage(sender, "&7/hdb &9search(s) &b(id:|tg:)&c &7- Search for specific heads."); + PlayerUtils.sendMessage(sender, "&7/hdb &9give(g) &b(t:)&c &b[amount] &7- Give the player a specific head."); + PlayerUtils.sendMessage(sender, "&7/hdb &9update(u) &7- Manually update the database."); + PlayerUtils.sendMessage(sender, "&7/hdb &9language(l) &7- Change your language."); + PlayerUtils.sendMessage(sender, "&7/hdb &9settings(st) &7- Open the settings menu."); + PlayerUtils.sendMessage(sender, "&7/hdb &9texture(t) &7- Get the texture for the head your item."); + PlayerUtils.sendMessage(sender, "&7<===============================================================>"); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandInfo.java b/src/main/java/tsp/headdb/core/command/CommandInfo.java new file mode 100644 index 0000000..69145b2 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandInfo.java @@ -0,0 +1,27 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import tsp.headdb.HeadDB; +import tsp.headdb.core.util.BuildProperties; +import tsp.smartplugin.player.PlayerUtils; + +public class CommandInfo extends SubCommand { + + public CommandInfo() { + super("info", "i"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (HeadDB.getInstance().getConfig().getBoolean("showAdvancedPluginInfo")) { + BuildProperties build = HeadDB.getInstance().getBuildProperties(); + PlayerUtils.sendMessage(sender, "&7Running &6HeadDB - " + build.getVersion()); + PlayerUtils.sendMessage(sender, "&7Created by &6" + HeadDB.getInstance().getDescription().getAuthors()); + PlayerUtils.sendMessage(sender, "&7Compiled on &6" + build.getTimestamp() + " &7by &6" + build.getAuthor()); + } else { + PlayerUtils.sendMessage(sender, "&7Running &6HeadDB &7by &6TheSilentPro (Silent)"); + PlayerUtils.sendMessage(sender, "&7GitHub: &6https://github.com/TheSilentPro/HeadDB"); + } + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandLanguage.java b/src/main/java/tsp/headdb/core/command/CommandLanguage.java new file mode 100644 index 0000000..ea53642 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandLanguage.java @@ -0,0 +1,51 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.HeadDB; + +import java.util.Set; + +public class CommandLanguage extends SubCommand { + + public CommandLanguage() { + super("language", HeadDB.getInstance().getLocalization().getData().keySet(), "l", "lang"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (args.length < 2) { + getLocalization().sendMessage(sender, "invalidArguments"); + return; + } + String lang = args[1]; + + if (!getLocalization().getData().containsKey(lang)) { + getLocalization().sendMessage(sender, "invalidLanguage", msg -> msg.replace("%languages%", toString(getLocalization().getData().keySet()))); + return; + } + + if (!(sender instanceof Player player)) { + getLocalization().setConsoleLanguage(lang); + } else { + getLocalization().setLanguage(player.getUniqueId(), lang); + } + + getLocalization().sendMessage(sender, "languageChanged", msg -> msg.replace("%language%", lang)); + } + + private String toString(Set set) { + String[] array = set.toArray(new String[0]); + + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + builder.append(array[i]); + if (i < array.length - 1) { + builder.append(","); + } + } + + return builder.toString(); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandMain.java b/src/main/java/tsp/headdb/core/command/CommandMain.java new file mode 100644 index 0000000..ef07b94 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandMain.java @@ -0,0 +1,209 @@ +package tsp.headdb.core.command; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import tsp.headdb.HeadDB; +import tsp.headdb.core.api.HeadAPI; +import tsp.headdb.core.util.Utils; +import tsp.headdb.implementation.category.Category; +import tsp.headdb.implementation.head.Head; +import tsp.headdb.implementation.head.LocalHead; +import tsp.smartplugin.inventory.Button; +import tsp.smartplugin.inventory.PagedPane; +import tsp.smartplugin.inventory.Pane; +import net.wesjd.anvilgui.AnvilGUI; +import tsp.smartplugin.utils.StringUtils; + +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class CommandMain extends HeadDBCommand implements CommandExecutor, TabCompleter { + + public CommandMain() { + super( + "headdb", + "headdb.command.open", + HeadDB.getInstance().getCommandManager().getCommandsMap().values().stream().map(HeadDBCommand::getName).collect(Collectors.toList()) + ); + } + + @Override + @ParametersAreNonnullByDefault + public void handle(CommandSender sender, String[] args) { + if (args.length == 0) { + if (!(sender instanceof Player player)) { + getLocalization().sendConsoleMessage("noConsole"); + return; + } + + if (!player.hasPermission(getPermission())) { + getLocalization().sendMessage(sender, "noPermission"); + return; + } + getLocalization().sendMessage(player.getUniqueId(), "openDatabase"); + + Pane pane = new Pane(6, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.main.title").orElse("&cHeadDB &7(" + HeadAPI.getTotalHeads() + ")"), HeadAPI.getTotalHeads(), "Main")); + // Set category buttons + for (Category category : Category.VALUES) { + pane.setButton(getInstance().getConfig().getInt("gui.main.category." + category.getName(), category.getDefaultSlot()), new Button(category.getItem(player.getUniqueId()), e -> { + e.setCancelled(true); + if (e.isLeftClick()) { + Bukkit.dispatchCommand(e.getWhoClicked(), "hdb open " + category.getName()); + } else if (e.isRightClick()) { + new AnvilGUI.Builder() + .onComplete((p, text) -> { + try { + int page = Integer.parseInt(text); + // to be replaced with own version of anvil-gui + List heads = HeadAPI.getHeads(category); + PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.category.name").orElse(category.getName()), heads.size(), category.getName())); + Utils.addHeads(player, category, main, heads); + main.selectPage(page); + main.reRender(); + return AnvilGUI.Response.openInventory(main.getInventory()); + } catch (NumberFormatException nfe) { + return AnvilGUI.Response.text("Invalid number!"); + } + }) + .text("Query") + .title(StringUtils.colorize(getLocalization().getMessage(player.getUniqueId(), "menu.main.category.page.name").orElse("Enter page"))) + .plugin(getInstance()) + .open(player); + } + })); + } + + // Set meta buttons + // favorites + 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); + List heads = HeadAPI.getFavoriteHeads(player.getUniqueId()); + PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.main.favorites.name").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); + } else if (fe.isRightClick()) { + HeadDB.getInstance().getStorage().getPlayerStorage().removeFavorite(player.getUniqueId(), head.getTexture()); + } + })); + } + + main.open(player); + })); + + // search + pane.setButton(getInstance().getConfig().getInt("gui.main.meta.search.slot"), new Button(Utils.getItemFromConfig("gui.main.meta.search.item", Material.DARK_OAK_SIGN), e -> { + e.setCancelled(true); + new AnvilGUI.Builder() + .onComplete((p, query) -> { + // Copied from CommandSearch + List heads = new ArrayList<>(); + List headList = HeadAPI.getHeads(); + if (query.length() > 3) { + if (query.startsWith("id:")) { + try { + HeadAPI.getHeadById(Integer.parseInt(query.substring(3))).ifPresent(heads::add); + } catch (NumberFormatException ignored) { + } + } else if (query.startsWith("tg:")) { + heads.addAll(headList.stream().filter(head -> Utils.matches(head.getTags(), query.substring(3))).toList()); + } else { + // no query prefix + heads.addAll(headList.stream().filter(head -> Utils.matches(head.getName(), query)).toList()); + } + } else { + // query is <=3, no point in looking for prefixes + heads.addAll(headList.stream().filter(head -> Utils.matches(head.getName(), query)).toList()); + } + + PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.search.name").orElse("&cHeadDB - &eSearch Results"), heads.size(), "None", query)); + Utils.addHeads(player, null, main, heads); + main.reRender(); + return AnvilGUI.Response.openInventory(main.getInventory()); + }) + .title(StringUtils.colorize(getLocalization().getMessage(player.getUniqueId(), "menu.main.search.name").orElse("Search"))) + .text("Query") + .plugin(getInstance()) + .open(player); + })); + + // local + pane.setButton(getInstance().getConfig().getInt("gui.main.meta.local.slot"), new Button(Utils.getItemFromConfig("gui.main.meta.local.item", Material.COMPASS), e -> { + Set localHeads = HeadAPI.getLocalHeads(); + PagedPane localPane = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.main.local.name").orElse("Local Heads"), localHeads.size(), "Local")); + for (LocalHead head : localHeads) { + localPane.addButton(new Button(head.getItem(), le -> { + if (le.isLeftClick()) { + ItemStack localItem = head.getItem(); + if (le.isShiftClick()) { + localItem.setAmount(64); + } + + player.getInventory().addItem(localItem); + } + })); + } + + localPane.open(player); + })); + + // Fill + Utils.fill(pane, Utils.getItemFromConfig("gui.main.fill", Material.BLACK_STAINED_GLASS)); + + pane.open(player); + return; + } + + getInstance().getCommandManager().getCommand(args[0]).ifPresentOrElse(command -> { + if (sender instanceof Player player && !player.hasPermission(command.getPermission())) { + getLocalization().sendMessage(player.getUniqueId(), "noPermission"); + return; + } + + command.handle(sender, args); + }, () -> getLocalization().sendMessage(sender, "invalidSubCommand")); + } + + @Override + @ParametersAreNonnullByDefault + public boolean onCommand(CommandSender sender, Command command, String s, String[] args) { + handle(sender, args); + return true; + } + + @Nullable + @Override + @ParametersAreNonnullByDefault + public List onTabComplete(CommandSender sender, Command command, String label, String[] args) { + if (args.length == 0) { + return new ArrayList<>(getCompletions()); + } else { + Optional sub = getInstance().getCommandManager().getCommand(args[0]); + if (sub.isPresent()) { + return new ArrayList<>(sub.get().getCompletions()); + } + } + + return Collections.singletonList("error"); // for debug purpose, todo: remove + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandManager.java b/src/main/java/tsp/headdb/core/command/CommandManager.java new file mode 100644 index 0000000..7d22096 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandManager.java @@ -0,0 +1,40 @@ +package tsp.headdb.core.command; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class CommandManager { + + private final Map commands = new HashMap<>(); + + public void register(SubCommand command) { + this.commands.put(command.getName(), command); + } + + public Optional getCommand(String name) { + SubCommand command = commands.get(name); + if (command != null) { + return Optional.of(command); + } + + return getCommandByAlias(name); + } + + public Optional getCommandByAlias(String alias) { + for (SubCommand entry : commands.values()) { + if (entry.getAliases().isPresent()) { + if (Arrays.stream(entry.getAliases().get()).anyMatch(name -> name.equalsIgnoreCase(alias))) { + return Optional.of(entry); + } + } + } + + return Optional.empty(); + } + + public Map getCommandsMap() { + return commands; + } +} diff --git a/src/main/java/tsp/headdb/core/command/CommandSearch.java b/src/main/java/tsp/headdb/core/command/CommandSearch.java new file mode 100644 index 0000000..f5bebd4 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandSearch.java @@ -0,0 +1,67 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.core.api.HeadAPI; +import tsp.headdb.core.util.Utils; +import tsp.headdb.implementation.head.Head; +import tsp.smartplugin.inventory.PagedPane; + +import java.util.ArrayList; +import java.util.List; + +public class CommandSearch extends SubCommand { + + public CommandSearch() { + super("search", "s"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + getLocalization().sendConsoleMessage("noConsole"); + return; + } + + if (args.length < 2) { + getLocalization().sendMessage(player, "invalidArguments"); + return; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 1; i < args.length; i++) { + builder.append(args[i]); + if (i != args.length - 1) { + builder.append(" "); + } + } + + final String query = builder.toString(); + + List heads = new ArrayList<>(); + List headList = HeadAPI.getHeads(); + if (query.length() > 3) { + if (query.startsWith("id:")) { + try { + HeadAPI.getHeadById(Integer.parseInt(query.substring(3))).ifPresent(heads::add); + } catch (NumberFormatException ignored) { + } + } else if (query.startsWith("tg:")) { + heads.addAll(headList.stream().filter(head -> Utils.matches(head.getTags(), query.substring(3))).toList()); + } else { + // no query prefix + heads.addAll(headList.stream().filter(head -> Utils.matches(head.getName(), query)).toList()); + } + } else { + // query is <=3, no point in looking for prefixes + heads.addAll(headList.stream().filter(head -> Utils.matches(head.getName(), query)).toList()); + } + + getLocalization().sendMessage(player.getUniqueId(), "searchCommand", msg -> msg.replace("%query%", query)); + PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.search.name").orElse("&cHeadDB - &eSearch Results"), heads.size(), "None", query)); + Utils.addHeads(player, null, main, heads); + getLocalization().sendMessage(player.getUniqueId(), "searchCommandResults", msg -> msg.replace("%size%", String.valueOf(heads.size())).replace("%query%", query)); + main.open(player); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/command/CommandSettings.java b/src/main/java/tsp/headdb/core/command/CommandSettings.java new file mode 100644 index 0000000..5b36dc6 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandSettings.java @@ -0,0 +1,53 @@ +package tsp.headdb.core.command; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.core.util.Utils; +import tsp.smartplugin.builder.item.ItemBuilder; +import tsp.smartplugin.inventory.Button; +import tsp.smartplugin.inventory.PagedPane; +import tsp.smartplugin.inventory.Pane; +import tsp.smartplugin.utils.StringUtils; + +import java.util.Set; + +public class CommandSettings extends SubCommand { + + public CommandSettings() { + super("settings", "st"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + getLocalization().sendConsoleMessage("noConsole"); + return; + } + + Set languages = getLocalization().getData().keySet(); + Pane pane = new Pane(1, StringUtils.colorize(getLocalization().getMessage(player.getUniqueId(), "menu.settings.name").orElse("&cHeadDB - Settings"))); + pane.addButton(new Button(new ItemBuilder(Material.BOOK) + .name(getLocalization().getMessage(player.getUniqueId(), "menu.settings.language.name").orElse("&cLanguage")) + .setLore(getLocalization().getMessage(player.getUniqueId(), "menu.settings.language.available").orElse("&7Languages Available: &e%size%").replace("%size%", String.valueOf(languages.size()))) + .build(), e -> { + e.setCancelled(true); + PagedPane langPane = new PagedPane(4, 6, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.settings.language.title").orElse("&cHeadDB &7- &eSelect Language").replace("%languages%", "%size%"), languages.size(), "Selector: Language")); + for (String lang : languages) { + langPane.addButton(new Button(new ItemBuilder(Material.PAPER) + .name(getLocalization().getMessage(player.getUniqueId(), "menu.settings.language.format").orElse(ChatColor.YELLOW + lang).replace("%language%", lang)) + .build(), langEvent -> { + e.setCancelled(true); + getLocalization().setLanguage(player.getUniqueId(), lang); + getLocalization().sendMessage(player.getUniqueId(), "languageChanged", msg -> msg.replace("%language%", lang)); + })); + } + + langPane.open(player); + })); + + pane.open(player); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandTexture.java b/src/main/java/tsp/headdb/core/command/CommandTexture.java new file mode 100644 index 0000000..22bd7a0 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandTexture.java @@ -0,0 +1,33 @@ +package tsp.headdb.core.command; + +import net.md_5.bungee.api.chat.ClickEvent; +import net.md_5.bungee.api.chat.HoverEvent; +import net.md_5.bungee.api.chat.TextComponent; +import net.md_5.bungee.api.chat.hover.content.Text; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.core.util.Utils; +import tsp.smartplugin.utils.StringUtils; + +public class CommandTexture extends SubCommand { + + public CommandTexture() { + super("texture", "t"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + getLocalization().sendConsoleMessage("noConsole"); + return; + } + + Utils.getTexture(player.getInventory().getItemInMainHand()).ifPresentOrElse(texture -> getLocalization().getMessage(player.getUniqueId(), "itemTexture").ifPresent(message -> { + TextComponent component = new TextComponent(StringUtils.colorize(message.replace("%texture%", texture))); + component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(StringUtils.colorize(getLocalization().getMessage(player.getUniqueId(), "copyTexture").orElse("Click to copy!"))))); + component.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, texture)); + player.spigot().sendMessage(component); + }), () -> getLocalization().sendMessage(sender,"itemNoTexture")); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/CommandUpdate.java b/src/main/java/tsp/headdb/core/command/CommandUpdate.java new file mode 100644 index 0000000..711e527 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/CommandUpdate.java @@ -0,0 +1,19 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import tsp.headdb.HeadDB; +import tsp.headdb.core.api.HeadAPI; + +public class CommandUpdate extends SubCommand { + + public CommandUpdate() { + super("update", "u"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + getLocalization().sendMessage(sender, "updateDatabase"); + HeadAPI.getDatabase().update((time, result) -> HeadDB.getInstance().getLog().debug("Database Updated! Heads: " + result.values().size() + " | Took: " + time + "ms")); + } + +} diff --git a/src/main/java/tsp/headdb/core/command/HeadDBCommand.java b/src/main/java/tsp/headdb/core/command/HeadDBCommand.java new file mode 100644 index 0000000..e1c3509 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/HeadDBCommand.java @@ -0,0 +1,59 @@ +package tsp.headdb.core.command; + +import org.bukkit.command.CommandSender; +import tsp.headdb.HeadDB; +import tsp.smartplugin.localization.TranslatableLocalization; +import tsp.smartplugin.utils.Validate; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.util.ArrayList; +import java.util.Collection; + +public abstract class HeadDBCommand { + + private static final TranslatableLocalization localization = HeadDB.getInstance().getLocalization(); + private final HeadDB instance = HeadDB.getInstance(); + + private final String name; + private final String permission; + private final Collection completions; + + @ParametersAreNonnullByDefault + public HeadDBCommand(String name, String permission, Collection completions) { + Validate.notNull(name, "Name can not be null!"); + Validate.notNull(permission, "Permission can not be null!"); + Validate.notNull(completions, "Completions can not be null!"); + + this.name = name; + this.permission = permission; + this.completions = completions; + } + + @ParametersAreNonnullByDefault + public HeadDBCommand(String name, String permission) { + this(name, permission, new ArrayList<>()); + } + + public abstract void handle(CommandSender sender, String[] args); + + public Collection getCompletions() { + return completions; + } + + public String getName() { + return name; + } + + public String getPermission() { + return permission; + } + + public TranslatableLocalization getLocalization() { + return localization; + } + + public HeadDB getInstance() { + return instance; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/command/SubCommand.java b/src/main/java/tsp/headdb/core/command/SubCommand.java new file mode 100644 index 0000000..f4fd5a8 --- /dev/null +++ b/src/main/java/tsp/headdb/core/command/SubCommand.java @@ -0,0 +1,36 @@ +package tsp.headdb.core.command; + +import tsp.headdb.HeadDB; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; + +// args[0] = sub-command | args[1+] = params +public abstract class SubCommand extends HeadDBCommand { + + private final String[] aliases; + + public SubCommand(String name, Collection completions, @Nullable String... aliases) { + super(name, "headdb.command." + name, completions); + this.aliases = aliases; + } + + public SubCommand(String name, String... aliases) { + this(name, new ArrayList<>(), aliases); + } + + public SubCommand(String name) { + this(name, (String[]) null); + } + + public Optional getAliases() { + return Optional.ofNullable(aliases); + } + + public void register() { + HeadDB.getInstance().getCommandManager().register(this); + } + +} diff --git a/src/main/java/tsp/headdb/core/economy/BasicEconomyProvider.java b/src/main/java/tsp/headdb/core/economy/BasicEconomyProvider.java new file mode 100644 index 0000000..286cfb7 --- /dev/null +++ b/src/main/java/tsp/headdb/core/economy/BasicEconomyProvider.java @@ -0,0 +1,20 @@ +package tsp.headdb.core.economy; + +import org.bukkit.entity.Player; + +import java.math.BigDecimal; +import java.util.concurrent.CompletableFuture; + +public interface BasicEconomyProvider { + + CompletableFuture canPurchase(Player player, BigDecimal cost); + + CompletableFuture withdraw(Player player, BigDecimal amount); + + default CompletableFuture purchase(Player player, BigDecimal amount) { + return canPurchase(player, amount).thenCompose(result -> result ? withdraw(player, amount) : CompletableFuture.completedFuture(false)); + } + + void init(); + +} diff --git a/src/main/java/tsp/headdb/core/economy/VaultProvider.java b/src/main/java/tsp/headdb/core/economy/VaultProvider.java new file mode 100644 index 0000000..1f8eaaa --- /dev/null +++ b/src/main/java/tsp/headdb/core/economy/VaultProvider.java @@ -0,0 +1,45 @@ +package tsp.headdb.core.economy; + +import net.milkbowl.vault.economy.Economy; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.RegisteredServiceProvider; +import tsp.headdb.HeadDB; + +import java.math.BigDecimal; +import java.util.concurrent.CompletableFuture; + +public class VaultProvider implements BasicEconomyProvider { + + private Economy economy; + + @Override + public CompletableFuture canPurchase(Player player, BigDecimal cost) { + double effectiveCost = cost.doubleValue(); + return CompletableFuture.supplyAsync(() -> economy.has(player, effectiveCost >= 0 ? effectiveCost : 0)); // Vault is really old... + } + + @Override + public CompletableFuture withdraw(Player player, BigDecimal amount) { + double effectiveCost = amount.doubleValue(); + return CompletableFuture.supplyAsync(() -> economy.withdrawPlayer(player, effectiveCost >= 0 ? effectiveCost : 0).transactionSuccess()); + } + + + @Override + public void init() { + if (!Bukkit.getServer().getPluginManager().isPluginEnabled("Vault")) { + HeadDB.getInstance().getLog().error("Vault is not installed!"); + return; + } + + RegisteredServiceProvider economyProvider = Bukkit.getServer().getServicesManager().getRegistration(Economy.class); + if (economyProvider == null) { + HeadDB.getInstance().getLog().error("Could not find vault economy provider!"); + return; + } + + economy = economyProvider.getProvider(); + } + +} diff --git a/src/main/java/tsp/headdb/core/hook/Hooks.java b/src/main/java/tsp/headdb/core/hook/Hooks.java new file mode 100644 index 0000000..dc72c32 --- /dev/null +++ b/src/main/java/tsp/headdb/core/hook/Hooks.java @@ -0,0 +1,9 @@ +package tsp.headdb.core.hook; + +import org.bukkit.Bukkit; + +public class Hooks { + + public static final PluginHook PAPI = new PluginHook(Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null); + +} diff --git a/src/main/java/tsp/headdb/core/hook/PluginHook.java b/src/main/java/tsp/headdb/core/hook/PluginHook.java new file mode 100644 index 0000000..bb224f4 --- /dev/null +++ b/src/main/java/tsp/headdb/core/hook/PluginHook.java @@ -0,0 +1,3 @@ +package tsp.headdb.core.hook; + +public record PluginHook(boolean enabled) {} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java b/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java new file mode 100644 index 0000000..187f522 --- /dev/null +++ b/src/main/java/tsp/headdb/core/listener/PlayerJoinListener.java @@ -0,0 +1,20 @@ +package tsp.headdb.core.listener; + +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import tsp.headdb.HeadDB; + +public final class PlayerJoinListener implements Listener { + + public PlayerJoinListener() { + Bukkit.getPluginManager().registerEvents(this, HeadDB.getInstance()); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + //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..5cdff12 --- /dev/null +++ b/src/main/java/tsp/headdb/core/storage/PlayerData.java @@ -0,0 +1,12 @@ +package tsp.headdb.core.storage; + +import java.io.Serializable; +import java.util.Set; +import java.util.UUID; + +// Notice that there is no need to add any additional boilerplate in order to make this serializable. +// Specifically, there is no need to add a serialVersionUID field, +// since the serialVersionUID of a record class is 0L unless explicitly declared, +// and the requirement for matching the serialVersionUID value is waived for record classes. +// Source: https://docs.oracle.com/en/java/javase/15/serializable-records/index.html#:~:text=Specifically%2C%20there%20is%20no%20need,is%20waived%20for%20record%20classes. +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..3ad5ccc --- /dev/null +++ b/src/main/java/tsp/headdb/core/storage/PlayerStorage.java @@ -0,0 +1,79 @@ +package tsp.headdb.core.storage; + +import tsp.headdb.HeadDB; +import tsp.warehouse.storage.file.SerializableFileDataManager; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; + +public class PlayerStorage extends SerializableFileDataManager> { + + private final Map players = new HashMap<>(); + + public PlayerStorage(HeadDB instance, Storage storage) { + super(new File(instance.getDataFolder(), "data/players.data"), storage.getExecutor()); + } + + public void set(PlayerData data) { + this.players.put(data.uniqueId(), data); + } + + public Set getFavorites(UUID uuid) { + return players.containsKey(uuid) ? players.get(uuid).favorites() : new HashSet<>(); + } + + public void addFavorite(UUID uuid, String texture) { + Set fav = getFavorites(uuid); + fav.add(texture); + players.put(uuid, new PlayerData(uuid, new HashSet<>(fav))); + } + + public void removeFavorite(UUID uuid, String texture) { + Set fav = getFavorites(uuid); + fav.remove(texture); + players.put(uuid, new PlayerData(uuid, new HashSet<>(fav))); + } + + 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(new HashSet<>(players.values())).whenComplete((success, ex) -> HeadDB.getInstance().getLog().debug("Saved " + players.values().size() + " player data!")); + } + + public void suspend() { + Boolean saved = save(new HashSet<>(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..ca5d775 --- /dev/null +++ b/src/main/java/tsp/headdb/core/storage/Storage.java @@ -0,0 +1,33 @@ +package tsp.headdb.core.storage; + +import tsp.headdb.HeadDB; + +import java.io.File; +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); + validateDataDirectory(); + playerStorage = new PlayerStorage(HeadDB.getInstance(), this); + } + + public PlayerStorage getPlayerStorage() { + return playerStorage; + } + + public Executor getExecutor() { + return executor; + } + + private void validateDataDirectory() { + //noinspection ResultOfMethodCallIgnored + new File(HeadDB.getInstance().getDataFolder(), "data").mkdir(); + } + +} diff --git a/src/main/java/tsp/headdb/core/task/UpdateTask.java b/src/main/java/tsp/headdb/core/task/UpdateTask.java new file mode 100644 index 0000000..faf22d7 --- /dev/null +++ b/src/main/java/tsp/headdb/core/task/UpdateTask.java @@ -0,0 +1,35 @@ +package tsp.headdb.core.task; + +import tsp.headdb.HeadDB; +import tsp.headdb.core.api.HeadAPI; +import tsp.smartplugin.tasker.Task; + +import java.util.concurrent.TimeUnit; + +@SuppressWarnings("ClassCanBeRecord") +public class UpdateTask implements Task { + + private final long interval; + + public UpdateTask(long interval) { + this.interval = interval; + } + + @Override + public void run() { + HeadAPI.getDatabase().update((time, heads) -> HeadDB.getInstance().getLog().debug("Fetched: " + heads.size() + " Heads | Provider: " + HeadAPI.getDatabase().getRequester().getProvider().name() + " | Time: " + time + "ms (" + TimeUnit.MILLISECONDS.toSeconds(time) + "s)")); + HeadDB.getInstance().getStorage().getPlayerStorage().backup(); + HeadDB.getInstance().getLog().debug("UpdateTask finished!"); + } + + @Override + public long getRepeatInterval() { + return interval; + } + + @Override + public boolean isAsync() { + return true; + } + +} diff --git a/src/main/java/tsp/headdb/core/util/BuildProperties.java b/src/main/java/tsp/headdb/core/util/BuildProperties.java new file mode 100644 index 0000000..2c5a157 --- /dev/null +++ b/src/main/java/tsp/headdb/core/util/BuildProperties.java @@ -0,0 +1,45 @@ +package tsp.headdb.core.util; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.plugin.java.JavaPlugin; + +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * This class contains the properties of the build. + * + * @author TheSilentPro (Silent) + */ +public class BuildProperties { + + private String version = "unknown"; + private String timestamp = "unknown"; + private String author = "unknown"; + + public BuildProperties(JavaPlugin plugin) { + InputStream in = plugin.getResource("plugin.yml"); + if (in == null) { + plugin.getLogger().severe("Could not get plugin.yml to read build information."); + return; + } + + YamlConfiguration data = YamlConfiguration.loadConfiguration(new InputStreamReader(in)); + this.version = data.getString("version", "unknown"); + this.timestamp = data.getString("buildTimestamp", "unknown"); + this.author = data.getString("buildAuthor", "unknown"); + } + + public String getVersion() { + return version; + } + + public String getTimestamp() { + return timestamp; + } + + public String getAuthor() { + return author; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/util/Utils.java b/src/main/java/tsp/headdb/core/util/Utils.java new file mode 100644 index 0000000..b65af22 --- /dev/null +++ b/src/main/java/tsp/headdb/core/util/Utils.java @@ -0,0 +1,234 @@ +package tsp.headdb.core.util; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import me.clip.placeholderapi.PlaceholderAPI; +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.HeadDB; +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.implementation.category.Category; +import tsp.headdb.implementation.head.Head; +import tsp.smartplugin.inventory.Button; +import tsp.smartplugin.inventory.PagedPane; +import tsp.smartplugin.inventory.Pane; +import tsp.smartplugin.utils.StringUtils; +import tsp.smartplugin.utils.Validate; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; +import java.lang.reflect.Field; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class Utils { + + private static final HeadDB instance = HeadDB.getInstance(); + + public static Optional validateUniqueId(@Nonnull String raw) { + try { + return Optional.of(UUID.fromString(raw)); + } catch (IllegalArgumentException ignored) { + return Optional.empty(); + } + } + + @ParametersAreNonnullByDefault + public static String translateTitle(String raw, int size, String category, @Nullable String query) { + return StringUtils.colorize(raw) + .replace("%size%", String.valueOf(size)) + .replace("%category%", category) + .replace("%query%", (query != null ? query : "%query%")); + } + + @ParametersAreNonnullByDefault + public static String translateTitle(String raw, int size, String category) { + return translateTitle(raw, size, category, null); + } + + public static boolean matches(String provided, String query) { + provided = ChatColor.stripColor(provided.toLowerCase(Locale.ROOT)); + query = query.toLowerCase(Locale.ROOT); + return provided.equals(query) + || provided.startsWith(query) + || provided.contains(query); + //|| provided.endsWith(query); + } + + public static void fill(@Nonnull Pane pane, @Nullable ItemStack item) { + Validate.notNull(pane, "Pane can not be null!"); + + if (item == null) { + item = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta meta = item.getItemMeta(); + //noinspection ConstantConditions + meta.setDisplayName(""); + item.setItemMeta(meta); + } + + for (int i = 0; i < pane.getInventory().getSize(); i++) { + ItemStack current = pane.getInventory().getItem(i); + if (current == null || current.getType().isAir()) { + pane.setButton(i, new Button(item, e -> e.setCancelled(true))); + } + } + } + + @SuppressWarnings("SpellCheckingInspection") + public static PagedPane createPaged(Player player, String title) { + PagedPane main = new PagedPane(4, 6, title); + HeadAPI.getHeadByTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODY1MmUyYjkzNmNhODAyNmJkMjg2NTFkN2M5ZjI4MTlkMmU5MjM2OTc3MzRkMThkZmRiMTM1NTBmOGZkYWQ1ZiJ9fX0=").ifPresent(head -> main.setBackItem(head.getItem(player.getUniqueId()))); + HeadAPI.getHeadByTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvY2Q5MWY1MTI2NmVkZGM2MjA3ZjEyYWU4ZDdhNDljNWRiMDQxNWFkYTA0ZGFiOTJiYjc2ODZhZmRiMTdmNGQ0ZSJ9fX0=").ifPresent(head -> main.setCurrentItem(head.getItem(player.getUniqueId()))); + HeadAPI.getHeadByTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmEzYjhmNjgxZGFhZDhiZjQzNmNhZThkYTNmZTgxMzFmNjJhMTYyYWI4MWFmNjM5YzNlMDY0NGFhNmFiYWMyZiJ9fX0=").ifPresent(head -> main.setNextItem(head.getItem(player.getUniqueId()))); + main.setControlCurrent(new Button(main.getCurrentItem(), e -> Bukkit.dispatchCommand(player, "hdb"))); + return main; + } + + @ParametersAreNonnullByDefault + public static void addHeads(Player player, @Nullable Category category, PagedPane pane, Collection heads) { + for (Head head : heads) { + ItemStack item = head.getItem(player.getUniqueId()); + pane.addButton(new Button(item, e -> { + e.setCancelled(true); + + if (category != null && instance.getConfig().getBoolean("requireCategoryPermission") && !player.hasPermission("headdb.category." + category.getName())) { + instance.getLocalization().sendMessage(player.getUniqueId(), "noPermission"); + return; + } + + if (e.isLeftClick()) { + int amount = 1; + if (e.isShiftClick()) { + amount = 64; + } + + purchase(player, head, amount); + } else if (e.isRightClick()) { + HeadDB.getInstance().getStorage().getPlayerStorage().addFavorite(player.getUniqueId(), head.getTexture()); + } + })); + } + } + + private static CompletableFuture processPayment(Player player, Head head, int amount) { + Optional optional = HeadDB.getInstance().getEconomyProvider(); + if (optional.isEmpty()) { + return CompletableFuture.completedFuture(true); // No economy, the head is free + } else { + BigDecimal cost = BigDecimal.valueOf(HeadDB.getInstance().getConfig().getDouble("economy.cost." + head.getCategory().getName()) * amount); + HeadDB.getInstance().getLocalization().sendMessage(player.getUniqueId(), "processPayment", msg -> msg + .replace("%name%", head.getName()) + .replace("%amount%", String.valueOf(amount)) + .replace("%cost%", HeadDB.getInstance().getDecimalFormat().format(cost)) + ); + return optional.get().purchase(player, cost).thenApply(success -> { + HeadPurchaseEvent event = new HeadPurchaseEvent(player, head, cost, success); + Bukkit.getPluginManager().callEvent(event); + return !event.isCancelled() && success; + }); + } + } + + private static void purchase(Player player, Head head, int amount) { + processPayment(player, head, amount).whenComplete((success, ex) -> { + if (ex != null) { + HeadDB.getInstance().getLog().error("Failed to purchase head '" + head.getName() + "' for player: " + player.getName()); + ex.printStackTrace(); + } else { + // Bukkit API, therefore task is ran sync. + Bukkit.getScheduler().runTask(HeadDB.getInstance(), () -> { + ItemStack item = head.getItem(player.getUniqueId()); + item.setAmount(amount); + player.getInventory().addItem(item); + HeadDB.getInstance().getConfig().getStringList("commands.purchase").forEach(command -> { + if (command.isEmpty()) { + return; + } + if (Hooks.PAPI.enabled()) { + command = PlaceholderAPI.setPlaceholders(player, command); + } + + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); + }); + }); + } + }); + } + + public static Optional getTexture(ItemStack head) { + ItemMeta meta = head.getItemMeta(); + if (meta == null) { + return Optional.empty(); + } + + try { + Field profileField = meta.getClass().getDeclaredField("profile"); + profileField.setAccessible(true); + GameProfile profile = (GameProfile) profileField.get(meta); + if (profile == null) { + return Optional.empty(); + } + + return profile.getProperties().get("textures").stream() + .filter(p -> p.getName().equals("textures")) + .findAny() + .map(Property::getValue); + } catch (NoSuchFieldException | SecurityException | IllegalAccessException e ) { + e.printStackTrace(); + return Optional.empty(); + } + } + + public static int resolveInt(String raw) { + try { + return Integer.parseInt(raw); + } catch (NumberFormatException nfe) { + return 1; + } + } + + public static ItemStack getItemFromConfig(String path, Material def) { + ConfigurationSection section = HeadDB.getInstance().getConfig().getConfigurationSection(path); + Validate.notNull(section, "Section can not be null!"); + + Material material = Material.matchMaterial(section.getString("material", def.name())); + if (material == null) { + material = def; + } + + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + + if (meta != null) { + //noinspection ConstantConditions + meta.setDisplayName(StringUtils.colorize(section.getString("name"))); + + List lore = new ArrayList<>(); + for (String line : section.getStringList("lore")) { + if (line != null && !line.isEmpty()) { + lore.add(StringUtils.colorize(line)); + } + } + meta.setLore(lore); + item.setItemMeta(meta); + } + + return item; + } + +} diff --git a/src/main/java/tsp/headdb/economy/BasicEconomyProvider.java b/src/main/java/tsp/headdb/economy/BasicEconomyProvider.java deleted file mode 100644 index f1b3e83..0000000 --- a/src/main/java/tsp/headdb/economy/BasicEconomyProvider.java +++ /dev/null @@ -1,48 +0,0 @@ -package tsp.headdb.economy; - -import org.bukkit.entity.Player; - -import java.math.BigDecimal; -import java.util.function.Consumer; - -/** - * An interface for generalizing Economy Provider's - * - * @author TheSilentPro - * @since 4.0.0 - * @see VaultProvider - * @see TreasuryProvider - */ -public interface BasicEconomyProvider { - - /** - * Retrieve if the player can purchase a head using this economy provider - * - * @param player The player - * @param cost The cost - * @param result If the player has enough to purchase - */ - default void canPurchase(Player player, BigDecimal cost, Consumer result) { - result.accept(true); - } - - /** - * Charge the player a specific amount using this economy provider - * - * @param player The player - * @param amount The amount - * @param result If the transaction was successful - */ - default void charge(Player player, BigDecimal amount, Consumer result) { - result.accept(true); - } - - /** - * Convenience method for initializing economy - * - * @see VaultProvider#initProvider() - * @see TreasuryProvider#initProvider() - */ - void initProvider(); - -} diff --git a/src/main/java/tsp/headdb/economy/TreasuryProvider.java b/src/main/java/tsp/headdb/economy/TreasuryProvider.java deleted file mode 100644 index 9d905cf..0000000 --- a/src/main/java/tsp/headdb/economy/TreasuryProvider.java +++ /dev/null @@ -1,98 +0,0 @@ -package tsp.headdb.economy; - -import me.lokka30.treasury.api.common.service.Service; -import me.lokka30.treasury.api.common.service.ServiceRegistry; -import me.lokka30.treasury.api.economy.EconomyProvider; -import me.lokka30.treasury.api.economy.account.PlayerAccount; -import me.lokka30.treasury.api.economy.currency.Currency; -import me.lokka30.treasury.api.economy.response.EconomyException; -import me.lokka30.treasury.api.economy.response.EconomySubscriber; -import me.lokka30.treasury.api.economy.transaction.EconomyTransactionInitiator; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import tsp.headdb.HeadDB; -import tsp.headdb.util.Log; - -import java.math.BigDecimal; -import java.util.Optional; -import java.util.function.Consumer; - -/** - * A {@link BasicEconomyProvider} for Treasury - * - * @author TheSilentPro - * @since 4.0.0 - */ -public class TreasuryProvider implements BasicEconomyProvider { - - private EconomyProvider provider; - private EconomyTransactionInitiator transactionInitiator; - private Currency currency; - - @Override - public void canPurchase(Player player, BigDecimal cost, Consumer result) { - EconomySubscriber - .asFuture(s -> provider.hasPlayerAccount(player.getUniqueId(), s)) - .thenCompose(val -> { - if (Boolean.TRUE.equals(val)) { - return EconomySubscriber.asFuture(s -> provider.retrievePlayerAccount(player.getUniqueId(), s)); - } else { - return EconomySubscriber.asFuture(s -> provider.createPlayerAccount(player.getUniqueId(), s)); - } - }) - .thenCompose(account -> EconomySubscriber.asFuture(s -> account.retrieveBalance(currency, s))) - .whenComplete((bal, ex) -> result.accept(bal.compareTo(cost) >= 0)); - } - - @Override - public void charge(Player player, BigDecimal amount, Consumer result) { - EconomySubscriber - .asFuture(s -> provider.hasPlayerAccount(player.getUniqueId(), s)) - .thenCompose(val -> { - if (Boolean.TRUE.equals(val)) { - return EconomySubscriber.asFuture(s -> provider.retrievePlayerAccount(player.getUniqueId(), s)); - } else { - return EconomySubscriber.asFuture(s -> provider.createPlayerAccount(player.getUniqueId(), s)); - } - }).whenComplete((account, ex) -> account.withdrawBalance( - amount, - transactionInitiator, - currency, - new EconomySubscriber() { - @Override - public void succeed(@NotNull BigDecimal bigDecimal) { - result.accept(true); - } - - @Override - public void fail(@NotNull EconomyException exception) { - result.accept(false); - Log.error(ex); - } - })); - } - - @Override - public void initProvider() { - Optional> service = ServiceRegistry.INSTANCE.serviceFor(EconomyProvider.class); - - if(!service.isPresent()) { - Log.error("Unable to find a supported economy plugin for Treasury!"); - return; - } - - provider = service.get().get(); - transactionInitiator = EconomyTransactionInitiator.createInitiator(EconomyTransactionInitiator.Type.PLUGIN, "HeadDB"); - - String rawCurrency = HeadDB.getInstance().getConfig().getString("economy.currency"); - if (rawCurrency == null || rawCurrency.isEmpty()) { - currency = provider.getPrimaryCurrency(); - } else { - provider.getCurrencies().stream() - .filter(c -> c.getIdentifier().equalsIgnoreCase(rawCurrency)) - .findFirst() - .ifPresentOrElse(c -> currency = c, () -> Log.error("Could not find currency: " + rawCurrency)); - } - } - -} diff --git a/src/main/java/tsp/headdb/economy/VaultProvider.java b/src/main/java/tsp/headdb/economy/VaultProvider.java deleted file mode 100644 index 1b7d6b6..0000000 --- a/src/main/java/tsp/headdb/economy/VaultProvider.java +++ /dev/null @@ -1,52 +0,0 @@ -package tsp.headdb.economy; - -import net.milkbowl.vault.economy.Economy; -import org.bukkit.Bukkit; -import org.bukkit.entity.Player; -import org.bukkit.plugin.RegisteredServiceProvider; -import tsp.headdb.util.Log; -import tsp.headdb.util.Utils; - -import java.math.BigDecimal; -import java.util.function.Consumer; - -/** - * A {@link BasicEconomyProvider} for Vault - * - * @author TheSilentPro - * @since 4.0.0 - */ -public class VaultProvider implements BasicEconomyProvider { - - private Economy economy; - - @Override - public void canPurchase(Player player, BigDecimal cost, Consumer result) { - Utils.async(t -> result.accept(economy.has(player, cost.doubleValue()))); - } - - @Override - public void charge(Player player, BigDecimal amount, Consumer result) { - Utils.async(t -> result.accept(economy.withdrawPlayer(player, amount.doubleValue()).transactionSuccess())); - } - - public void initProvider() { - if (!Bukkit.getServer().getPluginManager().isPluginEnabled("Vault")) { - Log.error("Vault is not installed!"); - return; - } - - RegisteredServiceProvider economyProvider = Bukkit.getServer().getServicesManager().getRegistration(Economy.class); - if (economyProvider == null) { - Log.error("Could not find vault economy provider!"); - return; - } - - economy = economyProvider.getProvider(); - } - - public Economy getProvider() { - return economy; - } - -} diff --git a/src/main/java/tsp/headdb/implementation/Category.java b/src/main/java/tsp/headdb/implementation/Category.java deleted file mode 100644 index a5d1d19..0000000 --- a/src/main/java/tsp/headdb/implementation/Category.java +++ /dev/null @@ -1,91 +0,0 @@ -package tsp.headdb.implementation; - -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import tsp.headdb.api.HeadAPI; - -import javax.annotation.Nullable; -import java.util.Objects; -import java.util.Optional; - -/** - * Represents a category for heads - * - * @author TheSilentPro - */ -public enum Category { - - ALPHABET("alphabet", ChatColor.YELLOW, 20), - ANIMALS("animals", ChatColor.DARK_AQUA, 21), - BLOCKS("blocks", ChatColor.DARK_GRAY, 22), - DECORATION("decoration", ChatColor.LIGHT_PURPLE, 23), - FOOD_DRINKS("food-drinks", ChatColor.GOLD, 24), - HUMANS("humans", ChatColor.DARK_BLUE, 29), - HUMANOID("humanoid", ChatColor.AQUA, 30), - MISCELLANEOUS("miscellaneous", ChatColor.DARK_GREEN, 31), - MONSTERS("monsters", ChatColor.RED, 32), - PLANTS("plants", ChatColor.GREEN, 33); - - private final String name; - private final ChatColor color; - private final int location; - private static final Category[] cache = values(); - - Category(String name, ChatColor color, int location) { - this.name = name; - this.color = color; - this.location = location; - } - - public String getName() { - return name; - } - - public ChatColor getColor() { - return color; - } - - public int getLocation() { - return location; - } - - /** - * Retrieve the first valid head from a category - * - * @return First valid head - */ - public ItemStack getItem() { - Optional result = HeadAPI.getHeads(this).stream() - .filter(Objects::nonNull) - .findFirst(); - - if (result.isPresent()) { - return result.get().getMenuItem(); - } else { - return new ItemStack(Material.PLAYER_HEAD); - } - } - - /** - * Retrieve a {@link Category} by name - * - * @param name The name - * @return The category if it exists. Else it returns null - */ - @Nullable - public static Category getByName(String name) { - for (Category category : cache) { - if (category.getName().equalsIgnoreCase(name)) { - return category; - } - } - - return null; - } - - public static Category[] getCache() { - return cache; - } - -} diff --git a/src/main/java/tsp/headdb/implementation/DataSaveTask.java b/src/main/java/tsp/headdb/implementation/DataSaveTask.java deleted file mode 100644 index 00ac241..0000000 --- a/src/main/java/tsp/headdb/implementation/DataSaveTask.java +++ /dev/null @@ -1,11 +0,0 @@ -package tsp.headdb.implementation; - -import tsp.headdb.HeadDB; - -public class DataSaveTask implements Runnable { - - @Override - public void run() { - HeadDB.getInstance().getPlayerData().save(); - } -} diff --git a/src/main/java/tsp/headdb/implementation/DatabaseUpdateTask.java b/src/main/java/tsp/headdb/implementation/DatabaseUpdateTask.java deleted file mode 100644 index 413711f..0000000 --- a/src/main/java/tsp/headdb/implementation/DatabaseUpdateTask.java +++ /dev/null @@ -1,16 +0,0 @@ -package tsp.headdb.implementation; - -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Log; - -/** - * Task that updates the database on an interval - */ -public class DatabaseUpdateTask implements Runnable { - - @Override - public void run() { - HeadAPI.getDatabase().update(heads -> Log.info("Fetched " + HeadAPI.getHeads().size() + " heads!")); - } - -} diff --git a/src/main/java/tsp/headdb/implementation/Head.java b/src/main/java/tsp/headdb/implementation/Head.java deleted file mode 100644 index 6a0315e..0000000 --- a/src/main/java/tsp/headdb/implementation/Head.java +++ /dev/null @@ -1,142 +0,0 @@ -package tsp.headdb.implementation; - -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.properties.Property; -import org.apache.commons.lang.Validate; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; -import tsp.headdb.util.Log; -import tsp.headdb.util.Utils; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; -import java.util.regex.Pattern; - -/** - * Represents a Head that a player can obtain via the database - * - * @author TheSilentPro - */ -public class Head { - - public static final Pattern SPLIT = Pattern.compile(","); - private String name; - private UUID uuid; - private String value; - private Category category; - private int id; - private List tags; - private ItemStack menuItem; - - public Head() {} - - public Head(int id) { - this.id = id; - } - - public ItemStack getMenuItem() { - if (menuItem == null) { - Validate.notNull(name, "name must not be null!"); - Validate.notNull(uuid, "uuid must not be null!"); - Validate.notNull(value, "value must not be null!"); - - ItemStack item = new ItemStack(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) item.getItemMeta(); - meta.setDisplayName(Utils.colorize(category != null ? category.getColor() + name : "&8" + name)); - // set skull owner - GameProfile profile = new GameProfile(uuid, name); - profile.getProperties().put("textures", new Property("textures", value)); - Field profileField; - try { - profileField = meta.getClass().getDeclaredField("profile"); - profileField.setAccessible(true); - profileField.set(meta, profile); - } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { - Log.error("Could not set skull owner for " + uuid.toString() + " | Stack Trace:"); - Log.error(ex); - } - - meta.setLore(Arrays.asList( - Utils.colorize("&cID: " + id), - Utils.colorize("&e" + buildTagLore(tags)), - "", - Utils.colorize("&8Right-Click to add/remove from favorites.") - )); - - item.setItemMeta(meta); - menuItem = item; - } - - return menuItem; - } - - public String getName() { - return name; - } - - public UUID getUniqueId() { - return uuid; - } - - public String getValue() { - return value; - } - - public Category getCategory() { - return category; - } - - public int getId() { - return id; - } - - public List getTags() { - return tags; - } - - public Head name(String name) { - this.name = name; - return this; - } - - public Head uniqueId(UUID uuid) { - this.uuid = uuid; - return this; - } - - public Head value(String value) { - this.value = value; - return this; - } - - public Head category(Category category) { - this.category = category; - return this; - } - - public Head id(int id) { - this.id = id; - return this; - } - - public Head tags(String tags) { - this.tags = Arrays.asList(SPLIT.split(tags)); - return this; - } - - private String buildTagLore(List tags) { - StringBuilder builder = new StringBuilder(); - for (int i = 0; i < tags.size(); i++) { - builder.append(tags.get(i)); - if (i != tags.size() - 1) { - builder.append(","); - } - } - - return builder.toString(); - } - -} diff --git a/src/main/java/tsp/headdb/implementation/HeadDatabase.java b/src/main/java/tsp/headdb/implementation/HeadDatabase.java deleted file mode 100644 index 8a72208..0000000 --- a/src/main/java/tsp/headdb/implementation/HeadDatabase.java +++ /dev/null @@ -1,298 +0,0 @@ -package tsp.headdb.implementation; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.plugin.java.JavaPlugin; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.parser.JSONParser; -import org.json.simple.parser.ParseException; -import tsp.headdb.HeadDB; -import tsp.headdb.api.event.DatabaseUpdateEvent; -import tsp.headdb.util.Log; -import tsp.headdb.util.Utils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.TimeUnit; -import java.util.function.Consumer; -import javax.annotation.Nonnull; - -/** - * This is the Database that holds all heads - * - * @author TheSilentPro - */ -// TODO: Optionals instead of null. -public class HeadDatabase { - - private final JavaPlugin plugin; - private final EnumMap> heads = new EnumMap<>(Category.class); - private long refresh; - private int timeout; - private long updated; - private int nextId; // Internal only - - public HeadDatabase(JavaPlugin plugin) { - this.plugin = plugin; - this.refresh = 3600; - this.timeout = 5000; - } - - public Head getHeadByValue(String value) { - List fetched = getHeads(); - for (Head head : fetched) { - if (head.getValue().equals(value)) { - return head; - } - } - - return null; - } - - public Head getHeadByID(int id) { - List fetched = getHeads(); - for (Head head : fetched) { - if (head.getId() == id) { - return head; - } - } - - return null; - } - - public Head getHeadByUniqueId(UUID uuid) { - List fetched = getHeads(); - for (Head head : fetched) { - if (head.getUniqueId().equals(uuid)) { - return head; - } - } - - return null; - } - - public List getHeadsByTag(String tag) { - List result = new ArrayList<>(); - List fetched = getHeads(); - tag = tag.toLowerCase(Locale.ROOT); - for (Head head : fetched) { - for (String t : head.getTags()) { - if (t.toLowerCase(Locale.ROOT).contains(tag)) { - result.add(head); - } - } - } - - return result; - } - - public List getHeadsByName(Category category, String name) { - List result = new ArrayList<>(); - List fetched = getHeads(category); - for (Head head : fetched) { - String hName = ChatColor.stripColor(head.getName().toLowerCase(Locale.ROOT)); - if (hName.contains(ChatColor.stripColor(name.toLowerCase(Locale.ROOT)))) { - result.add(head); - } - } - - return result; - } - - public List getHeadsByName(String name) { - List result = new ArrayList<>(); - for (Category category : Category.getCache()) { - result.addAll(getHeadsByName(category, name)); - } - - return result; - } - - @Nonnull - public List getHeads(Category category) { - List result = heads.get(category); - return result != null ? Collections.unmodifiableList(result) : Collections.emptyList(); - } - - /** - * Gets all heads from the cache if available. - * - * @return List containing each head in its category. - */ - @Nonnull - public List getHeads() { - List result = new ArrayList<>(); - for (Category category : heads.keySet()) { - result.addAll(getHeads(category)); - } - return result; - } - - public void getHeadsNoCache(Consumer>> resultSet) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, task -> { - Log.debug("[" + plugin.getName() + "] Updating database... "); - EnumMap> result = new EnumMap<>(Category.class); - Category[] categories = Category.getCache(); - - for (Category category : categories) { - Log.debug("Caching heads from: " + category.getName()); - List results = new ArrayList<>(); - try { - // First the original api is fetched - results = gather("https://minecraft-heads.com/scripts/api.php?cat=" + category.getName() + "&tags=true", category); - } catch (ParseException | IOException e) { - Log.debug("[" + plugin.getName() + "] Failed to fetch heads (no-cache) from category " + category.getName() + " | Stack Trace:"); - Log.debug(e); - Log.warning("Failed to fetch heads for " + category.getName()); - if (HeadDB.getInstance().getConfig().getBoolean("fallback", true)) { - Log.info("Attempting fallback provider for: " + category.getName()); - try { - // If the original fails and fallback is enabled, fetch from static archive - results = gather("https://heads.pages.dev/archive/" + category.getName() + ".json", category); - } catch (IOException | ParseException ex) { - Log.error("Failed to fetch heads for " + category.getName() + "! (OF)"); // OF = Original-Fallback, both failed - Log.error(ex); - } - } - } - - updated = System.nanoTime(); - result.put(category, results); - } - - resultSet.accept(result); - }); - } - - /** - * Fetches and gathers the heads from the url. - * For internal use only! - * - * @param url The url - * @param category The category of the heads - * @return List of heads for that category - * @throws IOException error - * @throws ParseException error - */ - protected List gather(String url, Category category) throws IOException, ParseException { - long start = System.currentTimeMillis(); - List headList = new ArrayList<>(); - // TODO: gson - JSONParser parser = new JSONParser(); - JSONArray array = (JSONArray) parser.parse(fetch(url)); - for (Object o : array) { - JSONObject obj = (JSONObject) o; - String rawUUID = obj.get("uuid").toString(); - - UUID uuid; - if (Utils.validateUniqueId(rawUUID)) { - uuid = UUID.fromString(rawUUID); - } else { - uuid = UUID.randomUUID(); - } - - Head head = new Head(nextId++) - .name(obj.get("name").toString()) - .uniqueId(uuid) - .value(obj.get("value").toString()) - .tags(obj.get("tags") != null ? obj.get("tags").toString() : "None") - .category(category); - - headList.add(head); - } - - long elapsed = (System.currentTimeMillis() - start); - Log.debug(category.getName() + " -> Done! Time: " + elapsed + "ms (" + TimeUnit.MILLISECONDS.toSeconds(elapsed) + "s)"); - return headList; - } - - /** - * Fetches heads from the url. - * For internal use only! - * - * @param url The url - * @return JSON-string response - * @throws IOException error - */ - protected String fetch(String url) throws IOException { - String line; - StringBuilder response = new StringBuilder(); - - URLConnection connection = new URL(url).openConnection(); - connection.setConnectTimeout(timeout); - connection.setRequestProperty("User-Agent", plugin.getName() + "-DatabaseUpdater"); - BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); - while ((line = in.readLine()) != null) { - response.append(line); - } - - return response.toString(); - } - - public void update(Consumer>> result) { - Bukkit.getScheduler().runTaskAsynchronously(plugin, task -> getHeadsNoCache(headsList -> { - if (headsList == null) { - Log.error("[" + plugin.getName() + "] Failed to update database! Check above for any errors."); - result.accept(null); - return; - } - - heads.clear(); - heads.putAll(headsList); - result.accept(heads); - Bukkit.getPluginManager().callEvent(new DatabaseUpdateEvent(this, heads)); - })); - } - - /** - * Get the last time the database was updated. - * - * @return Last update in seconds - */ - public long getLastUpdate() { - long now = System.nanoTime(); - long elapsed = now - updated; - return TimeUnit.NANOSECONDS.toSeconds(elapsed); - } - - /** - * Checks if the update is past the refresh time - * - * @return Whether the update is old - */ - public boolean isLastUpdateOld() { - return getLastUpdate() >= refresh; - } - - public void setTimeout(int timeout) { - this.timeout = timeout; - } - - public int getTimeout() { - return timeout; - } - - public long getRefresh() { - return refresh; - } - - public void setRefresh(long refresh) { - this.refresh = refresh; - } - - public JavaPlugin getPlugin() { - return plugin; - } - -} diff --git a/src/main/java/tsp/headdb/implementation/LocalHead.java b/src/main/java/tsp/headdb/implementation/LocalHead.java deleted file mode 100644 index dfaffd0..0000000 --- a/src/main/java/tsp/headdb/implementation/LocalHead.java +++ /dev/null @@ -1,87 +0,0 @@ -package tsp.headdb.implementation; - -import org.apache.commons.lang.Validate; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; -import tsp.headdb.util.Utils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.UUID; - -/** - * Represents a local player head that can be obtained via the LocalHeads option - * - * @author TheSilentPro - */ -public class LocalHead extends Head { - - private UUID uuid; - private String name; - - public LocalHead(UUID uuid) { - this.uuid = uuid; - } - - @Override - public ItemStack getMenuItem() { - Validate.notNull(uuid, "uuid must not be null!"); - - ItemStack item = new ItemStack(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) item.getItemMeta(); - meta.setOwningPlayer(Bukkit.getOfflinePlayer(uuid)); - meta.setDisplayName(Utils.colorize("&e" + name)); - List lore = new ArrayList<>(); - lore.add(Utils.colorize("&7UUID: " + uuid.toString())); - meta.setLore(lore); - item.setItemMeta(meta); - - return item; - } - - @Override - public UUID getUniqueId() { - return uuid; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getValue() { - return null; - } - - @Override - public Category getCategory() { - return null; - } - - @Override - public int getId() { - return -1; - } - - @Override - public List getTags() { - return Collections.emptyList(); - } - - @Override - public LocalHead uniqueId(UUID uuid) { - this.uuid = uuid; - return this; - } - - @Override - public LocalHead name(String name) { - this.name = name; - return this; - } - -} diff --git a/src/main/java/tsp/headdb/implementation/category/Category.java b/src/main/java/tsp/headdb/implementation/category/Category.java new file mode 100644 index 0000000..f5034a4 --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/category/Category.java @@ -0,0 +1,85 @@ +package tsp.headdb.implementation.category; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.HeadDB; +import tsp.headdb.core.api.HeadAPI; +import tsp.headdb.core.util.Utils; +import tsp.smartplugin.builder.item.ItemBuilder; +import tsp.smartplugin.utils.StringUtils; + +import javax.annotation.Nonnull; +import java.util.Locale; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +public enum Category { + + ALPHABET("alphabet", 20), + ANIMALS("animals", 21), + BLOCKS("blocks", 22), + DECORATION("decoration", 23), + FOOD_DRINKS("food-drinks", 24), + HUMANS("humans", 29), + HUMANOID("humanoid", 30), + MISCELLANEOUS("miscellaneous", 31), + MONSTERS("monsters", 32), + PLANTS("plants", 33); + + private final String name; + private final int defaultSlot; + private ItemStack item; + + public static final Category[] VALUES = values(); + + Category(String name, int slot) { + this.name = name; + this.defaultSlot = slot; + } + + public String getName() { + return name; + } + + public int getDefaultSlot() { + return defaultSlot; + } + + public static Optional getByName(String cname) { + for (Category value : VALUES) { + if (value.name.equalsIgnoreCase(cname) || value.getName().equalsIgnoreCase(cname)) { + return Optional.of(value); + } + } + + return Optional.empty(); + } + + @Nonnull + public ItemStack getItem(UUID receiver) { + if (item == null) { + HeadAPI.getHeads(this).stream().findFirst() + .ifPresentOrElse(head -> { + ItemStack retrieved = new ItemStack(head.getItem(receiver)); + ItemMeta meta = retrieved.getItemMeta(); + if (meta != null && meta.getLore() != null) { + meta.setDisplayName(Utils.translateTitle(HeadDB.getInstance().getLocalization().getMessage(receiver, "menu.main.category.name").orElse("&e" + getName()), HeadAPI.getHeads(this).size(), getName().toUpperCase(Locale.ROOT))); + meta.setLore(HeadDB.getInstance().getConfig().getStringList("menu.main.category.lore").stream() + .map(StringUtils::colorize) + .collect(Collectors.toList())); + retrieved.setItemMeta(meta); + item = retrieved; + } else { + item = new ItemStack(Material.PLAYER_HEAD); + HeadDB.getInstance().getLog().debug("Failed to get null-meta category item for: " + name()); + } + }, + () -> item = new ItemBuilder(Material.PLAYER_HEAD).name(getName().toUpperCase(Locale.ROOT)).build()); + } + + return item.clone(); // Return clone that changes are not reflected + } + +} diff --git a/src/main/java/tsp/headdb/implementation/head/Head.java b/src/main/java/tsp/headdb/implementation/head/Head.java new file mode 100644 index 0000000..a67e05b --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/head/Head.java @@ -0,0 +1,103 @@ +package tsp.headdb.implementation.head; + +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.HeadDB; +import tsp.headdb.implementation.category.Category; +import tsp.smartplugin.builder.item.ItemBuilder; +import tsp.smartplugin.localization.TranslatableLocalization; +import tsp.smartplugin.utils.Validate; + +import javax.annotation.ParametersAreNonnullByDefault; +import java.lang.reflect.Field; +import java.util.Locale; +import java.util.UUID; + +public class Head { + + private final int id; + private final UUID uniqueId; + private final String name; + private final String texture; + private final String tags; + private final String updated; + private final Category category; + private ItemStack item; + + @ParametersAreNonnullByDefault + public Head(int id, UUID uniqueId, String name, String texture, String tags, String updated, Category category) { + Validate.notNull(uniqueId, "Unique id can not be null!"); + Validate.notNull(name, "Name can not be null!"); + Validate.notNull(texture, "Texture can not be null!"); + Validate.notNull(tags, "Tags can not be null!"); + Validate.notNull(updated, "Updated can not be null!"); + Validate.notNull(category, "Category can not be null!"); + + this.id = id; + this.uniqueId = uniqueId; + this.name = name; + this.texture = texture; + this.tags = tags; + this.updated = updated; + this.category = category; + } + + public ItemStack getItem(UUID receiver) { + if (item == null) { + TranslatableLocalization localization = HeadDB.getInstance().getLocalization(); + item = new ItemBuilder(Material.PLAYER_HEAD) + .name(localization.getMessage(receiver, "menu.head.name").orElse("&e" + name.toUpperCase(Locale.ROOT)).replace("%name%", name)) + .setLore("&cID: " + id, "&7Tags: &e" + tags) + .build(); + + ItemMeta meta = item.getItemMeta(); + GameProfile profile = new GameProfile(uniqueId, name); + profile.getProperties().put("textures", new Property("textures", texture)); + try { + //noinspection ConstantConditions + Field profileField = meta.getClass().getDeclaredField("profile"); + profileField.setAccessible(true); + profileField.set(meta, profile); + } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { + //Log.error("Could not set skull owner for " + uuid.toString() + " | Stack Trace:"); + ex.printStackTrace(); + } + + item.setItemMeta(meta); + } + + return item.clone(); // Return clone that changes are not reflected + } + + public int getId() { + return id; + } + + public UUID getUniqueId() { + return uniqueId; + } + + public String getName() { + return name; + } + + public String getTexture() { + return texture; + } + + public String getTags() { + return tags; + } + + public String getUpdated() { + return updated; + } + + public Category getCategory() { + return category; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java b/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java new file mode 100644 index 0000000..3f488ad --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java @@ -0,0 +1,70 @@ +package tsp.headdb.implementation.head; + +import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitScheduler; +import tsp.headdb.implementation.category.Category; +import tsp.headdb.implementation.requester.HeadProvider; +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 { + + private final JavaPlugin plugin; + private final BukkitScheduler scheduler; + private final Requester requester; + 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 ConcurrentHashMap<>(); + } + + public Map> getHeads() { + return heads; + } + + public void getHeadsNoCache(BiConsumer>> heads) { + getScheduler().runTaskAsynchronously(plugin, () -> { + long start = System.currentTimeMillis(); + Map> result = new HashMap<>(); + for (Category category : Category.VALUES) { + requester.fetchAndResolve(category, response -> result.put(category, response)); + } + + heads.accept(System.currentTimeMillis() - start, result); + }); + } + + public void update(BiConsumer>> fetched) { + getHeadsNoCache((elapsed, result) -> { + heads.putAll(result); + timestamp = System.currentTimeMillis(); + fetched.accept(elapsed, result); + }); + } + + public long getTimestamp() { + return timestamp; + } + + public JavaPlugin getPlugin() { + return plugin; + } + + public BukkitScheduler getScheduler() { + return scheduler; + } + + public Requester getRequester() { + return requester; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/head/LocalHead.java b/src/main/java/tsp/headdb/implementation/head/LocalHead.java new file mode 100644 index 0000000..613b108 --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/head/LocalHead.java @@ -0,0 +1,28 @@ +package tsp.headdb.implementation.head; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; + +import java.util.Collections; +import java.util.UUID; + +public record LocalHead(UUID uniqueId, String name) { + + public ItemStack getItem() { + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.getItemMeta(); + if (meta != null) { + meta.setOwningPlayer(Bukkit.getOfflinePlayer(uniqueId)); + meta.setDisplayName(ChatColor.GOLD + name); + //noinspection UnnecessaryToStringCall + meta.setLore(Collections.singletonList(ChatColor.GRAY + "UUID: " + uniqueId.toString())); + item.setItemMeta(meta); + } + + return item.clone(); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/requester/HeadProvider.java b/src/main/java/tsp/headdb/implementation/requester/HeadProvider.java new file mode 100644 index 0000000..062330e --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/requester/HeadProvider.java @@ -0,0 +1,26 @@ +package tsp.headdb.implementation.requester; + +import tsp.headdb.implementation.category.Category; + +public enum HeadProvider { + + HEAD_API("https://minecraft-heads.com/scripts/api.php?cat=%s&tags=true"), // No ids + HEAD_STORAGE("https://raw.githubusercontent.com/TheSilentPro/HeadStorage/master/storage/%s.json"), + HEAD_WORKER(""), // Unimplemented yet. + HEAD_ARCHIVE("https://heads.pages.dev/archive/%s.json"); + + private final String url; + + HeadProvider(String url) { + this.url = url; + } + + public String getUrl() { + return url; + } + + public String getFormattedUrl(Category category) { + return String.format(getUrl(), category.getName()); + } + +} diff --git a/src/main/java/tsp/headdb/implementation/requester/Requester.java b/src/main/java/tsp/headdb/implementation/requester/Requester.java new file mode 100644 index 0000000..2b66632 --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/requester/Requester.java @@ -0,0 +1,96 @@ +package tsp.headdb.implementation.requester; + +import org.bukkit.plugin.java.JavaPlugin; +import tsp.headdb.HeadDB; +import tsp.headdb.core.util.Utils; +import tsp.headdb.implementation.category.Category; +import tsp.headdb.implementation.head.Head; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; + +public class Requester { + + private final JavaPlugin plugin; + private HeadProvider provider; + + public Requester(JavaPlugin plugin, HeadProvider provider) { + this.plugin = plugin; + this.provider = provider; + } + + public void fetchAndResolve(Category category, Consumer> heads) { + try { + fetch(category, response -> { + List result = new ArrayList<>(); + if (response.code() != 200) { + heads.accept(result); + return; + } + + JsonArray main = JsonParser.parseString(response.response()).getAsJsonArray(); + for (JsonElement entry : main) { + JsonObject obj = entry.getAsJsonObject(); + result.add(new Head( + obj.get("id").getAsInt(), + Utils.validateUniqueId(obj.get("uuid").getAsString()).orElse(UUID.randomUUID()), + obj.get("name").getAsString(), + obj.get("value").getAsString(), + obj.get("tags").getAsString(), + response.date(), + category + )); + } + + heads.accept(result); + }); + } catch (IOException ex) { + HeadDB.getInstance().getLog().debug("Failed to load from provider: " + provider.name()); + if (HeadDB.getInstance().getConfig().getBoolean("fallback") && provider != HeadProvider.HEAD_ARCHIVE) { // prevent recursion. Maybe switch to an attempts counter down in the future + provider = HeadProvider.HEAD_ARCHIVE; + fetchAndResolve(category, heads); + } + } + } + + public void fetch(Category category, Consumer response) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new URL(provider.getFormattedUrl(category)).openConnection(); + connection.setConnectTimeout(5000); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", plugin.getName() + "/" + plugin.getDescription().getVersion()); + connection.setRequestProperty("Accept", "application/json"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + + response.accept(new Response(builder.toString(), connection.getResponseCode(), connection.getHeaderField("date"))); + } + + connection.disconnect(); + } + + public HeadProvider getProvider() { + return provider; + } + + public JavaPlugin getPlugin() { + return plugin; + } + +} diff --git a/src/main/java/tsp/headdb/implementation/requester/Response.java b/src/main/java/tsp/headdb/implementation/requester/Response.java new file mode 100644 index 0000000..9d21755 --- /dev/null +++ b/src/main/java/tsp/headdb/implementation/requester/Response.java @@ -0,0 +1,3 @@ +package tsp.headdb.implementation.requester; + +public record Response(String response, int code, String date) {} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/inventory/Button.java b/src/main/java/tsp/headdb/inventory/Button.java deleted file mode 100644 index 2fabb66..0000000 --- a/src/main/java/tsp/headdb/inventory/Button.java +++ /dev/null @@ -1,78 +0,0 @@ -package tsp.headdb.inventory; - -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.inventory.ItemStack; - -import java.util.Objects; -import java.util.function.Consumer; - -/** - * A button - */ -public class Button { - - private static int counter = 0; - private static final int ID = counter++; - - private final ItemStack itemStack; - private Consumer action; - - /** - * @param itemStack The Item - */ - @SuppressWarnings("unused") - public Button(ItemStack itemStack) { - this(itemStack, event -> { - }); - } - - /** - * @param itemStack The Item - * @param action The action - */ - public Button(ItemStack itemStack, Consumer action) { - this.itemStack = itemStack; - this.action = action; - } - - - - /** - * @return The icon - */ - @SuppressWarnings("WeakerAccess") - public ItemStack getItemStack() { - return itemStack; - } - - /** - * @param action The new action - */ - @SuppressWarnings("unused") - public void setAction(Consumer action) { - this.action = action; - } - - /** - * @param event The event that triggered it - */ - @SuppressWarnings("WeakerAccess") - public void onClick(InventoryClickEvent event) { - action.accept(event); - } - - // We do not want equals collisions. The default hashcode would not fulfil this contract. - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - - return o instanceof Button; - } - - @Override - public int hashCode() { - return Objects.hash(ID); - } -} diff --git a/src/main/java/tsp/headdb/inventory/InventoryUtils.java b/src/main/java/tsp/headdb/inventory/InventoryUtils.java deleted file mode 100644 index 7d086df..0000000 --- a/src/main/java/tsp/headdb/inventory/InventoryUtils.java +++ /dev/null @@ -1,422 +0,0 @@ -package tsp.headdb.inventory; - -import me.clip.placeholderapi.PlaceholderAPI; -import org.bukkit.Bukkit; -import org.bukkit.Material; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; - -import tsp.headdb.HeadDB; -import tsp.headdb.implementation.Head; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.implementation.LocalHead; -import tsp.headdb.implementation.Category; -import tsp.headdb.economy.BasicEconomyProvider; -import tsp.headdb.api.event.PlayerHeadPurchaseEvent; -import tsp.headdb.util.Localization; -import tsp.headdb.util.Utils; - -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import java.util.stream.Collectors; - -/** - * Class for handling the "dirty" work - * such as inventories and economy. - */ -// TODO: Rewrite -public class InventoryUtils { - - private InventoryUtils() {} - private static final FileConfiguration config = HeadDB.getInstance().getConfig(); - - private static final Localization localization = HeadDB.getInstance().getLocalization(); - private static final Map uiLocation = new HashMap<>(); - private static final Map uiItem = new HashMap<>(); - - public static void openLocalMenu(Player player) { - List heads = HeadAPI.getLocalHeads(); - - PagedPane pane = new PagedPane(4, 6, - replace(localization.getMessage("menu.local"), heads.size(), "Local", "None", player)); - for (LocalHead localHead : heads) { - pane.addButton(new Button(localHead.getMenuItem(), e -> { - if (e.getClick() == ClickType.SHIFT_LEFT) { - purchaseHead(player, localHead, 64, "local", localHead.getName()); - return; - } - if (e.getClick() == ClickType.LEFT) { - purchaseHead(player, localHead, 1, "local", localHead.getName()); - return; - } - if (e.getClick() == ClickType.RIGHT) { - Utils.sendMessage(player, localization.getMessage("localFavorites")); - } - })); - } - - pane.open(player); - } - - public static void openFavoritesMenu(Player player) { - List heads = config.getBoolean("hidden.hideFavorites") ? filter(HeadAPI.getFavoriteHeads(player.getUniqueId())) : HeadAPI.getFavoriteHeads(player.getUniqueId()); - - PagedPane pane = new PagedPane(4, 6, - replace(localization.getMessage("menu.favorites"), heads.size(), "Favorites", "None", player)); - for (Head head : heads) { - pane.addButton(new Button(head.getMenuItem(), e -> { - if (e.getClick() == ClickType.SHIFT_LEFT) { - purchaseHead(player, head, 64, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.LEFT) { - purchaseHead(player, head, 1, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.RIGHT) { - HeadAPI.removeFavoriteHead(player.getUniqueId(), head.getValue()); - openFavoritesMenu(player); - Utils.sendMessage(player, "&7Removed &e" + head.getName() + " &7from favorites."); - Utils.playSound(player, "removeFavorite"); - } - })); - } - - pane.open(player); - } - - public static PagedPane openSearchDatabase(Player player, String search) { - List heads = filter(HeadAPI.getHeadsByName(search)); - - PagedPane pane = new PagedPane(4, 6, - replace(localization.getMessage("menu.search"), heads.size(), "None", search, player)); - for (Head head : heads) { - pane.addButton(new Button(head.getMenuItem(), e -> { - if (e.getClick() == ClickType.SHIFT_LEFT) { - purchaseHead(player, head, 64, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.LEFT) { - purchaseHead(player, head, 1, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.RIGHT) { - if (!player.hasPermission("headdb.favorites")) { - Utils.sendMessage(player, localization.getMessage("noPermission")); - Utils.playSound(player, "noPermission"); - return; - } - - HeadAPI.addFavoriteHead(player.getUniqueId(), head.getValue()); - Utils.sendMessage(player, "&7Added &e" + head.getName() + " &7to favorites."); - Utils.playSound(player, "addFavorite"); - } - })); - } - - pane.open(player); - return pane; - } - - public static void openTagSearchDatabase(Player player, String tag) { - List heads = filter(HeadAPI.getHeadsByTag(tag)); - - PagedPane pane = new PagedPane(4, 6, - replace(localization.getMessage("menu.tagSearch"), heads.size(), "None", tag, player)); - for (Head head : heads) { - pane.addButton(new Button(head.getMenuItem(), e -> { - if (e.getClick() == ClickType.SHIFT_LEFT) { - purchaseHead(player, head, 64, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.LEFT) { - purchaseHead(player, head, 1, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.RIGHT) { - if (!player.hasPermission("headdb.favorites")) { - Utils.sendMessage(player, localization.getMessage("noPermission")); - Utils.playSound(player, "noPermission"); - return; - } - - HeadAPI.addFavoriteHead(player.getUniqueId(), head.getValue()); - Utils.sendMessage(player, "&7Added &e" + head.getName() + " &7to favorites."); - Utils.playSound(player, "addFavorite"); - } - })); - } - - pane.open(player); - } - - public static void openCategoryDatabase(Player player, Category category) { - List heads = filter(HeadAPI.getHeads(category)); - - PagedPane pane = new PagedPane(4, 6, - replace(localization.getMessage("menu.category"), heads.size(), category.getName(), "None", player)); - for (Head head : heads) { - pane.addButton(new Button(head.getMenuItem(), e -> { - if (e.getClick() == ClickType.SHIFT_LEFT) { - purchaseHead(player, head, 64, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.LEFT) { - purchaseHead(player, head, 1, head.getCategory().getName(), head.getName()); - } - if (e.getClick() == ClickType.RIGHT) { - if (!player.hasPermission("headdb.favorites")) { - Utils.sendMessage(player, localization.getMessage("noPermission")); - Utils.playSound(player, "noPermission"); - return; - } - - HeadAPI.addFavoriteHead(player.getUniqueId(), head.getValue()); - Utils.sendMessage(player, "&7Added &e" + head.getName() + " &7to favorites."); - Utils.playSound(player, "addFavorite"); - } - })); - } - - pane.open(player); - } - - @SuppressWarnings("ConstantConditions") - public static void openDatabase(Player player) { - Inventory inventory = Bukkit.createInventory(null, 54, - replace(localization.getMessage("menu.main"), HeadAPI.getHeads().size(), "Main", "None", player)); - - for (Category category : Category.getCache()) { - ItemStack item = getUIItem(category.getName(), category.getItem()); - ItemMeta meta = item.getItemMeta(); - meta.setDisplayName(Utils.colorize(localization.getMessage("menu.heads." + category.getName()))); - List lore = new ArrayList<>(); - lore.add(Utils.colorize(replace(localization.getMessage("menu.lore"), HeadAPI.getHeads(category).size(), "Main", "None", player))); - meta.setLore(lore); - item.setItemMeta(meta); - inventory.setItem(getUILocation(category.getName(), category.getLocation()), item); - } - - if (player.hasPermission("headdb.favorites")) { - inventory.setItem(getUILocation("favorites", 39), buildButton( - getUIItem("favorites", new ItemStack(Material.BOOK)), - "&eFavorites", - "", - "&8Click to view your favorites") - ); - } - - if (player.hasPermission("headdb.search")) { - inventory.setItem(getUILocation("search", 40), buildButton( - getUIItem("search", new ItemStack(Material.DARK_OAK_SIGN)), - "&9Search", - "", - "&8Click to open search menu" - )); - } - - if (player.hasPermission("headdb.local")) { - inventory.setItem(getUILocation("local", 41), buildButton( - getUIItem("local", new ItemStack(Material.COMPASS)), - "&aLocal", - "", - "&8Heads from any players that have logged on the server" - )); - } - - fill(inventory); - player.openInventory(inventory); - } - - private static void fill(Inventory inv) { - ItemStack item = getUIItem("fill", new ItemStack(Material.BLACK_STAINED_GLASS_PANE)); - // Do not bother filling the inventory if item to fill it with is AIR. - if (item == null || item.getType() == Material.AIR) return; - - // Fill any non-empty inventory slots with the given item. - int size = inv.getSize(); - for (int i = 0; i < size; i++) { - ItemStack slotItem = inv.getItem(i); - if (slotItem == null || slotItem.getType() == Material.AIR) { - inv.setItem(i, item); - } - } - } - - private static int getUILocation(String category, int slot) { - // Try to use the cached value first. - if (uiLocation.containsKey(category)) return uiLocation.get(category); - - // Try to get the value from the config file. - if (HeadDB.getInstance().getConfig().contains("ui.category." + category + ".location")) { - uiLocation.put(category, HeadDB.getInstance().getConfig().getInt("ui.category." + category + ".location")); - return uiLocation.get(category); - } - - // No valid value in the config file, return the given default. - uiLocation.put(category, slot); - return slot; - } - - @SuppressWarnings("ConstantConditions") - private static ItemStack getUIItem(String category, ItemStack item) { - // Try to use the cached item first. - if (uiItem.containsKey(category)) return uiItem.get(category); - - // Try to get a head from the config file. - if (HeadDB.getInstance().getConfig().contains("ui.category." + category + ".head")) { - int id = HeadDB.getInstance().getConfig().getInt("ui.category." + category + ".head"); - Head head = HeadAPI.getHeadByID(id); - if (head != null) { - uiItem.put(category, head.getMenuItem()); - return uiItem.get(category); - } - } - - // Try to get an item from the config file. - if (HeadDB.getInstance().getConfig().contains("ui.category." + category + ".item")) { - String cfg = HeadDB.getInstance().getConfig().getString("ui.category." + category + ".item"); - Material mat = Material.matchMaterial(cfg); - - // AIR is allowed as the fill material for the menu, but not as a category item. - if (mat != null && (category.equals("fill") || mat != Material.AIR)) { - uiItem.put(category, new ItemStack(mat)); - return uiItem.get(category); - } - } - - // No valid head or item in the config file, return the given default. - uiItem.put(category, item); - return item; - } - - @SuppressWarnings("ConstantConditions") - private static ItemStack buildButton(ItemStack item, String name, String... lore) { - ItemMeta meta = item.getItemMeta(); - meta.setDisplayName(Utils.colorize(name)); - List list = meta.getLore(); - if (list == null) { - list = new ArrayList<>(); - } - - for (String line : lore) { - list.add(Utils.colorize(line)); - } - - item.setItemMeta(meta); - return item; - } - - public static double getCategoryCost(Player player, String category) { - // If the player has the permission headdb.economy.free or headdb.economy.CATEGORY.free, the item is free. - if (player.hasPermission("headdb.economy.free") || player.hasPermission("headdb.economy." + category + ".free")) return 0; - - // Otherwise, get the price for this category from the config file. - return HeadDB.getInstance().getConfig().getDouble("economy.cost." + category); - } - - public static void processPayment(Player player, int amount, String category, String description, Consumer result) { - Utils.async(task -> { - BasicEconomyProvider economyProvider = HeadDB.getInstance().getEconomyProvider(); - - // If economy is disabled or no plugin is present, the item is free. - // Don't mention receiving it for free in this case, since it is always free. - if (economyProvider == null) { - Utils.sendMessage(player, String.format(localization.getMessage("noEconomy"), amount, description)); - Utils.playSound(player, "noEconomy"); - result.accept(true); - return; - } - - BigDecimal cost = BigDecimal.valueOf(getCategoryCost(player, category) * amount); - - // If the cost is higher than zero, attempt to charge for it. - if (cost.compareTo(BigDecimal.ZERO) > 0) { - economyProvider.canPurchase(player, cost, paymentResult -> { - if (Boolean.TRUE.equals(paymentResult)) { - economyProvider.charge(player, cost, chargeResult -> { - if (Boolean.TRUE.equals(chargeResult)) { - Utils.sendMessage(player, String.format(localization.getMessage("purchasedHead"), amount, description, cost)); - Utils.playSound(player, "paid"); - result.accept(true); - } - }); - } else { - Utils.sendMessage(player, String.format(localization.getMessage("notEnoughMoney"), amount, description)); - Utils.playSound(player, "unavailable"); - result.accept(false); - } - }); - return; - } - - // Otherwise, the item is free. - Utils.sendMessage(player, String.format(localization.getMessage("free"), amount, description)); - Utils.playSound(player, "free"); - result.accept(true); - }); - } - - public static void purchaseHead(Player player, Head head, int amount, String category, String description) { - if (config.getBoolean("closeOnPurchase", false)) { - player.closeInventory(); - } - - Utils.sendMessage(player, String.format(localization.getMessage("processPayment"), amount, head.getName())); - processPayment(player, amount, category, description, result -> { - if (Boolean.TRUE.equals(result)) { - PlayerHeadPurchaseEvent event = new PlayerHeadPurchaseEvent(player, head, getCategoryCost(player, category)); - Bukkit.getPluginManager().callEvent(event); - if (!event.isCancelled()) { - ItemStack item = head.getMenuItem(); - item.setAmount(amount); - player.getInventory().addItem(item); - - // Commands can only be ran on main thread - Bukkit.getScheduler().runTask(HeadDB.getInstance(), () -> runPurchaseCommands(player)); - } - } - }); - } - - private static void runPurchaseCommands(Player player) { - for (String cmd : config.getStringList("commands.purchase")) { - if (cmd.isEmpty()) continue; - if (Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI")) PlaceholderAPI.setPlaceholders(player, cmd); - Bukkit.dispatchCommand(Bukkit.getConsoleSender(), cmd.replace("%player%", player.getName())); - } - } - - private static String replace(String message, int size, String category, String search, Player player) { - return message - .replace("%size%", String.valueOf(size)) - .replace("%category%", category) - .replace("%search%", search) - .replace("%player%", player.getName()); - } - - private static List filter(List source) { - if (!config.getBoolean("hidden.enabled")) { - return source; - } - - List result = new ArrayList<>(); - List tags = config.getStringList("hidden.tags"); - List names = config.getStringList("hidden.names"); - - for (Head head : source) { - if (!names.contains(head.getName()) && !contains(head.getTags(), tags)) { - result.add(head); - } - } - - - return result; - } - - private static boolean contains(List list1, List list2) { - return list1.stream().anyMatch(list2.stream().collect(Collectors.toSet())::contains); - } - -} diff --git a/src/main/java/tsp/headdb/inventory/PagedPane.java b/src/main/java/tsp/headdb/inventory/PagedPane.java deleted file mode 100644 index 5f58768..0000000 --- a/src/main/java/tsp/headdb/inventory/PagedPane.java +++ /dev/null @@ -1,386 +0,0 @@ -package tsp.headdb.inventory; - -import net.wesjd.anvilgui.AnvilGUI; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.event.inventory.InventoryClickEvent; -import org.bukkit.event.inventory.InventoryType; -import org.bukkit.inventory.Inventory; -import org.bukkit.inventory.InventoryHolder; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import tsp.headdb.HeadDB; -import tsp.headdb.api.HeadAPI; -import tsp.headdb.util.Utils; - -import java.util.*; -import java.util.Map.Entry; - -/** - * A paged pane. Credits @ I Al Ianstaan - */ -public class PagedPane implements InventoryHolder { - - private Inventory inventory; - - private SortedMap pages = new TreeMap<>(); - private int currentIndex; - private int pageSize; - - @SuppressWarnings("WeakerAccess") - protected Button controlBack; - @SuppressWarnings("WeakerAccess") - protected Button controlNext; - @SuppressWarnings("WeakerAccess") - protected Button controlMain; - - /** - * @param pageSize The page size. inventory rows - 2 - */ - public PagedPane(int pageSize, int rows, String title) { - Objects.requireNonNull(title, "title can not be null!"); - if (rows > 6) { - throw new IllegalArgumentException("Rows must be <= 6, got " + rows); - } - if (pageSize > 6) { - throw new IllegalArgumentException("Page size must be <= 6, got" + pageSize); - } - - this.pageSize = pageSize; - inventory = Bukkit.createInventory(this, rows * 9, color(title)); - - pages.put(0, new Page(pageSize)); - } - - /** - * @param button The button to add - */ - public void addButton(Button button) { - for (Entry entry : pages.entrySet()) { - if (entry.getValue().addButton(button)) { - if (entry.getKey() == currentIndex) { - reRender(); - } - return; - } - } - Page page = new Page(pageSize); - page.addButton(button); - pages.put(pages.lastKey() + 1, page); - - reRender(); - } - - /** - * @param button The Button to remove - */ - @SuppressWarnings("unused") - public void removeButton(Button button) { - for (Iterator> iterator = pages.entrySet().iterator(); iterator.hasNext(); ) { - Entry entry = iterator.next(); - if (entry.getValue().removeButton(button)) { - - // we may need to delete the page - if (entry.getValue().isEmpty()) { - // we have more than one page, so delete it - if (pages.size() > 1) { - iterator.remove(); - } - // the currentIndex now points to a page that does not exist. Correct it. - if (currentIndex >= pages.size()) { - currentIndex--; - } - } - // if we modified the current one, re-render - // if we deleted the current page, re-render too - if (entry.getKey() >= currentIndex) { - reRender(); - } - return; - } - } - } - - /** - * @return The amount of pages - */ - @SuppressWarnings("WeakerAccess") - public int getPageAmount() { - return pages.size(); - } - - /** - * @return The number of the current page (1 based) - */ - @SuppressWarnings("WeakerAccess") - public int getCurrentPage() { - return currentIndex + 1; - } - - /** - * @param index The index of the new page - */ - @SuppressWarnings("WeakerAccess") - public void selectPage(int index) { - if (index < 0 || index >= getPageAmount()) { - throw new IllegalArgumentException( - "Index out of bounds s: " + index + " [" + 0 + " " + getPageAmount() + ")" - ); - } - if (index == currentIndex) { - return; - } - - currentIndex = index; - reRender(); - } - - /** - * Renders the inventory again - */ - @SuppressWarnings("WeakerAccess") - public void reRender() { - inventory.clear(); - pages.get(currentIndex).render(inventory); - - controlBack = null; - controlNext = null; - controlMain = null; - createControls(inventory); - } - - /** - * @param event The {@link InventoryClickEvent} - */ - @SuppressWarnings("WeakerAccess") - public void onClick(InventoryClickEvent event) { - event.setCancelled(true); - - // back item - if (event.getSlot() == inventory.getSize() - 8) { - if (controlBack != null) { - controlBack.onClick(event); - } - return; - } - // next item - else if (event.getSlot() == inventory.getSize() - 2) { - if (controlNext != null) { - controlNext.onClick(event); - } - return; - } - else if (event.getSlot() == inventory.getSize()- 5) { - if (controlMain != null){ - controlMain.onClick(event); - } - return; - } - - pages.get(currentIndex).handleClick(event); - - } - - /** - * Get the object's inventory. - * - * @return The inventory. - */ - @Override - public Inventory getInventory() { - return inventory; - } - - /** - * Creates the controls - * - * @param inventory The inventory - */ - @SuppressWarnings("WeakerAccess") - protected void createControls(Inventory inventory) { - // create separator - fillRow( - inventory.getSize() / 9 - 2, - new ItemStack(Material.BLACK_STAINED_GLASS_PANE), - inventory - ); - - if (getCurrentPage() > 1) { - String name = String.format( - Locale.ROOT, - "&3&lPage &a&l%d &7/ &c&l%d", - getCurrentPage() - 1, getPageAmount() - ); - String lore = String.format( - Locale.ROOT, - "&7Previous: &c%d", - getCurrentPage() - 1 - ); - ItemStack itemStack = setMeta(HeadAPI.getHeadByValue("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODY1MmUyYjkzNmNhODAyNmJkMjg2NTFkN2M5ZjI4MTlkMmU5MjM2OTc3MzRkMThkZmRiMTM1NTBmOGZkYWQ1ZiJ9fX0=").getMenuItem(), name, lore); - controlBack = new Button(itemStack, event -> selectPage(currentIndex - 1)); - inventory.setItem(inventory.getSize() - 8, itemStack); - } - - if (getCurrentPage() < getPageAmount()) { - String name = String.format( - Locale.ROOT, - "&3&lPage &a&l%d &7/ &c&l%d", - getCurrentPage() + 1, getPageAmount() - ); - String lore = String.format( - Locale.ROOT, - "&7Next: &c%d", - getCurrentPage() + 1 - ); - ItemStack itemStack = setMeta(HeadAPI.getHeadByValue("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmEzYjhmNjgxZGFhZDhiZjQzNmNhZThkYTNmZTgxMzFmNjJhMTYyYWI4MWFmNjM5YzNlMDY0NGFhNmFiYWMyZiJ9fX0=").getMenuItem(), name, lore); - controlNext = new Button(itemStack, event -> selectPage(getCurrentPage())); - inventory.setItem(inventory.getSize() - 2, itemStack); - } - - String name = String.format( - Locale.ROOT, - "&3&lPage &a&l%d &7/ &c&l%d", - getCurrentPage(), getPageAmount() - ); - ItemStack itemStack = setMeta(HeadAPI.getHeadByValue("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvY2Q5MWY1MTI2NmVkZGM2MjA3ZjEyYWU4ZDdhNDljNWRiMDQxNWFkYTA0ZGFiOTJiYjc2ODZhZmRiMTdmNGQ0ZSJ9fX0=").getMenuItem(), - name, - "&7Left-Click to go to the &cMain Menu", - "&7Right-Click to go to a &6Specific Page"); - controlMain = new Button(itemStack, event -> { - if (event.getClick() == ClickType.RIGHT) { - new AnvilGUI.Builder() - .onComplete((player, text) -> { - try { - int i = Integer.parseInt(text); - if (i > getPageAmount()) { - Utils.sendMessage(player, "&cPage number is out of bounds! Max: &e" + getPageAmount()); - return AnvilGUI.Response.text("&cOut of bounds!"); - } - selectPage(i - 1); - return AnvilGUI.Response.openInventory(this.getInventory()); - } catch (NumberFormatException nfe) { - Utils.sendMessage(player, "&cValue must be a number!"); - return AnvilGUI.Response.text(Utils.colorize("&cValue must be a number!")); - } - }) - .title("Select Page") - .text("Page number...") - .plugin(HeadDB.getInstance()) - .open((Player) event.getWhoClicked()); - } else { - InventoryUtils.openDatabase((Player) event.getWhoClicked()); - } - }); - inventory.setItem(inventory.getSize() - 5, itemStack); - } - - private void fillRow(int rowIndex, ItemStack itemStack, Inventory inventory) { - int yMod = rowIndex * 9; - for (int i = 0; i < 9; i++) { - int slot = yMod + i; - inventory.setItem(slot, setMeta(itemStack, "")); - } - } - - protected ItemStack setMeta(ItemStack itemStack, String name, String... lore) { - ItemMeta meta = itemStack.getItemMeta(); - meta.setDisplayName(Utils.colorize(name)); - meta.setLore(Arrays.stream(lore).map(this::color).toList()); - itemStack.setItemMeta(meta); - return itemStack; - } - - @SuppressWarnings("WeakerAccess") - protected String color(String input) { - return ChatColor.translateAlternateColorCodes('&', input); - } - - /** - * @param player The {@link Player} to open it for - */ - public void open(Player player) { - reRender(); - player.openInventory(getInventory()); - } - - - private static class Page { - private List