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
+
+ ${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