diff --git a/pom.xml b/pom.xml index 3af403d..0ac4811 100644 --- a/pom.xml +++ b/pom.xml @@ -14,8 +14,8 @@ UTF-8 - 17 - 17 + 21 + 21 TheSilentPro (Silent) dd-MM-yyyy HH:mm:ss ${maven.build.timestamp} @@ -55,42 +55,33 @@ org.spigotmc spigot-api - 1.20.2-R0.1-SNAPSHOT + 1.21.1-R0.1-SNAPSHOT provided - com.mojang - authlib - 4.0.43 + org.slf4j + slf4j-api + 2.0.16 provided com.google.code.gson gson - 2.10.1 + 2.11.0 + provided + + + org.xerial + sqlite-jdbc + 3.47.0.0 provided com.github.TheSilentPro - NexusLib - 45b4813899 - - - com.github.TheSilentPro - Warehouse - b228e4f8b1 - - - net.wesjd - anvilgui - 1.9.2-SNAPSHOT - - - com.github.TheSilentPro - HelperLite - 775582f23b + InvLib + c184c31f6e @@ -124,18 +115,18 @@ org.apache.maven.plugins maven-compiler-plugin - 3.10.1 + 3.13.0 ${maven.compiler.source} ${maven.compiler.target} - + org.apache.maven.plugins maven-shade-plugin - 3.4.1 + 3.6.0 package @@ -159,41 +150,19 @@ *:* false - tsp.headdb.core.util.anvilgui + tsp/headdb/core/util/anvilgui/** + + org/yaml/** + META-INF/maven/org.yaml + javax/** + - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.4.1 - - - javadoc-generate - package - - javadoc - - - - - public - none - - - org.spigotmc - spigot-api - 1.20.2-R0.1-SNAPSHOT - - - - diff --git a/src/main/java/tsp/headdb/HeadDB.java b/src/main/java/tsp/headdb/HeadDB.java index a462171..c497ab7 100644 --- a/src/main/java/tsp/headdb/HeadDB.java +++ b/src/main/java/tsp/headdb/HeadDB.java @@ -1,227 +1,167 @@ package tsp.headdb; +import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; -import tsp.headdb.core.command.*; -import tsp.headdb.core.economy.BasicEconomyProvider; +import org.bukkit.plugin.java.JavaPlugin; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.core.commands.CommandManager; +import tsp.headdb.core.player.PlayerDatabase; +import tsp.headdb.core.config.ConfigData; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.core.economy.EconomyProvider; import tsp.headdb.core.economy.VaultProvider; +import tsp.headdb.api.model.Head; import tsp.headdb.core.storage.Storage; -import tsp.headdb.core.task.UpdateTask; -import tsp.headdb.core.util.HeadDBLogger; -import tsp.headdb.core.util.Utils; -import tsp.helperlite.HelperLite; -import tsp.helperlite.Schedulers; -import tsp.helperlite.scheduler.promise.Promise; -import tsp.helperlite.scheduler.task.Task; -import tsp.nexuslib.NexusPlugin; -import tsp.nexuslib.inventory.PaneListener; -import tsp.nexuslib.localization.TranslatableLocalization; +import tsp.headdb.core.util.Localization; +import tsp.invlib.InvLib; -import java.io.BufferedReader; -import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.text.DecimalFormat; -import java.util.Optional; +import java.util.List; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; -public class HeadDB extends NexusPlugin { +/** + * @author TheSilentPro (Silent) + */ +public class HeadDB extends JavaPlugin { + private static final Logger LOGGER = LoggerFactory.getLogger(HeadDB.class); private static HeadDB instance; - private HeadDBLogger logger; - private TranslatableLocalization localization; + private ConfigData config; + private EconomyProvider economyProvider; + private boolean PAPI; + private PlayerDatabase playerDatabase; + private Localization localization; private Storage storage; - private BasicEconomyProvider economyProvider; private CommandManager commandManager; - private Task updateTask; @Override - public void onStart(NexusPlugin nexusPlugin) { + public void onEnable() { instance = this; - HelperLite.init(this); + saveDefaultConfig(); + this.config = new ConfigData(getConfig()); + this.playerDatabase = new PlayerDatabase(); + this.storage = new Storage().init(); + this.playerDatabase.load(); + LOGGER.info("Loaded {} languages!", loadLocalization()); - instance.saveDefaultConfig(); - instance.logger = new HeadDBLogger(getConfig().getBoolean("debug")); - instance.logger.info("Loading HeadDB - " + Utils.getVersion().orElse(getDescription().getVersion() + " (UNKNOWN SEMVER)")); + InvLib.init(this); - instance.logger.info("Loaded " + loadLocalization() + " languages!"); + this.PAPI = Bukkit.getPluginManager().isPluginEnabled("PlaceholderAPI"); - instance.initStorage(); - instance.initEconomy(); + if (this.config.isEconomyEnabled()) { + if (this.config.getEconomyProvider().equalsIgnoreCase("VAULT")) { + this.economyProvider = new VaultProvider(); + } else { + LOGGER.error("Invalid economy provider in config.yml!"); + this.setEnabled(false); + return; + } - startUpdateTask(); + this.economyProvider.init(); + } - new PaneListener(this); + Bukkit.getScheduler().runTaskTimerAsynchronously(this, this::updateDatabase, 0L, 86400 * 20); - // TODO: Commands helperlite - instance.commandManager = new CommandManager(); - loadCommands(); - - initMetrics(); - ensureLatestVersion(); - instance.logger.info("Done!"); + this.commandManager = new CommandManager().init(); + PluginCommand mainCommand = getCommand("headdb"); + if (mainCommand == null) { + LOGGER.error("Failed to get main /headdb command!"); + this.setEnabled(false); + return; + } + mainCommand.setExecutor(commandManager); + mainCommand.setTabCompleter(commandManager); } @Override public void onDisable() { - if (storage != null) { - storage.getPlayerStorage().suspend(); - File langFile = new File(getDataFolder(), "langs.data"); - if (!langFile.exists()) { - try { - //noinspection ResultOfMethodCallIgnored - langFile.createNewFile(); - localization.saveLanguages(langFile); - } catch (IOException ex) { - logger.error("Failed to save receiver langauges!"); - ex.printStackTrace(); + // Save language data + if (playerDatabase != null) { + playerDatabase.save(); + } + } + + public CompletableFuture> updateDatabase() { + LOGGER.info("Fetching heads..."); + long fetchStart = System.currentTimeMillis(); + return HeadAPI.getDatabase().getHeadsNoCache().thenApply(result -> { + LOGGER.info("Fetched {} total heads! ({}s)", HeadAPI.getTotalHeads(), TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - fetchStart)); + + long preloadStart = System.currentTimeMillis(); + int total = result.size(); + int index = 0; + + LOGGER.info("Preloading {} heads...", total); + // Milestone percentages we want to print + int[] milestones = {25, 50, 75, 100}; + int nextMilestoneIndex = 0; + + for (Head head : result) { + // Simulate processing each head + head.getItem(); + index++; + + // Calculate percentage completion + int progress = (int) ((index / (double) total) * 100); + + // Check if the current progress matches the next milestone + if (nextMilestoneIndex < milestones.length && progress >= milestones[nextMilestoneIndex]) { + LOGGER.info("Preloading heads... {}%", progress); + nextMilestoneIndex++; // Move to the next milestone } } - } - - updateTask.stop(); - } - - private void startUpdateTask() { - updateTask = Schedulers.builder() - .async() - .every(getConfig().getLong("refresh", 86400L), TimeUnit.SECONDS) - .run(new UpdateTask()); - } - - private void ensureLatestVersion() { - Promise.start().thenApplyAsync(a -> { - try { - URLConnection connection = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + 84967).openConnection(); - connection.setConnectTimeout(5000); - connection.setRequestProperty("User-Agent", this.getName() + "-VersionChecker"); - - return new BufferedReader(new InputStreamReader(connection.getInputStream())).readLine().equals(Utils.getVersion().orElse(getDescription().getVersion())); - } catch (IOException ex) { - return false; - } - }).thenAcceptAsync(latest -> { - if (latest) { - instance.logger.warning("There is a new update available for HeadDB on spigot!"); - instance.logger.warning("Download: https://www.spigotmc.org/resources/84967"); - } + LOGGER.info("Preloaded {} total heads! ({}s)", index, TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - preloadStart)); + return result; }); } - // Loaders - - private void initMetrics() { - Metrics metrics = new Metrics(this, 9152); - - metrics.addCustomChart(new Metrics.SimplePie("economy_provider", () -> { - if (getEconomyProvider().isPresent()) { - return this.getConfig().getString("economy.provider"); - } - - return "None"; - })); - } - - private void initStorage() { - storage = new Storage(); - storage.getPlayerStorage().init(); - } - private int loadLocalization() { - instance.localization = new TranslatableLocalization(this, "messages"); + instance.localization = new Localization(this, "messages"); try { instance.localization.createDefaults(); - int count = instance.localization.load(); - File langFile = new File(getDataFolder(), "langs.data"); - if (langFile.exists()) { - localization.loadLanguages(langFile); - } - - return count; + return instance.localization.load(); } catch (URISyntaxException | IOException ex) { - instance.logger.error("Failed to load localization!"); - ex.printStackTrace(); + LOGGER.error("Failed to load localization!", ex); 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 CommandReload().register(); - new CommandTexture().register(); - new CommandLanguage().register(); - new CommandSettings().register(); - new CommandInfo().register(); - } - - // Getters - - public Optional getUpdateTask() { - return Optional.ofNullable(updateTask); - } - - public Storage getStorage() { - return storage; - } - public CommandManager getCommandManager() { return commandManager; } - public Optional getEconomyProvider() { - return Optional.ofNullable(economyProvider); + public Storage getStorage() { + return storage; } - @SuppressWarnings("DataFlowIssue") - private DecimalFormat decimalFormat = new DecimalFormat(getConfig().getString("economy.format")); - - public DecimalFormat getDecimalFormat() { - return decimalFormat != null ? decimalFormat : (decimalFormat = new DecimalFormat("##.##")); + public PlayerDatabase getPlayerDatabase() { + return playerDatabase; } - public TranslatableLocalization getLocalization() { + public Localization getLocalization() { return localization; } - public HeadDBLogger getLog() { - return logger; + public boolean isPAPI() { + return PAPI; + } + + public EconomyProvider getEconomyProvider() { + return economyProvider; + } + + @NotNull + public ConfigData getCfg() { + return config; } public static HeadDB getInstance() { return instance; } -} +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/Metrics.java b/src/main/java/tsp/headdb/Metrics.java index f1cbf8a..d5576e0 100644 --- a/src/main/java/tsp/headdb/Metrics.java +++ b/src/main/java/tsp/headdb/Metrics.java @@ -22,13 +22,27 @@ import java.util.logging.Level; import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; -@SuppressWarnings({"all", "deprecation"}) // Class is from bstats, can't modify it. +@SuppressWarnings("all") class Metrics { private final Plugin plugin; private final MetricsBase metricsBase; + private static final Metrics METRICS = new Metrics(HeadDB.getInstance(), 9152); + + static { + /* + METRICS.addCustomChart(new Metrics.SimplePie("economy_provider", () -> { + if (HeadDB.getInstance().getEconomyProvider().isPresent()) { + return this.getConfig().getString("economy.provider"); + } + + return "None"; + })); + */ + } + /** * Creates a new Metrics instance. * @@ -36,7 +50,7 @@ class Metrics { * @param serviceId The id of the service. It can be found at What is my plugin id? */ - public Metrics(JavaPlugin plugin, int serviceId) { + private Metrics(JavaPlugin plugin, int serviceId) { this.plugin = plugin; // Get the config file File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); @@ -108,7 +122,7 @@ class Metrics { } private void appendServiceData(JsonObjectBuilder builder) { - builder.appendField("pluginVersion", Utils.getVersion().orElse("Unknown")); + builder.appendField("pluginVersion", Utils.getUserAgent()); } private int getPlayerAmount() { diff --git a/src/main/java/tsp/headdb/api/HeadAPI.java b/src/main/java/tsp/headdb/api/HeadAPI.java new file mode 100644 index 0000000..7786f6a --- /dev/null +++ b/src/main/java/tsp/headdb/api/HeadAPI.java @@ -0,0 +1,333 @@ +package tsp.headdb.api; + +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import tsp.headdb.HeadDB; +import tsp.headdb.api.model.Head; +import tsp.headdb.api.model.LocalHead; +import tsp.headdb.api.provider.HeadDataProvider; +import tsp.headdb.core.player.PlayerData; +import tsp.headdb.core.util.Utils; + +import org.jetbrains.annotations.NotNull; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 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() {} + + static final ExecutorService executor = Executors.newFixedThreadPool(2, r -> new Thread(r, "HeadDB Fetcher")); + + /** + * The main {@link HeadDatabase}. + */ + private static final HeadDatabase database = new HeadDatabase(HeadDB.getInstance(), executor, new HeadDataProvider()); + + /** + * 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} + */ + @NotNull + public static CompletableFuture> getHeadsByName(String name, boolean lenient) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> (lenient ? Utils.matches(head.getName(), name) : head.getName().equalsIgnoreCase(name))).collect(Collectors.toList()), executor); + } + + /** + * Retrieve a {@link List} of {@link Head} matching the name. + * + * @param name The name to match against + * @return {@link List Heads} + */ + @NotNull + public static CompletableFuture> 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 CompletableFuture> getHeadByExactName(String name, boolean lenient) { + return CompletableFuture.supplyAsync(getHeadStream().filter(head -> (lenient ? Utils.matches(head.getName(), name) : head.getName().equalsIgnoreCase(name)))::findAny, executor); + } + + /** + * Retrieve a {@link Head} by its exact name. + * + * @param name The name to look for + * @return The {@link Head}, else empty + */ + @NotNull + public static CompletableFuture> 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 + */ + @NotNull + public static CompletableFuture> getHeadById(int id) { + return CompletableFuture.supplyAsync(getHeadStream().filter(head -> head.getId() == id)::findAny, executor); + } + + /** + * Retrieve a {@link Head} by its texture value. + * + * @param texture The texture to look for + * @return The {@link Head}, else empty + */ + @NotNull + public static CompletableFuture> getHeadByTexture(String texture) { + return CompletableFuture.supplyAsync(getHeadStream().filter(head -> head.getTexture().orElse("N/A").equals(texture))::findAny, executor); + } + + /** + * Retrieve a {@link List} of {@link Head Heads} matching the category. + * + * @param category The category + * @return The list of matching heads. + */ + @NotNull + public static CompletableFuture> getHeadsByCategory(String category) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> head.getCategory().orElse("?").equalsIgnoreCase(category)).toList(), executor); + } + + /** + * Retrieve a {@link List} of {@link Head Heads} matching the date. + * + * @param date The date + * @return The list of matching heads. + */ + @NotNull + public static CompletableFuture> getHeadsByDate(String date) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> head.getPublishDate().orElse("?").equalsIgnoreCase(date)).toList(), executor); + } + + /** + * Retrieve a {@link List} of {@link Head Heads} matching the date. + * + * @param dates The dates + * @return The list of matching heads. + */ + @NotNull + public static CompletableFuture> getHeadsByDates(String... dates) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> { + if (head.getPublishDate().isEmpty()) { + return false; + } + for (String date : dates) { + if (head.getPublishDate().get().equalsIgnoreCase(date)) { + return true; + } + } + return false; + }).toList(), executor); + } + + /** + * Retrieve a {@link List} of {@link Head Heads} matching the tags. + * + * @param tags The tags + * @return The list of matching heads. + */ + @NotNull + public static CompletableFuture> getHeadsByTags(String... tags) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> { + String[] array = head.getTags().orElse(null); + if (array == null) { + return false; + } + for (String entry : array) { + for (String tag : tags) { + if (entry.equalsIgnoreCase(tag)) { + return true; + } + } + } + return false; + }).toList(), executor); + } + + /** + * Retrieve a {@link List} of {@link Head Heads} matching the contributors. + * + * @param contributors The contributors + * @return The list of matching heads. + */ + @NotNull + public static CompletableFuture> getHeadsByContributors(String... contributors) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> { + String[] array = head.getContributors().orElse(null); + if (array == null) { + return false; + } + for (String entry : array) { + for (String contributor : contributors) { + if (entry.equalsIgnoreCase(contributor)) { + return true; + } + } + } + return false; + }).toList(), executor); + } + + /** + * Retrieve a {@link List} of {@link Head Heads} matching the collections. + * + * @param collections The collections + * @return The list of matching heads. + */ + @NotNull + public static CompletableFuture> getHeadsByCollections(String... collections) { + return CompletableFuture.supplyAsync(() -> getHeadStream().filter(head -> { + String[] array = head.getTags().orElse(null); + if (array == null) { + return false; + } + for (String entry : array) { + for (String collection : collections) { + if (entry.equalsIgnoreCase(collection)) { + return true; + } + } + } + return false; + }).toList(), executor); + } + + /** + * Retrieve a {@link Stream} of {@link Head} within the main {@link HeadDatabase}. + * + * @return The streamed heads + */ + @NotNull + public static Stream getHeadStream() { + return getHeads().stream(); + } + + /** + * Retrieve a {@link List} of {@link Head} within the main {@link HeadDatabase}. + * + * @return {@link List Heads} + */ + @NotNull + public static List getHeads() { + return 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} + */ + @NotNull + public static CompletableFuture> getLocalHeads(boolean calculateDate) { + OfflinePlayer[] players = Bukkit.getOfflinePlayers(); + return CompletableFuture.supplyAsync(() -> Arrays.stream(players) + .filter(player -> player.getName() != null) + .map(player -> { + PlayerData data = HeadDB.getInstance().getPlayerDatabase().getOrCreate(player.getUniqueId()); + String date = null; + if (calculateDate) { + date = Instant.ofEpochMilli(player.getFirstPlayed()) + .atZone(ZoneId.systemDefault()) + .toLocalDate() + .format(Utils.DATE_FORMATTER); + } + return new LocalHead(data.getId(), data.getUuid(), player.getName(), date); + }) + .collect(Collectors.toSet()), executor); + } + + public static CompletableFuture> getLocalHeads() { + return getLocalHeads(false); + } + + /** + * Retrieve a {@link Set} of favorite heads for the specified {@link UUID player id}. + * Note that this calculates the heads on every call. + * + * @return {@link Set Favorite Heads} + */ + @NotNull + public static CompletableFuture> getFavoriteHeads(UUID player) { + return CompletableFuture.supplyAsync(() -> HeadDB.getInstance() + .getPlayerDatabase() + .getOrCreate(player) + .getFavorites() + .stream() + .map(id -> { + Optional head = HeadAPI.getHeadById(id).join(); + if (head.isPresent()) { + return head; + } else { + return getLocalHeads() + .join() + .stream() + .filter(localHead -> localHead.getId() == id) + .findAny(); + } + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()), executor); + } + + /** + * Returns true when the database is ready with all heads cached. + * + * @return If the database has cached all heads. + */ + public boolean isReady() { + return database.isReady(); + } + + /** + * Retrieve the main {@link HeadDatabase} used by the plugin. + * + * @return {@link HeadDatabase Database} + */ + @NotNull + public static HeadDatabase getDatabase() { + return database; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/HeadDatabase.java b/src/main/java/tsp/headdb/api/HeadDatabase.java new file mode 100644 index 0000000..4fcdd6c --- /dev/null +++ b/src/main/java/tsp/headdb/api/HeadDatabase.java @@ -0,0 +1,62 @@ +package tsp.headdb.api; + +import org.bukkit.plugin.java.JavaPlugin; +import tsp.headdb.api.model.Category; +import tsp.headdb.api.model.Head; +import tsp.headdb.api.provider.HeadProvider; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; + +/** + * @author TheSilentPro (Silent) + */ +public class HeadDatabase { + + private final JavaPlugin plugin; + private final ExecutorService executor; + private final CopyOnWriteArrayList heads; + private final HeadProvider provider; + private long timestamp; + private volatile boolean ready; + + public HeadDatabase(JavaPlugin plugin, ExecutorService executor, HeadProvider provider) { + this.plugin = plugin; + this.executor = executor; + this.provider = provider; + this.heads = new CopyOnWriteArrayList<>(); + this.ready = false; + } + + public List getHeads() { + return Collections.unmodifiableList(heads); + } + + public CompletableFuture> getHeadsNoCache() { + this.ready = false; + this.heads.clear(); + return CompletableFuture.supplyAsync(() -> { + heads.addAll(provider.fetchHeads(executor).join().heads()); + this.ready = true; + timestamp = System.currentTimeMillis(); + return Collections.unmodifiableList(heads); + }, executor); + } + + public boolean isReady() { + return ready; + } + + public long getTimestamp() { + return timestamp; + } + + public JavaPlugin getPlugin() { + return plugin; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/model/Category.java b/src/main/java/tsp/headdb/api/model/Category.java new file mode 100644 index 0000000..e2b59d1 --- /dev/null +++ b/src/main/java/tsp/headdb/api/model/Category.java @@ -0,0 +1,101 @@ +/** + * @author TheSilentPro (Silent) + */ +package tsp.headdb.api.model; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.core.util.Utils; + +import org.jetbrains.annotations.NotNull; +import java.util.List; +import java.util.Optional; + +public enum Category { + + ALPHABET("alphabet", "Alphabet", 20), + ANIMALS("animals", "Animals", 21), + BLOCKS("blocks", "Blocks", 22), + DECORATION("decoration", "Decoration", 23), + FOOD_DRINKS("food-drinks", "Food & Drinks", 24), + HUMANS("humans", "Humans", 29), + HUMANOID("humanoid", "Humanoid", 30), + MISCELLANEOUS("miscellaneous", "Miscellaneous", 31), + MONSTERS("monsters", "Monsters", 32), + PLANTS("plants", "Plants", 33); + + private final String name; + private final String displayName; + private final int defaultSlot; + private ItemStack item; + + public static final Category[] VALUES = values(); + + Category(String name, String displayName, int defaultSlot) { + this.name = name; + this.displayName = displayName; + this.defaultSlot = defaultSlot; + } + + public String getName() { + return name; + } + + public String getDisplayName() { + return displayName; + } + + public int getDefaultSlot() { + return defaultSlot; + } + + public static Optional getByName(String cname) { + for (Category value : VALUES) { + if (value.name.equalsIgnoreCase(cname) || value.name().equalsIgnoreCase(cname)) { + return Optional.of(value); + } + } + + return Optional.empty(); + } + + @NotNull + public ItemStack getDisplayItem() { + if (item == null) { + List headsList = HeadAPI.getHeads() + .stream() + .filter(head -> head.getCategory().orElse("N/A").equalsIgnoreCase(getName())) + .toList(); + + headsList.stream() + .findFirst() + .ifPresentOrElse(head -> { + ItemStack retrieved = new ItemStack(head.getItem()); + ItemMeta meta = retrieved.getItemMeta(); + if (meta != null) { + meta.setDisplayName(ChatColor.BOLD + "" + ChatColor.GOLD + getName().toUpperCase()); + meta.setLore(List.of(ChatColor.GRAY + "Total Heads » " + ChatColor.GOLD + headsList.size())); + retrieved.setItemMeta(meta); + } else { + retrieved = new ItemStack(Material.PLAYER_HEAD); + } + item = retrieved; + }, + () -> { + ItemStack copy = new ItemStack(Material.PLAYER_HEAD); + ItemMeta meta = copy.getItemMeta(); + //noinspection DataFlowIssue + meta.setDisplayName(Utils.colorize(getName().toUpperCase())); + copy.setItemMeta(meta); + item = copy; + }); + + } + + return item.clone(); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/model/Head.java b/src/main/java/tsp/headdb/api/model/Head.java new file mode 100644 index 0000000..57f6dcc --- /dev/null +++ b/src/main/java/tsp/headdb/api/model/Head.java @@ -0,0 +1,95 @@ +package tsp.headdb.api.model; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import tsp.headdb.core.util.Utils; + +import java.util.Objects; +import java.util.Optional; + +/** + * @author TheSilentPro (Silent) + */ +public class Head { + + private final int id; + private final String name; + private final String texture; + private final String category; + private final String publishDate; + private final String[] tags; + private final String[] contributors; + private final String[] collections; + + protected ItemStack item; + + public Head( + int id, + @NotNull String name, + @Nullable String texture, + @Nullable String category, + @Nullable String publishDate, + @Nullable String[] tags, + @Nullable String[] contributors, + @Nullable String[] collections + ) { + Objects.requireNonNull(name, "Name must not be null!"); + + this.id = id; + this.name = name; + this.texture = texture; + this.category = category; + this.publishDate = publishDate; + this.tags = tags; + this.contributors = contributors; + this.collections = collections; + } + + @NotNull + public ItemStack getItem() { + if (item == null) { + if (texture != null) { + item = new ItemStack(Utils.asItem(this)); + } else { + item = new ItemStack(Material.PLAYER_HEAD); + } + } + + return item; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public Optional getTexture() { + return Optional.ofNullable(texture); + } + + public Optional getCategory() { + return Optional.ofNullable(category); + } + + public Optional getPublishDate() { + return Optional.ofNullable(publishDate); + } + + public Optional getTags() { + return Optional.ofNullable(tags); + } + + public Optional getContributors() { + return Optional.ofNullable(contributors); + } + + public Optional getCollections() { + return Optional.ofNullable(collections); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/model/LocalHead.java b/src/main/java/tsp/headdb/api/model/LocalHead.java new file mode 100644 index 0000000..ffb3a5f --- /dev/null +++ b/src/main/java/tsp/headdb/api/model/LocalHead.java @@ -0,0 +1,58 @@ +package tsp.headdb.api.model; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.SkullMeta; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +/** + * Represents a head from a player that has joined the server. + * + * @author TheSilentPro (Silent) + */ +public class LocalHead extends Head { + + private final UUID uuid; + + public LocalHead(int id, @NotNull UUID uniqueId, @NotNull String name, @org.jetbrains.annotations.Nullable String date) { + super(-id, name, null, "Local Head", date, new String[]{"Local Heads"}, null, null); + this.uuid = uniqueId; + } + + public UUID getUniqueId() { + return uuid; + } + + @Nullable + @Override + public Optional getCategory() { + return Optional.of("Local"); + } + + @NotNull + @Override + public ItemStack getItem() { + if (this.item == null) { + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.getItemMeta(); + + if (meta != null) { + meta.setOwningPlayer(Bukkit.getOfflinePlayer(getUniqueId())); + meta.setDisplayName(ChatColor.GOLD + getName()); + meta.setLore(List.of(ChatColor.GRAY + "UUID » " + ChatColor.GOLD + getUniqueId().toString(), getPublishDate().isPresent() ? (ChatColor.GRAY + "First Joined » " + ChatColor.GOLD + getPublishDate().get()) : "")); + item.setItemMeta(meta); + } + + this.item = new ItemStack(item); + } + + return item; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/provider/HeadDataProvider.java b/src/main/java/tsp/headdb/api/provider/HeadDataProvider.java new file mode 100644 index 0000000..16c9b6d --- /dev/null +++ b/src/main/java/tsp/headdb/api/provider/HeadDataProvider.java @@ -0,0 +1,118 @@ +package tsp.headdb.api.provider; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.api.model.Head; +import tsp.headdb.core.util.Utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * @author TheSilentPro (Silent) + */ +public class HeadDataProvider implements HeadProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(HeadDataProvider.class); + + @Override + public String getUrl() { + return "https://raw.githubusercontent.com/TheSilentPro/HeadData/refs/heads/main/heads.json"; + } + + @Override + public CompletableFuture fetchHeads(ExecutorService executor) { + return CompletableFuture.supplyAsync(() -> { + try { + List heads = new ArrayList<>(); + + HttpURLConnection connection = (HttpURLConnection) URI.create(getUrl()).toURL().openConnection(); + connection.setConnectTimeout(5000); + connection.setRequestProperty("User-Agent", Utils.getUserAgent()); + connection.setRequestProperty("Accept", "application/json"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + connection.disconnect(); + + JsonArray main = JsonParser.parseString(builder.toString()).getAsJsonArray(); + + for (JsonElement entry : main) { + JsonObject obj = entry.getAsJsonObject(); + + // Index for json arrays + int i = 0; + + String[] tags = null; + if (!obj.get("tags").isJsonNull()) { + JsonArray rawTags = obj.get("tags").getAsJsonArray(); + tags = new String[rawTags.size()]; + for (JsonElement rawTag : rawTags) { + tags[i] = rawTag.getAsString(); + i++; + } + } + + String[] contributors = null; + if (!obj.get("contributors").isJsonNull()) { + i = 0; + JsonArray rawContributors = obj.get("contributors").getAsJsonArray(); + contributors = new String[rawContributors.size()]; + for (JsonElement rawContributor : rawContributors) { + contributors[i] = rawContributor.getAsString(); + i++; + } + } + + String[] collections = null; + if (!obj.get("collections").isJsonNull()) { + i = 0; + JsonArray rawCollections = obj.get("collections").getAsJsonArray(); + collections = new String[rawCollections.size()]; + for (JsonElement rawCollection : rawCollections) { + collections[i] = rawCollection.getAsString(); + i++; + } + } + + String date = obj.has("publish_date") && !obj.get("publish_date").isJsonNull() ? obj.get("publish_date").getAsString() : null; + heads.add(new Head( + obj.get("id").getAsInt(), + obj.get("name").getAsString(), + obj.has("texture") && !obj.get("texture").isJsonNull() ? obj.get("texture").getAsString() : null, + obj.has("category") && !obj.get("category").isJsonNull() ? obj.get("category").getAsString() : null, + date != null ? (date.substring(8, 10) + "-" + date.substring(5, 7) + "-" + date.substring(0, 4)) : null, // Parses and reverses the date format (yyyy-MM-dd -> dd-MM-yyyy) + tags, + contributors, + collections + )); + } + } + + return new ProviderResponse(heads, Date.from(Instant.now())); + } catch (Exception ex) { + LOGGER.error("Failed to fetch heads!", ex); + return new ProviderResponse(new ArrayList<>(), Date.from(Instant.now())); + } + }, executor); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/provider/HeadFileProvider.java b/src/main/java/tsp/headdb/api/provider/HeadFileProvider.java new file mode 100644 index 0000000..8bb1160 --- /dev/null +++ b/src/main/java/tsp/headdb/api/provider/HeadFileProvider.java @@ -0,0 +1,117 @@ +package tsp.headdb.api.provider; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.api.model.Head; +import tsp.headdb.core.util.Utils; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * {@link HeadProvider} that will read from a json file. + * The json format should be that of {@link HeadDataProvider}. + * + * @author TheSilentPro (Silent) + */ +public class HeadFileProvider implements HeadProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(HeadFileProvider.class); + + private final File file; + + public HeadFileProvider(File file) { + this.file = file; + } + + public File getFile() { + return file; + } + + /** + * Reading is done from the file, not an url. + * + * @return Always null. + */ + @Override + public String getUrl() { + return null; + } + + @Override + public CompletableFuture fetchHeads(ExecutorService executor) { + return CompletableFuture.supplyAsync(() -> { + try { + List heads = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + + JsonArray main = JsonParser.parseString(builder.toString()).getAsJsonArray(); + + for (JsonElement entry : main) { + JsonObject obj = entry.getAsJsonObject(); + JsonArray rawTags = obj.get("tags").getAsJsonArray(); + String[] tags = new String[rawTags.size()]; + int i = 0; + for (JsonElement rawTag : rawTags) { + tags[i] = rawTag.getAsString(); + i++; + } + + i = 0; + JsonArray rawContributors = obj.get("contributors").getAsJsonArray(); + String[] contributors = new String[rawContributors.size()]; + for (JsonElement rawContributor : rawContributors) { + contributors[i] = rawContributor.getAsString(); + i++; + } + + i = 0; + JsonArray rawCollections = obj.get("collections").getAsJsonArray(); + String[] collections = new String[rawCollections.size()]; + for (JsonElement rawCollection : rawCollections) { + collections[i] = rawCollection.getAsString(); + i++; + } + + heads.add(new Head( + obj.get("id").getAsInt(), + obj.get("name").getAsString(), + obj.get("texture").getAsString(), + obj.get("category").getAsString(), + obj.get("publish_date").getAsString(), + tags, + contributors, + collections + )); + } + } + + return new ProviderResponse(heads, Date.from(Instant.now())); + } catch (Exception ex) { + LOGGER.error("Failed to fetch heads!", ex); + return new ProviderResponse(new ArrayList<>(), Date.from(Instant.now())); + } + }, executor); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/provider/HeadProvider.java b/src/main/java/tsp/headdb/api/provider/HeadProvider.java new file mode 100644 index 0000000..e4ae2d0 --- /dev/null +++ b/src/main/java/tsp/headdb/api/provider/HeadProvider.java @@ -0,0 +1,15 @@ +package tsp.headdb.api.provider; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * @author TheSilentPro (Silent) + */ +public interface HeadProvider { + + String getUrl(); + + CompletableFuture fetchHeads(ExecutorService executor); + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/provider/HeadStorageProvider.java b/src/main/java/tsp/headdb/api/provider/HeadStorageProvider.java new file mode 100644 index 0000000..fa5a0fb --- /dev/null +++ b/src/main/java/tsp/headdb/api/provider/HeadStorageProvider.java @@ -0,0 +1,92 @@ +package tsp.headdb.api.provider; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.api.model.Category; +import tsp.headdb.api.model.Head; +import tsp.headdb.core.util.Utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.time.Instant; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * @author TheSilentPro (Silent) + * + * @deprecated Replaced by {@link HeadDataProvider}. + */ +@Deprecated(forRemoval = true, since = "5.0.0") +public class HeadStorageProvider implements HeadProvider { + + private static final Logger LOGGER = LoggerFactory.getLogger(HeadStorageProvider.class); + + @Override + public String getUrl() { + return "https://raw.githubusercontent.com/TheSilentPro/HeadStorage/master/storage/%s.json"; + } + + public CompletableFuture fetchHeads(Category category, ExecutorService executor) { + return CompletableFuture.supplyAsync(() -> { + try { + List heads = new ArrayList<>(); + + HttpURLConnection connection = (HttpURLConnection) URI.create(String.format(getUrl(), category.getName())).toURL().openConnection(); + connection.setConnectTimeout(5000); + connection.setRequestProperty("User-Agent", Utils.getUserAgent()); + connection.setRequestProperty("Accept", "application/json"); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + connection.disconnect(); + + JsonArray main = JsonParser.parseString(builder.toString()).getAsJsonArray(); + + for (JsonElement entry : main) { + JsonObject obj = entry.getAsJsonObject(); + heads.add(new Head( + obj.get("id").getAsInt(), + obj.get("name").getAsString(), + obj.get("value").getAsString(), + category.getName(), + null, + obj.get("tags").getAsString().split(","), + null, + null + )); + } + } + + return new ProviderResponse(heads, Date.from(Instant.now())); + } catch (IOException ex) { + LOGGER.error("Failed to fetch heads for category: {}", category, ex); + return new ProviderResponse(new ArrayList<>(), Date.from(Instant.now())); + } + }, executor); + } + + @Override + public CompletableFuture fetchHeads(ExecutorService executor) { + return CompletableFuture.supplyAsync(() -> { + List heads = new ArrayList<>(); + for (Category category : Category.VALUES) { + heads.addAll(fetchHeads(category, executor).join().heads()); + } + return new ProviderResponse(heads, Date.from(Instant.now())); + }); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/api/provider/ProviderResponse.java b/src/main/java/tsp/headdb/api/provider/ProviderResponse.java new file mode 100644 index 0000000..1be31db --- /dev/null +++ b/src/main/java/tsp/headdb/api/provider/ProviderResponse.java @@ -0,0 +1,11 @@ +package tsp.headdb.api.provider; + +import tsp.headdb.api.model.Head; + +import java.util.Date; +import java.util.List; + +/** + * @author TheSilentPro (Silent) + */ +public record ProviderResponse(List heads, Date date) {} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/api/HeadAPI.java b/src/main/java/tsp/headdb/core/api/HeadAPI.java deleted file mode 100644 index 0bf0a02..0000000 --- a/src/main/java/tsp/headdb/core/api/HeadAPI.java +++ /dev/null @@ -1,189 +0,0 @@ -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 tsp.helperlite.scheduler.promise.Promise; - -import javax.annotation.Nonnull; -import java.util.*; -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 Promise> getFavoriteHeads(UUID player) { - return Promise.supplyingAsync(() -> { - 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/events/AsyncHeadsFetchedEvent.java b/src/main/java/tsp/headdb/core/api/events/AsyncHeadsFetchedEvent.java deleted file mode 100644 index 7930ca9..0000000 --- a/src/main/java/tsp/headdb/core/api/events/AsyncHeadsFetchedEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -package tsp.headdb.core.api.events; - -import org.bukkit.event.Event; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; -import tsp.headdb.implementation.category.Category; -import tsp.headdb.implementation.head.Head; - -import java.util.List; -import java.util.Map; - -public class AsyncHeadsFetchedEvent extends Event { - private static final HandlerList HANDLER_LIST = new HandlerList(); - private final Map> heads; - private final String providerName; - private final long timeTook; - - public AsyncHeadsFetchedEvent(Map> heads, String providerName, long timeTook) { - super(true); - this.heads = heads; - this.providerName = providerName; - this.timeTook = timeTook; - } - - @NotNull - public static HandlerList getHandlerList() { - return HANDLER_LIST; - } - - @NotNull - @Override - public HandlerList getHandlers() { - return HANDLER_LIST; - } - - public Map> getHeads() { - return heads; - } - - public String getProviderName() { - return providerName; - } - - public long getTimeTook() { - return timeTook; - } -} diff --git a/src/main/java/tsp/headdb/core/command/CommandCategory.java b/src/main/java/tsp/headdb/core/command/CommandCategory.java deleted file mode 100644 index b366967..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandCategory.java +++ /dev/null @@ -1,61 +0,0 @@ -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.nexuslib.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 deleted file mode 100644 index 522e332..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandGive.java +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index b57de2e..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandHelp.java +++ /dev/null @@ -1,30 +0,0 @@ -package tsp.headdb.core.command; - -import org.bukkit.command.CommandSender; -import tsp.nexuslib.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 &9reload(r) &7- Reload configuration files."); - 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 deleted file mode 100644 index 688cf13..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandInfo.java +++ /dev/null @@ -1,26 +0,0 @@ -package tsp.headdb.core.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; -import tsp.headdb.core.util.Utils; -import tsp.nexuslib.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")) { - PlayerUtils.sendMessage(sender, "&7Running &6HeadDB - " + Utils.getVersion().orElse(HeadDB.getInstance().getDescription().getVersion() + " &7(&4UNKNOWN SEMVER&7)")); - //PlayerUtils.sendMessage(sender, "&7Build: " + HeadDB.getInstance().getDescription().getVersion()); - PlayerUtils.sendMessage(sender, "&7GitHub: &6https://github.com/TheSilentPro/HeadDB"); - } 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 deleted file mode 100644 index fb6c36d..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandLanguage.java +++ /dev/null @@ -1,36 +0,0 @@ -package tsp.headdb.core.command; - -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import tsp.headdb.HeadDB; -import tsp.headdb.core.util.Utils; - -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%", Utils.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)); - } - -} diff --git a/src/main/java/tsp/headdb/core/command/CommandMain.java b/src/main/java/tsp/headdb/core/command/CommandMain.java deleted file mode 100644 index 9d1c512..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandMain.java +++ /dev/null @@ -1,192 +0,0 @@ -package tsp.headdb.core.command; - -import net.wesjd.anvilgui.AnvilGUI; -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.nexuslib.inventory.Button; -import tsp.nexuslib.inventory.PagedPane; -import tsp.nexuslib.inventory.Pane; -import tsp.nexuslib.util.StringUtils; - -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNonnullByDefault; -import java.util.*; -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().onClick((slot, stateSnapshot) -> { - try { - int page = Integer.parseInt(stateSnapshot.getText()); - // 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 Arrays.asList(AnvilGUI.ResponseAction.openInventory(main.getInventory())); - } catch (NumberFormatException nfe) { - return Arrays.asList(AnvilGUI.ResponseAction.replaceInputText("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); - if (!player.hasPermission("headdb.favorites")) { - HeadDB.getInstance().getLocalization().sendMessage(player, "noAccessFavorites"); - return; - } - - Utils.openFavoritesMenu(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() - .onClick((slot, stateSnapshot) -> { - // Copied from CommandSearch - List heads = new ArrayList<>(); - List headList = HeadAPI.getHeads(); - if (stateSnapshot.getText().length() > 3) { - if (stateSnapshot.getText().startsWith("id:")) { - try { - HeadAPI.getHeadById(Integer.parseInt(stateSnapshot.getText().substring(3))).ifPresent(heads::add); - } catch (NumberFormatException ignored) { - } - } else if (stateSnapshot.getText().startsWith("tg:")) { - heads.addAll(headList.stream().filter(head -> Utils.matches(head.getTags(), stateSnapshot.getText().substring(3))).toList()); - } else { - // no query prefix - heads.addAll(headList.stream().filter(head -> Utils.matches(head.getName(), stateSnapshot.getText())).toList()); - } - } else { - // query is <=3, no point in looking for prefixes - heads.addAll(headList.stream().filter(head -> Utils.matches(head.getName(), stateSnapshot.getText())).toList()); - } - - PagedPane main = Utils.createPaged(player, Utils.translateTitle(getLocalization().getMessage(player.getUniqueId(), "menu.search.name").orElse("&cHeadDB - &eSearch Results"), heads.size(), "None", stateSnapshot.getText())); - 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 - if (getInstance().getConfig().getBoolean("localHeads")) { - 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)); - - 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 null; - } - -} diff --git a/src/main/java/tsp/headdb/core/command/CommandManager.java b/src/main/java/tsp/headdb/core/command/CommandManager.java deleted file mode 100644 index 7d22096..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandManager.java +++ /dev/null @@ -1,40 +0,0 @@ -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/CommandReload.java b/src/main/java/tsp/headdb/core/command/CommandReload.java deleted file mode 100644 index 144237f..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandReload.java +++ /dev/null @@ -1,23 +0,0 @@ -package tsp.headdb.core.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; - -import java.util.HashSet; - -// Todo: async -public class CommandReload extends SubCommand { - - public CommandReload() { - super("reload", new HashSet<>(0), "r"); - } - - @Override - public void handle(CommandSender sender, String[] args) { - getLocalization().sendMessage(sender, "reloadCommand"); - getLocalization().load(); - HeadDB.getInstance().reloadConfig(); - getLocalization().sendMessage(sender, "reloadCommandDone"); - } - -} diff --git a/src/main/java/tsp/headdb/core/command/CommandSearch.java b/src/main/java/tsp/headdb/core/command/CommandSearch.java deleted file mode 100644 index 32a087c..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandSearch.java +++ /dev/null @@ -1,67 +0,0 @@ -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.nexuslib.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 deleted file mode 100644 index 34807d0..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandSettings.java +++ /dev/null @@ -1,53 +0,0 @@ -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.nexuslib.builder.ItemBuilder; -import tsp.nexuslib.inventory.Button; -import tsp.nexuslib.inventory.PagedPane; -import tsp.nexuslib.inventory.Pane; -import tsp.nexuslib.util.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 deleted file mode 100644 index 32cbebb..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandTexture.java +++ /dev/null @@ -1,33 +0,0 @@ -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.nexuslib.util.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 deleted file mode 100644 index 2da6f89..0000000 --- a/src/main/java/tsp/headdb/core/command/CommandUpdate.java +++ /dev/null @@ -1,28 +0,0 @@ -package tsp.headdb.core.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; -import tsp.headdb.core.api.HeadAPI; -import tsp.headdb.implementation.head.HeadResult; -import tsp.helperlite.scheduler.promise.Promise; - -public class CommandUpdate extends SubCommand { - - public CommandUpdate() { - super("update", "u"); - } - - @Override - public void handle(CommandSender sender, String[] args) { - getLocalization().sendMessage(sender, "updateDatabase"); - try (Promise promise = HeadAPI.getDatabase().update()) { - promise.thenAcceptSync(result -> { - HeadDB.getInstance().getLog().debug("Database Updated! Heads: " + result.heads().values().size() + " | Took: " + result.elapsed() + "ms"); - getLocalization().sendMessage(sender, "updateDatabaseDone", msg -> msg.replace("%size%", String.valueOf(result.heads().values().size()))); - }); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - -} diff --git a/src/main/java/tsp/headdb/core/command/HeadDBCommand.java b/src/main/java/tsp/headdb/core/command/HeadDBCommand.java deleted file mode 100644 index 401e5d2..0000000 --- a/src/main/java/tsp/headdb/core/command/HeadDBCommand.java +++ /dev/null @@ -1,59 +0,0 @@ -package tsp.headdb.core.command; - -import org.bukkit.command.CommandSender; -import tsp.headdb.HeadDB; -import tsp.nexuslib.localization.TranslatableLocalization; -import tsp.nexuslib.util.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 deleted file mode 100644 index f4fd5a8..0000000 --- a/src/main/java/tsp/headdb/core/command/SubCommand.java +++ /dev/null @@ -1,36 +0,0 @@ -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/commands/CommandGive.java b/src/main/java/tsp/headdb/core/commands/CommandGive.java new file mode 100644 index 0000000..0c63dfa --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandGive.java @@ -0,0 +1,106 @@ +package tsp.headdb.core.commands; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.api.model.Head; +import tsp.headdb.core.util.Utils; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandGive extends HDBCommand { + + public CommandGive() { + super("give", " ", "Give a specific head. You can use names, texture values, or prefix 'id:' for ids.", false, "g"); + } + + // hdb give + @Override + public void handle(@NotNull CommandSender sender, String[] args) { + if (args.length < 3) { + localization.sendMessage(sender, "invalidArguments"); + return; + } + + Player target = Bukkit.getPlayer(args[0]); + if (target == null) { + localization.sendMessage(sender, "invalidTarget", msg -> msg.replace("%name%", args[0])); + return; + } + + int amount = 1; + try { + amount = Integer.parseInt(args[1]); + } catch (NumberFormatException nfe) { + localization.sendMessage(sender, "invalidNumber", msg -> msg.replace("%name%", args[1])); + } + + //localization.sendMessage(sender, ""); + + final int fAmount = amount; + String id = String.join(" ", Arrays.copyOfRange(args, 2, args.length)); + HeadAPI.getHeadByExactName(id, true) + .thenCompose(optionalHead -> { + if (optionalHead.isPresent()) { + return CompletableFuture.completedFuture(optionalHead.get()); + } else if (id.startsWith("id:")) { + try { + int numericId = Integer.parseInt(id.substring(3)); + return HeadAPI.getHeadById(numericId).thenApply(optional -> optional.orElse(null)); + } catch (NumberFormatException e) { + return CompletableFuture.completedFuture(null); + } + } else { + return HeadAPI.getHeadByTexture(id).thenApply(optional -> optional.orElse(null)); + } + }) + .thenAcceptAsync(head -> { + if (head == null) { + localization.sendMessage(sender, "command.give.invalid", msg -> msg.replace("%name%", id)); + return; + } + ItemStack item = head.getItem(); + item.setAmount(fAmount); + target.getInventory().addItem(item); + localization.sendMessage(sender, "command.give.done", msg -> + msg.replace("%name%", head.getName()) + .replace("%amount%", String.valueOf(fAmount)) + .replace("%player%", target.getName())); + }, Utils.SYNC); + } + + private final List one = Collections.singletonList("1"); // No reason to do more. + + @Override + public List handleCompletions(CommandSender sender, String[] args) { + if (args.length == 1) { + return Bukkit.getOnlinePlayers().stream().map(Player::getName).toList(); + } else if (args.length == 2) { + return one; + } else if (args.length == 3) { + if (sender instanceof ConsoleCommandSender) { + return HeadAPI.getHeads().stream().map(Head::getName).toList().subList(1, 100); // The huge amount of heads can cause console terminals to crash. + } else { + return HeadAPI.getHeads().stream().map(Head::getName).toList(); + } + } else { + return null; + } + } + + @Override + public boolean waitUntilReady() { + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/CommandHelp.java b/src/main/java/tsp/headdb/core/commands/CommandHelp.java new file mode 100644 index 0000000..59a1d73 --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandHelp.java @@ -0,0 +1,52 @@ +package tsp.headdb.core.commands; + +import net.md_5.bungee.api.ChatColor; +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 tsp.headdb.HeadDB; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandHelp extends HDBCommand { + + private TextComponent[] messages; + + public CommandHelp() { + super("help", "Shows the help message.", false, "h", "commands", "cmds"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (messages == null) { + int size = HeadDB.getInstance().getCommandManager().getCommands().size(); + messages = new TextComponent[size]; + int index = 0; + for (HDBCommand command : HeadDB.getInstance().getCommandManager().getCommands()) { + TextComponent component = new TextComponent(ChatColor.GRAY + "/hdb " + ChatColor.DARK_AQUA + command.getName() + " " + ChatColor.DARK_AQUA + command.getParameters() + ChatColor.GRAY + " - " + command.getDescription()); + component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + new Text(ChatColor.GRAY + "Aliases: " + ChatColor.GOLD + String.join(ChatColor.GRAY + " | " + ChatColor.GOLD, command.getAliases())), + new Text(ChatColor.GRAY + "\nPermission: " + ChatColor.RED + "headdb.command." + command.getName()), + new Text("\n"), + new Text(command.isNoConsole() ? ChatColor.RED + "\nThis command CAN NOT be used from console!" : ChatColor.GREEN + "\nThis command CAN be used from console!") + )); + component.setClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/hdb " + command.getName() + " ")); + messages[index] = component; + index++; + } + } + + sender.sendMessage(ChatColor.GRAY + "<==================== [ " + ChatColor.RED + "HeadDB " + ChatColor.GRAY + "|" + ChatColor.DARK_PURPLE + "Commands" + ChatColor.GRAY + "] ====================>"); + sender.sendMessage(ChatColor.GRAY + "Format: /hdb " + ChatColor.DARK_AQUA + " " + ChatColor.RED + " " + ChatColor.GRAY + "- Description"); + sender.sendMessage(ChatColor.GRAY + "Required: " + ChatColor.RED + "<> " + ChatColor.GRAY + "| Optional: " + ChatColor.AQUA + "[]"); + sender.sendMessage(" "); + for (TextComponent message : messages) { + sender.spigot().sendMessage(message); + } + sender.sendMessage(ChatColor.GRAY + "<===============================================================>"); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/CommandInfo.java b/src/main/java/tsp/headdb/core/commands/CommandInfo.java new file mode 100644 index 0000000..25f14ef --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandInfo.java @@ -0,0 +1,31 @@ +package tsp.headdb.core.commands; + +import net.md_5.bungee.api.ChatColor; +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 tsp.headdb.HeadDB; +import tsp.headdb.core.util.Utils; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandInfo extends HDBCommand { + + public CommandInfo() { + super("info", "Show information about the plugin.", false, "i"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + TextComponent component = new TextComponent(ChatColor.GRAY + "Running " + ChatColor.GOLD + "HeadDB - " + HeadDB.getInstance().getDescription().getVersion() + "\n" + ChatColor.GRAY + "GitHub: " + ChatColor.GOLD + "https://github.com/TheSilentPro/HeadDB"); + if (sender.hasPermission("headdb.admin")) { + component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(ChatColor.GOLD + "CLICK TO COPY INFO: " + Utils.getUserAgent()))); + component.setClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, Utils.getUserAgent())); + } + sender.spigot().sendMessage(component); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/CommandManager.java b/src/main/java/tsp/headdb/core/commands/CommandManager.java new file mode 100644 index 0000000..64c0ad6 --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandManager.java @@ -0,0 +1,116 @@ +package tsp.headdb.core.commands; + +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.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.HeadDB; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.core.util.Localization; +import tsp.headdb.core.util.MenuSetup; + +import java.util.*; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandManager implements CommandExecutor, TabCompleter { + + private static final Logger LOGGER = LoggerFactory.getLogger(CommandManager.class); + private final Localization localization = HeadDB.getInstance().getLocalization(); + + private final Set commands = new HashSet<>(); + + public CommandManager init() { + register(new CommandHelp()); + register(new CommandInfo()); + register(new CommandOpen()); + register(new CommandGive()); + register(new CommandSearch()); + register(new CommandUpdate()); + return this; + } + + public void register(HDBCommand command) { + this.commands.add(command); + LOGGER.debug("Registered sub-command: {}", command.getName()); + } + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length > 0) { + for (HDBCommand sub : commands) { + if (sub.matches(args[0])) { + if (!sender.hasPermission("headdb.command." + sub.getName())) { + localization.sendMessage(sender, "noPermission"); + return true; + } + if (sub.isNoConsole() && !(sender instanceof Player)) { + localization.sendMessage(sender, "noConsole"); + return true; + } + if (sub.waitUntilReady() && !HeadAPI.getDatabase().isReady()) { + localization.sendMessage(sender, "notReadyDatabase"); + return true; + } + sub.handle(sender, Arrays.copyOfRange(args, 1, args.length)); + return true; + } + } + + localization.sendMessage(sender, "invalidSubCommand", msg -> msg.replace("%name%", args[0])); + return true; + } + + // No sub command provided, open gui + if (!(sender instanceof Player player)) { + localization.sendMessage(sender, "noConsole"); + return true; + } + + if (!HeadAPI.getDatabase().isReady()) { + localization.sendMessage(sender, "notReadyDatabase"); + return true; + } + + MenuSetup.mainGui.open(player); + localization.sendMessage(player, "openDatabase"); + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (args.length == 0) { + return null; + } + + if (args.length == 1) { + return commands.stream().map(HDBCommand::getName).toList(); + } + + for (HDBCommand sub : commands) { + if (sub.matches(args[0])) { + if (!sender.hasPermission("headdb.command." + sub.getName())) { + return null; + } + if (sub.isNoConsole() && !(sender instanceof Player)) { + return null; + } + + return sub.handleCompletions(sender, Arrays.copyOfRange(args, 1, args.length)); + } + } + + return null; + } + + public Set getCommands() { + return Collections.unmodifiableSet(commands); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/CommandOpen.java b/src/main/java/tsp/headdb/core/commands/CommandOpen.java new file mode 100644 index 0000000..143a1b7 --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandOpen.java @@ -0,0 +1,51 @@ +package tsp.headdb.core.commands; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.HeadDB; +import tsp.headdb.api.model.Category; +import tsp.headdb.core.util.MenuSetup; + +import java.util.Arrays; +import java.util.List; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandOpen extends HDBCommand { + + private final static List categories = Arrays.stream(Category.VALUES).map(Category::getName).toList(); + + public CommandOpen() { + super("open", "[category]", "Open the head menu. Optionally a specific category.", true, "o"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + if (args.length == 0) { + if (!(sender instanceof Player player)) { + localization.sendMessage(sender, "noConsole"); + return; + } + + MenuSetup.mainGui.open(player); + localization.sendMessage(player, "openDatabase"); + return; + } + + Category.getByName(args[0]).ifPresentOrElse(category -> MenuSetup.categoryGuis.get(category).open((Player) sender), + () -> localization.sendMessage(sender, "invalidCategory", msg -> msg.replace("%name%", args[0])) + ); + } + + @Override + public List handleCompletions(CommandSender sender, String[] args) { + return categories; + } + + @Override + public boolean waitUntilReady() { + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/CommandSearch.java b/src/main/java/tsp/headdb/core/commands/CommandSearch.java new file mode 100644 index 0000000..7ded257 --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandSearch.java @@ -0,0 +1,100 @@ +package tsp.headdb.core.commands; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.api.model.Head; +import tsp.headdb.core.util.MenuSetup; +import tsp.headdb.core.util.Utils; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandSearch extends HDBCommand { + + public CommandSearch() { + super("search", "[tags:|contributors:|collections:|dates:|before:|after:] ", "Search the database with possible filters.", true, "s", "find"); + } + + // hdb search + @Override + public void handle(CommandSender sender, String[] args) { + if (args.length < 1) { + localization.sendMessage(sender, "invalidArguments"); + return; + } + + String id = String.join(" ", Arrays.copyOfRange(args, 0, args.length)); + localization.sendMessage(sender, "command.search.wait", msg -> msg.replace("%name%", id)); + + if (id.startsWith("tags:")) { + args[0] = args[0].substring(5); // Remove prefix + HeadAPI.getHeadsByTags(args).thenAcceptAsync(heads -> handle(heads, sender, id.substring(5)), Utils.SYNC); + return; + } + + if (id.startsWith("contributors:")) { + args[0] = args[0].substring(13); // Remove prefix + HeadAPI.getHeadsByContributors(args).thenAcceptAsync(heads -> handle(heads, sender, id.substring(13)), Utils.SYNC); + return; + } + + if (id.startsWith("collections:")) { + args[0] = args[0].substring(12); // Remove prefix + HeadAPI.getHeadsByCollections(args).thenAcceptAsync(heads -> handle(heads, sender, id.substring(12)), Utils.SYNC); + return; + } + + if (id.startsWith("dates:")) { + args[0] = args[0].substring(6); // Remove prefix + HeadAPI.getHeadsByDates(args).thenAcceptAsync(heads -> handle(heads, sender, id.substring(6)), Utils.SYNC); + return; + } + + if (id.startsWith("before:")) { + LocalDate date = LocalDate.parse(id.substring(7), Utils.DATE_FORMATTER); + CompletableFuture.supplyAsync(() -> HeadAPI.getHeadStream().filter(head -> { + if (head.getPublishDate().isEmpty()) { + return false; + } + return LocalDate.parse(head.getPublishDate().get(), Utils.DATE_FORMATTER).isBefore(date); + }).toList()).thenAcceptAsync(heads -> handle(heads, sender, id.substring(7)), Utils.SYNC); + return; + } + + if (id.startsWith("after:")) { + LocalDate date = LocalDate.parse(id.substring(6), Utils.DATE_FORMATTER); + CompletableFuture.supplyAsync(() -> HeadAPI.getHeadStream().filter(head -> { + if (head.getPublishDate().isEmpty()) { + return false; + } + return LocalDate.parse(head.getPublishDate().get(), Utils.DATE_FORMATTER).isAfter(date); + }).toList()).thenAcceptAsync(heads -> handle(heads, sender, id.substring(6)), Utils.SYNC); + return; + } + + HeadAPI.getHeadsByName(id, true).thenAcceptAsync(heads -> handle(heads, sender, id), Utils.SYNC); + } + + private void handle(List heads, CommandSender sender, String id) { + if (heads.isEmpty()) { + localization.sendMessage(sender, "command.search.invalid", msg -> msg.replace("%name%", id)); + return; + } + + localization.sendMessage(sender, "command.search.done", msg -> msg.replace("%name%", id).replace("%size%", String.valueOf(heads.size()))); + MenuSetup.openSearch(heads, id, (Player) sender); + } + + @Override + public boolean waitUntilReady() { + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/CommandUpdate.java b/src/main/java/tsp/headdb/core/commands/CommandUpdate.java new file mode 100644 index 0000000..4c431c4 --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/CommandUpdate.java @@ -0,0 +1,28 @@ +package tsp.headdb.core.commands; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import tsp.headdb.HeadDB; +import tsp.headdb.core.util.Utils; + +/** + * @author TheSilentPro (Silent) + */ +public class CommandUpdate extends HDBCommand { + + public CommandUpdate() { + super("update", "Forcefully start updating the database. " + ChatColor.RED + "(NOT RECOMMENDED)", false, "u"); + } + + @Override + public void handle(CommandSender sender, String[] args) { + localization.sendMessage(sender, "updateStarted"); + HeadDB.getInstance().updateDatabase().thenAcceptAsync(heads -> localization.sendMessage(sender, "updateFinished", msg -> msg.replace("%size%", String.valueOf(heads.size()))), Utils.SYNC); + } + + @Override + public boolean waitUntilReady() { + return true; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/commands/HDBCommand.java b/src/main/java/tsp/headdb/core/commands/HDBCommand.java new file mode 100644 index 0000000..a774941 --- /dev/null +++ b/src/main/java/tsp/headdb/core/commands/HDBCommand.java @@ -0,0 +1,110 @@ +package tsp.headdb.core.commands; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import tsp.headdb.HeadDB; +import tsp.headdb.core.util.Localization; + +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** + * @author TheSilentPro (Silent) + */ +public abstract class HDBCommand { + + private static final Pattern DELIMITER = Pattern.compile(" "); + protected final Localization localization = HeadDB.getInstance().getLocalization(); + + private final String name; + private final String parameters; + private final String[] aliases; + private final String description; + private final boolean noConsole; + + public HDBCommand(String name, String parameters, String description, boolean noConsole, String... aliases) { + this.name = name; + this.aliases = aliases; + this.noConsole = noConsole; + this.description = description; + + if (parameters != null && !parameters.isEmpty()) { + String[] params = DELIMITER.split(parameters); + for (int i = 0; i < params.length; i++) { + String param = params[i]; + if (param.startsWith("[") && param.endsWith("]")) { + params[i] = ChatColor.DARK_AQUA + param; + } else { + params[i] = ChatColor.RED + param; + } + } + this.parameters = String.join(" ", params); + } else { + this.parameters = ""; + } + } + + public HDBCommand(String name, String description, boolean noConsole, String... aliases) { + this(name, null, description, noConsole, aliases); + } + + public boolean waitUntilReady() { + return false; + } + + public boolean matches(String raw) { + if (raw.equalsIgnoreCase(name)) { + return true; + } + + for (String alias : aliases) { + if (raw.equalsIgnoreCase(alias)) { + return true; + } + } + + return false; + } + + /** + * Handle sub command execution. + * Arguments do not include the sub command. + * + * @param sender The sender of the command. + * @param args The arguments. + */ + public abstract void handle(CommandSender sender, String[] args); + + /** + * Handle sub command execution. + * Arguments do not include the sub command. + * + * @param sender The sender of the command. + * @param args The arguments. + */ + public List handleCompletions(CommandSender sender, String[] args) { + return Collections.emptyList(); + } + + public String getName() { + return name; + } + + public String getParameters() { + return parameters; + } + + public String[] getAliases() { + return aliases; + } + + public String getDescription() { + return description; + } + + public boolean isNoConsole() { + return noConsole; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/config/ConfigData.java b/src/main/java/tsp/headdb/core/config/ConfigData.java new file mode 100644 index 0000000..2e897c9 --- /dev/null +++ b/src/main/java/tsp/headdb/core/config/ConfigData.java @@ -0,0 +1,115 @@ +package tsp.headdb.core.config; + +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.api.model.Category; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.api.model.Head; + +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +/** + * @author TheSilentPro (Silent) + */ +public class ConfigData { + + private static final Logger LOGGER = LoggerFactory.getLogger(ConfigData.class); + + private boolean requireCategoryPermission; + private boolean preloadHeads; + private boolean includeLore; + private boolean moreInfo; + + private boolean economyEnabled; + private String economyProvider; + private double defaultCost; + private double categoryDefaultCost; + private double localCost; + private final Map categoryCosts = new HashMap<>(); + private final Map costs = new HashMap<>(); + + public ConfigData(FileConfiguration config) { + reload(config); + } + + public void reload(FileConfiguration config) { + this.requireCategoryPermission = config.getBoolean("requireCategoryPermissions"); + this.preloadHeads = config.getBoolean("preloadHeads"); + this.includeLore = config.getBoolean("includeLore"); + this.moreInfo = config.getBoolean("moreInfo"); + + this.economyEnabled = config.getBoolean("economy.enabled"); + this.economyProvider = config.getString("economy.provider"); + this.defaultCost = config.getDouble("economy.defaultCost"); + this.categoryDefaultCost = config.getDouble("economy.defaultCategoryCost"); + this.localCost = config.getDouble("economy.localCost"); + + for (Category category : Category.VALUES) { + categoryCosts.put(category.getName(), config.getDouble("economy.categoryCost." + category.getName())); // Do not put default category cost as default value for this! + } + + if (this.economyEnabled) { + ConfigurationSection costSection = config.getConfigurationSection("economy.cost"); + if (costSection != null) { + for (String key : costSection.getKeys(false)) { + try { + HeadAPI.getHeadById(Integer.parseInt(key)).join().ifPresentOrElse(head -> costs.put(head, config.getDouble("economy.cost." + key)), () -> LOGGER.warn("Failed to find head with id: {}", key)); + } catch (NumberFormatException nfe) { + HeadAPI.getHeadByTexture(key).join().ifPresentOrElse(head -> costs.put(head, config.getDouble("economy.cost." + key)), () -> LOGGER.warn("Failed to find head with texture: {}", key)); + } + } + } else { + LOGGER.warn("No cost section defined in the configuration."); + } + } + } + + public boolean isEconomyEnabled() { + return economyEnabled; + } + + public String getEconomyProvider() { + return economyProvider; + } + + public double getLocalCost() { + return localCost; + } + + public double getDefaultCost() { + return defaultCost; + } + + public double getCategoryDefaultCost() { + return categoryDefaultCost; + } + + public Map getCategoryCosts() { + return categoryCosts; + } + + public Map getCosts() { + return costs; + } + + public boolean shouldPreloadHeads() { + return preloadHeads; + } + + public boolean shouldRequireCategoryPermission() { + return requireCategoryPermission; + } + + public boolean shouldIncludeLore() { + return includeLore; + } + + public boolean shouldIncludeMoreInfo() { + return moreInfo; + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/economy/BasicEconomyProvider.java b/src/main/java/tsp/headdb/core/economy/BasicEconomyProvider.java deleted file mode 100644 index 8fcc7c7..0000000 --- a/src/main/java/tsp/headdb/core/economy/BasicEconomyProvider.java +++ /dev/null @@ -1,20 +0,0 @@ -package tsp.headdb.core.economy; - -import org.bukkit.entity.Player; -import tsp.helperlite.scheduler.promise.Promise; - -import java.math.BigDecimal; - -public interface BasicEconomyProvider { - - Promise canPurchase(Player player, BigDecimal cost); - - Promise withdraw(Player player, BigDecimal amount); - - default Promise purchase(Player player, BigDecimal amount) { - return canPurchase(player, amount).thenComposeAsync(result -> result ? withdraw(player, amount) : Promise.completed(false)); - } - - void init(); - -} diff --git a/src/main/java/tsp/headdb/core/economy/EconomyProvider.java b/src/main/java/tsp/headdb/core/economy/EconomyProvider.java new file mode 100644 index 0000000..a590503 --- /dev/null +++ b/src/main/java/tsp/headdb/core/economy/EconomyProvider.java @@ -0,0 +1,28 @@ +package tsp.headdb.core.economy; + +import org.bukkit.entity.Player; + +import java.util.concurrent.CompletableFuture; + +/** + * @author TheSilentPro (Silent) + */ +public interface EconomyProvider { + + void init(); + + CompletableFuture canAfford(Player player, double amount); + + CompletableFuture withdraw(Player player, double amount); + + default CompletableFuture purchase(Player player, double cost) { + return canAfford(player, cost).thenCompose(afforded -> { + if (!afforded) { + return CompletableFuture.completedFuture(false); + } else { + return withdraw(player, cost); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/economy/VaultProvider.java b/src/main/java/tsp/headdb/core/economy/VaultProvider.java index 76f03a7..8080086 100644 --- a/src/main/java/tsp/headdb/core/economy/VaultProvider.java +++ b/src/main/java/tsp/headdb/core/economy/VaultProvider.java @@ -4,42 +4,43 @@ 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 tsp.helperlite.scheduler.promise.Promise; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.math.BigDecimal; +import java.util.concurrent.CompletableFuture; -public class VaultProvider implements BasicEconomyProvider { +/** + * @author TheSilentPro (Silent) + */ +public class VaultProvider implements EconomyProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(VaultProvider.class); private Economy economy; - @Override - public Promise canPurchase(Player player, BigDecimal cost) { - double effectiveCost = cost.doubleValue(); - return Promise.supplyingAsync(() -> economy.has(player, effectiveCost >= 0 ? effectiveCost : 0)); - } - - @Override - public Promise withdraw(Player player, BigDecimal amount) { - double effectiveCost = amount.doubleValue(); - return Promise.supplyingAsync(() -> economy.withdrawPlayer(player, effectiveCost >= 0 ? effectiveCost : 0).transactionSuccess()); - } - - @Override public void init() { if (Bukkit.getServer().getPluginManager().getPlugin("Vault") == null) { - HeadDB.getInstance().getLog().error("Vault is not installed!"); + LOGGER.error("Vault is not installed but is enabled in the config.yml!"); return; } RegisteredServiceProvider economyProvider = Bukkit.getServer().getServicesManager().getRegistration(net.milkbowl.vault.economy.Economy.class); if (economyProvider == null) { - HeadDB.getInstance().getLog().error("Could not find vault economy provider!"); + LOGGER.error("Could not find vault economy provider!"); return; } - economy = economyProvider.getProvider(); + this.economy = economyProvider.getProvider(); } -} + @Override + public CompletableFuture canAfford(Player player, double amount) { + return CompletableFuture.supplyAsync(() -> economy.has(player, Math.max(0, amount))); + } + + @Override + public CompletableFuture withdraw(Player player, double amount) { + return CompletableFuture.supplyAsync(() -> economy.withdrawPlayer(player, Math.max(0, amount)).transactionSuccess()); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/hook/Hooks.java b/src/main/java/tsp/headdb/core/hook/Hooks.java deleted file mode 100644 index dc72c32..0000000 --- a/src/main/java/tsp/headdb/core/hook/Hooks.java +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index bb224f4..0000000 --- a/src/main/java/tsp/headdb/core/hook/PluginHook.java +++ /dev/null @@ -1,3 +0,0 @@ -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/player/PlayerData.java b/src/main/java/tsp/headdb/core/player/PlayerData.java new file mode 100644 index 0000000..4f02437 --- /dev/null +++ b/src/main/java/tsp/headdb/core/player/PlayerData.java @@ -0,0 +1,65 @@ +package tsp.headdb.core.player; + +import java.util.*; + +/** + * @author TheSilentPro (Silent) + */ +public class PlayerData { + + private final int id; + private final UUID uuid; + private String lang; + private boolean soundEnabled; + private final Set favorites; + + public PlayerData(int id, UUID uuid, String lang, boolean soundEnabled, int... favorites) { + this.id = id; + this.uuid = uuid; + this.lang = lang != null ? lang : "en"; + this.soundEnabled = soundEnabled; + this.favorites = new HashSet<>(); + if (favorites != null) { + for (int favorite : favorites) { + this.favorites.add(favorite); + } + } + } + + public int getId() { + return id; + } + + public UUID getUuid() { + return uuid; + } + + public String getLang() { + return lang; + } + + public boolean isSoundEnabled() { + return soundEnabled; + } + + public void setSoundEnabled(boolean soundEnabled) { + this.soundEnabled = soundEnabled; + } + + public void setLang(String lang) { + this.lang = lang != null ? lang : "en"; + } + + public Set getFavorites() { + return favorites; + } + + public boolean addFavorite(int favorite) { + return this.favorites.add(favorite); + } + + public boolean removeFavorite(int favorite) { + return this.favorites.remove(favorite); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/player/PlayerDatabase.java b/src/main/java/tsp/headdb/core/player/PlayerDatabase.java new file mode 100644 index 0000000..7ae4799 --- /dev/null +++ b/src/main/java/tsp/headdb/core/player/PlayerDatabase.java @@ -0,0 +1,85 @@ +package tsp.headdb.core.player; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.HeadDB; + +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * @author TheSilentPro (Silent) + */ +public class PlayerDatabase { + + private static final Logger LOGGER = LoggerFactory.getLogger(PlayerDatabase.class); + + private static final Pattern FAVORITES_DELIMITER = Pattern.compile("\\|"); + private final ConcurrentHashMap data = new ConcurrentHashMap<>(); + + public PlayerData getOrCreate(UUID uuid) { + PlayerData data = this.data.get(uuid); + if (data == null) { + data = new PlayerData(this.data + .values() + .stream() + .mapToInt(PlayerData::getId) + .max() + .orElse(0) + 1, + uuid, + "en", + true); + this.data.put(uuid, data); + } + + return data; + } + + private PlayerData register(int id, UUID uuid, String lang, boolean enableSounds, int... favorites) { + if (!this.data.containsKey(uuid)) { + this.data.put(uuid, new PlayerData(id, uuid, lang, enableSounds, favorites)); + } + return this.data.get(uuid); + } + + public void save() { + HeadDB.getInstance().getStorage().insertAllPlayers(data).whenComplete((result, ex) -> { + if (ex != null) { + LOGGER.error("Failed to save all players!", ex); + return; + } + LOGGER.info("Successfully saved all players to the database."); + }); + } + + public void load() { + HeadDB.getInstance().getStorage().selectPlayers().whenComplete((resultSet, ex) -> { + if (ex != null) { + LOGGER.error("Failed to load players!", ex); + return; + } + + try { + int count = 0; + while (resultSet.next()) { + count++; + UUID uuid = UUID.fromString(resultSet.getString("uuid")); + int[] favorites = Arrays.stream(FAVORITES_DELIMITER.split(resultSet.getString("favorites"))) + .mapToInt(Integer::parseInt) + .toArray(); + this.data.put(uuid, register(resultSet.getInt("id"), uuid, resultSet.getString("lang"), resultSet.getBoolean("soundEnabled"), favorites)); + } + LOGGER.info("Loaded {} players!", count); + } catch (SQLException sqlex) { + LOGGER.error("Failed to iterate players!", sqlex); + } + }); + } + + public Map getData() { + return Collections.unmodifiableMap(data); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/storage/PlayerData.java b/src/main/java/tsp/headdb/core/storage/PlayerData.java deleted file mode 100644 index 83953a5..0000000 --- a/src/main/java/tsp/headdb/core/storage/PlayerData.java +++ /dev/null @@ -1,7 +0,0 @@ -package tsp.headdb.core.storage; - -import java.io.Serializable; -import java.util.Set; -import java.util.UUID; - -public record PlayerData(UUID uniqueId, Set favorites) implements Serializable {} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/storage/PlayerStorage.java b/src/main/java/tsp/headdb/core/storage/PlayerStorage.java deleted file mode 100644 index 155b8fe..0000000 --- a/src/main/java/tsp/headdb/core/storage/PlayerStorage.java +++ /dev/null @@ -1,73 +0,0 @@ -package tsp.headdb.core.storage; - -import tsp.headdb.HeadDB; -import tsp.warehouse.storage.file.SerializableFileDataManager; - -import java.io.File; -import java.util.*; - -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.orElse(new HashSet<>())) { - 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/SQLStatement.java b/src/main/java/tsp/headdb/core/storage/SQLStatement.java new file mode 100644 index 0000000..3ddb1cc --- /dev/null +++ b/src/main/java/tsp/headdb/core/storage/SQLStatement.java @@ -0,0 +1,103 @@ +package tsp.headdb.core.storage; + +import tsp.headdb.core.player.PlayerData; + +import java.io.IOException; +import java.sql.*; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author TheSilentPro (Silent) + */ +public enum SQLStatement { + + TABLES_CREATE( + """ + CREATE TABLE IF NOT EXISTS hdb_players ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uuid UUID UNIQUE NOT NULL, + lang VARCHAR(8), + soundEnabled BOOLEAN, + favorites TEXT + ); + CREATE TABLE IF NOT EXISTS hdb_heads ( + id INTEGER UNIQUE PRIMARY KEY NOT NULL, -- numeric id of the head + texture VARCHAR(255), -- texture + unique_id UUID NOT NULL, -- Unique UUID of the head + name VARCHAR(60) NOT NULL, -- Name of the head + tags VARCHAR(255), -- Tags for the head (optional) + category VARCHAR(255) -- Category of the head (optional) + ); + """ + ), + + SELECT_PLAYERS("SELECT * FROM hdb_players;"), + INSERT_PLAYER( + """ + INSERT INTO hdb_players(id, uuid, lang, soundEnabled, favorites) + VALUES(?, ?, ?, ?, ?) + ON CONFLICT(uuid) DO UPDATE SET + id = COALESCE(EXCLUDED.id, hdb_players.id), + lang = COALESCE(EXCLUDED.lang, hdb_players.lang), + soundEnabled = COALESCE(EXCLUDED.soundEnabled, hdb_players.soundEnabled), + favorites = COALESCE(EXCLUDED.favorites, hdb_players.favorites); + """, + Types.VARCHAR, Types.VARCHAR, Types.BOOLEAN, Types.VARCHAR + ); + + private final String statement; + private final int[] types; + + SQLStatement(String statement, int... types) { + this.statement = statement; + this.types = types; + } + + SQLStatement(String statement) { + this(statement, (int[]) null); + } + + public int executeUpdate(Connection connection) throws SQLException, IOException { + return connection.createStatement().executeUpdate(statement); + } + + public int executePreparedUpdate(Connection connection, Object... parameters) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(statement)) { + if (parameters.length != types.length) { + throw new IllegalArgumentException("Number of parameters does not match the number of placeholders."); + } + + for (int i = 0; i < parameters.length; i++) { + preparedStatement.setObject(i + 1, parameters[i], types[i]); + } + + return preparedStatement.executeUpdate(); + } + } + + public ResultSet executeQuery(Connection connection) throws SQLException { + return connection.createStatement().executeQuery(statement); + } + + public int executePreparedBatch(Connection connection, Map players) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(statement)) { + for (Map.Entry entry : players.entrySet()) { + PlayerData player = entry.getValue(); + + preparedStatement.setInt(1, entry.getValue().getId()); + preparedStatement.setString(2, entry.getKey().toString()); + preparedStatement.setString(3, player.getLang()); + preparedStatement.setBoolean(4, player.isSoundEnabled()); + preparedStatement.setString(5, player.getFavorites().stream().map(String::valueOf).collect(Collectors.joining("|"))); + + preparedStatement.addBatch(); + } + + int[] results = preparedStatement.executeBatch(); + return results.length; + } + } + +} \ 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 index 91d4349..e08f6f5 100644 --- a/src/main/java/tsp/headdb/core/storage/Storage.java +++ b/src/main/java/tsp/headdb/core/storage/Storage.java @@ -1,33 +1,117 @@ package tsp.headdb.core.storage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import tsp.headdb.HeadDB; -import tsp.helperlite.Schedulers; +import tsp.headdb.core.player.PlayerData; -import java.io.File; -import java.util.concurrent.Executor; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +/** + * @author TheSilentPro (Silent) + */ public class Storage { - private final Executor executor; - private final PlayerStorage playerStorage; + private static final Logger LOGGER = LoggerFactory.getLogger(Storage.class); + private final ExecutorService executor; + private Connection connection; public Storage() { - executor = Schedulers.async(); - validateDataDirectory(); - playerStorage = new PlayerStorage(HeadDB.getInstance(), this); + this.executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "HeadDB Storage")); } - public PlayerStorage getPlayerStorage() { - return playerStorage; + public Storage init() { + createTables(); + return this; } - public Executor getExecutor() { - return executor; + public CompletableFuture selectPlayers() { + return connect().thenComposeAsync(conn -> { + try { + LOGGER.debug("Fetching players from database..."); + return CompletableFuture.completedFuture(SQLStatement.SELECT_PLAYERS.executeQuery(conn)); + } catch (SQLException ex) { + LOGGER.error("Failed to select players!", ex); + return CompletableFuture.failedFuture(ex); + } finally { + disconnect(); + } + }, executor); } - private void validateDataDirectory() { - //noinspection ResultOfMethodCallIgnored - new File(HeadDB.getInstance().getDataFolder(), "data").mkdir(); + public CompletableFuture insertPlayer(UUID id, String lang, boolean soundEnabled, String favorites) { + return connect().thenAcceptAsync(conn -> { + try { + int res = SQLStatement.INSERT_PLAYER.executeUpdate(conn); + LOGGER.debug("[INSERT]: RESPONSE={} | ID={} | LANG={} | sound={} | FAVORITES={}", res, id, lang, soundEnabled, favorites.substring(0, 16)); + } catch (SQLException | IOException ex) { + LOGGER.error("Failed to create tables!", ex); + } finally { + disconnect(); + } + }, executor); } -} + public CompletableFuture insertAllPlayers(Map players) { + return connect().thenAcceptAsync(conn -> { + try { + int inserted = SQLStatement.INSERT_PLAYER.executePreparedBatch(conn, players); + LOGGER.debug("[INSERT | ALL]: IN={} | OUT={}", players.size(), inserted); + } catch (SQLException ex) { + LOGGER.error("Failed to insert all players!", ex); + } finally { + disconnect(); + } + }, executor); + } + + public CompletableFuture createTables() { + return connect().thenAcceptAsync(conn -> { + try { + int res = SQLStatement.TABLES_CREATE.executeUpdate(conn); + LOGGER.debug("[CREATE]: RES={}", res); + } catch (SQLException | IOException ex) { + LOGGER.error("Failed to create tables!", ex); + } finally { + disconnect(); + } + }, executor); + } + + public CompletableFuture connect() { + return CompletableFuture.supplyAsync(() -> { + try { + this.connection = DriverManager.getConnection("jdbc:sqlite:" + HeadDB.getInstance().getDataFolder() + "/data.db"); + LOGGER.debug("SQL connection established!"); + return this.connection; + } catch (SQLException ex) { + LOGGER.error("Failed to connect to database!", ex); + throw new CompletionException("Could not connect to database!", ex); + } + }, executor); + } + + public CompletableFuture disconnect() { + return CompletableFuture.runAsync(() -> { + try { + if (this.connection != null && !this.connection.isClosed()) { + this.connection.close(); + LOGGER.debug("SQL connection terminated!"); + } + } catch (SQLException ex) { + LOGGER.error("Failed to close connection!", ex); + throw new CompletionException("Could not close database connection", ex); + } + }, executor); + } +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/task/UpdateTask.java b/src/main/java/tsp/headdb/core/task/UpdateTask.java deleted file mode 100644 index cb9db86..0000000 --- a/src/main/java/tsp/headdb/core/task/UpdateTask.java +++ /dev/null @@ -1,47 +0,0 @@ -package tsp.headdb.core.task; - -import org.bukkit.Bukkit; -import tsp.headdb.HeadDB; -import tsp.headdb.core.api.HeadAPI; -import tsp.headdb.core.api.events.AsyncHeadsFetchedEvent; -import tsp.headdb.implementation.category.Category; -import tsp.headdb.implementation.head.Head; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -public class UpdateTask implements Runnable { - - @Override - public void run() { - HeadAPI.getDatabase().update().thenAcceptAsync(result -> { - HeadDB instance = HeadDB.getInstance(); - String providerName = HeadAPI.getDatabase().getRequester().getProvider().name(); - - instance.getLog().debug("Fetched: " + getHeadsCount(result.heads()) + " Heads | Provider: " + providerName + " | Time: " + result.elapsed() + "ms (" + TimeUnit.MILLISECONDS.toSeconds(result.elapsed()) + "s)"); - Bukkit.getPluginManager().callEvent( - new AsyncHeadsFetchedEvent( - result.heads(), - providerName, - result.elapsed())); - - instance.getStorage().getPlayerStorage().backup(); - instance.getUpdateTask().ifPresentOrElse(task -> { - instance.getLog().debug("UpdateTask completed! Times ran: " + task.getTimesRan()); - }, () -> instance.getLog().debug("Initial UpdateTask completed!")); - }); - } - - private int getHeadsCount(Map> heads) { - int n = 0; - for (List list : heads.values()) { - for (int i = 0; i < list.size(); i++) { - n++; - } - } - - return n; - } - -} diff --git a/src/main/java/tsp/headdb/core/util/HeadDBLogger.java b/src/main/java/tsp/headdb/core/util/HeadDBLogger.java deleted file mode 100644 index 164cb36..0000000 --- a/src/main/java/tsp/headdb/core/util/HeadDBLogger.java +++ /dev/null @@ -1,12 +0,0 @@ -package tsp.headdb.core.util; - -import tsp.nexuslib.logger.NexusLogger; - -@SuppressWarnings("unused") -public class HeadDBLogger extends NexusLogger { - - public HeadDBLogger(boolean debug) { - super("HeadDB", debug); - } - -} diff --git a/src/main/java/tsp/headdb/core/util/Localization.java b/src/main/java/tsp/headdb/core/util/Localization.java new file mode 100644 index 0000000..45ef202 --- /dev/null +++ b/src/main/java/tsp/headdb/core/util/Localization.java @@ -0,0 +1,463 @@ +package tsp.headdb.core.util; + +import org.bukkit.Bukkit; +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.command.RemoteConsoleCommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.plugin.java.JavaPlugin; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import javax.annotation.ParametersAreNonnullByDefault; + +import java.io.File; +import java.io.IOException; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Paths; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.function.UnaryOperator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import me.clip.placeholderapi.PlaceholderAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import tsp.headdb.HeadDB; + +/** + * Translatable Localization Utility Class. + * Gist (Source) + * + * @author TheSilentPro (Silent) + */ +@ParametersAreNonnullByDefault +public class Localization { + + private static final Logger LOGGER = LoggerFactory.getLogger(Localization.class); + + private final JavaPlugin plugin; + private final String messagesPath; + private final File container; + private final String defaultLanguage; + private final Map data; // Lang, Data + private String consoleLanguage; + private boolean colorize; + + + @SuppressWarnings("RegExpRedundantEscape") + private final Pattern ARGS_PATTERN = Pattern.compile("(\\{\\$arg(\\d+)\\})", Pattern.CASE_INSENSITIVE); // (\{\$arg(\d+)\}) + + /** + * Creates a new {@link Localization} instance. + * + * @param plugin The plugin associated with this instance. + * @param messagesPath The path for the messages in the /resources directory. + * @param container The container for the messages. Default: {pluginDataFolder}/messages + * @param defaultLanguage The default language file in your /resources directory. Default: en + */ + public Localization(JavaPlugin plugin, String messagesPath, @Nullable File container, @Nullable String defaultLanguage) { + notNull(plugin, "Plugin can not be null!"); + notNull(messagesPath, "Messages path can not be null!"); + + this.plugin = plugin; + this.messagesPath = messagesPath; + this.container = container; + this.defaultLanguage = defaultLanguage; + //this.languages = new HashMap<>(); + this.data = new HashMap<>(); + this.consoleLanguage = defaultLanguage; + this.colorize = true; + } + + /** + * Create a new {@link Localization} instance with default parameters. + * Notice: If you use this constructor make sure to FIRST create your {@link JavaPlugin#getDataFolder() plugins data folder} before calling the constructor! + * + * @param plugin The plugin associated with this instance. + * @param messagesPath The path for the messages in the /resources directory. + */ + public Localization(JavaPlugin plugin, String messagesPath) { + this(plugin, messagesPath, new File(plugin.getDataFolder() + "/messages"), "en"); + } + + // Non-Console + + /** + * Send message to the receiver. + * + * @param uuid The receiver. + * @param message The message. + * @see #sendMessage(UUID, String) + */ + private void sendTranslatedMessage(UUID uuid, String message) { + notNull(uuid, "UUID can not be null!"); + notNull(message, "Message can not be null!"); + if (message.isEmpty()) { + return; + } + + Entity receiver = Bukkit.getEntity(uuid); + if (receiver == null) { + //noinspection UnnecessaryToStringCall + throw new IllegalArgumentException("Invalid receiver with uuid: " + uuid.toString()); + } + + receiver.sendMessage(message); + } + + /** + * Send a message to a receiver. + * + * @param receiver The receiver of the message. + * @param key The key of the message. + * @param function Optional: Function to apply to the message. + * @param args Optional: Arguments for replacing. Format: {$argX} where X can be any argument number starting from 0. + */ + public void sendMessage(UUID receiver, String key, @Nullable UnaryOperator function, @Nullable String... args) { + notNull(receiver, "Receiver can not be null!"); + notNull(key, "Key can not be null!"); + + getMessage(receiver, key).ifPresent(message -> { + if (args != null) { + for (String arg : args) { + if (arg != null) { + Matcher matcher = ARGS_PATTERN.matcher(message); + while (matcher.find()) { + message = matcher.replaceAll(args[Integer.parseInt(matcher.group(2))]); + } + } + } + } + + // Apply function + message = function != null ? function.apply(message) : message; + sendTranslatedMessage(receiver, colorize ? ChatColor.translateAlternateColorCodes('&', message) : message); + }); + } + + public void sendMessage(UUID receiver, String key, @Nullable UnaryOperator function) { + sendMessage(receiver, key, function, (String[]) null); + } + + public void sendMessage(UUID receiver, String key, @Nullable String... args) { + sendMessage(receiver, key, null, args); + } + + public void sendMessage(UUID receiver, String key) { + sendMessage(receiver, key, null, (String[]) null); + } + + public void sendMessages(String key, UUID... receivers) { + for (UUID receiver : receivers) { + sendMessage(receiver, key); + } + } + + /** + * Retrieve a message by the receiver's language and the key. + * + * @param uuid The receiver. + * @param key The message key. + * @return If present, the message, otherwise an empty {@link Optional} + */ + @NotNull + public Optional getMessage(UUID uuid, String key) { + notNull(uuid, "UUID can not be null!"); + notNull(key, "Key can not be null!"); + + FileConfiguration messages = data.get(HeadDB.getInstance().getPlayerDatabase().getOrCreate(uuid).getLang()); + if (messages == null) { + return Optional.empty(); + } + + String message = messages.getString(key); + if (message == null) { + // Message not specified in language file, attempt to find it in the main one. + messages = data.get(defaultLanguage); + message = messages.getString(key); + } + + if (Bukkit.getPluginManager().getPlugin("PlaceholderAPI") != null && message != null) { + message = PlaceholderAPI.setPlaceholders(Bukkit.getOfflinePlayer(uuid), message); + } + + return Optional.ofNullable(message); + } + + // Console + + private ConsoleMessageFunction logFunction = message -> Bukkit.getConsoleSender().sendMessage(message); + + public void sendTranslatedConsoleMessage(String message) { + notNull(message, "Message can not be null!"); + if (message.isEmpty()) { + return; + } + + logFunction.logMessage(message); + } + + public void sendConsoleMessage(String key, @Nullable UnaryOperator function, @Nullable String... args) { + notNull(key, "Key can not be null!"); + + getConsoleMessage(key).ifPresent(message -> { + if (args != null) { + for (String arg : args) { + if (arg != null) { + Matcher matcher = ARGS_PATTERN.matcher(message); + while (matcher.find()) { + message = matcher.replaceAll(args[Integer.parseInt(matcher.group(2))]); + } + } + } + } + + // Apply function + message = function != null ? function.apply(message) : message; + sendTranslatedConsoleMessage(colorize ? ChatColor.translateAlternateColorCodes('&', message) : message); + }); + } + + public void sendConsoleMessage(String key, @Nullable UnaryOperator function) { + sendConsoleMessage(key, function, (String[]) null); + } + + public void sendConsoleMessage(String key, @Nullable String... args) { + sendConsoleMessage(key, null, args); + } + + public void sendConsoleMessage(String key) { + sendConsoleMessage(key, null, (String[]) null); + } + + public Optional getConsoleMessage(String key) { + notNull(key, "Key can not be null!"); + + FileConfiguration messages = data.get(consoleLanguage != null ? consoleLanguage : defaultLanguage); + if (messages == null) { + return Optional.empty(); + } + + String message = messages.getString(key); + if (message == null) { + // Message not specified in language file, attempt to find it in the main one. + messages = data.get(defaultLanguage); + message = messages.getString(key); + } + + return Optional.ofNullable(message); + } + + public interface ConsoleMessageFunction { + + void logMessage(String message); + + } + + public void setLogFunction(ConsoleMessageFunction logFunction) { + this.logFunction = logFunction; + } + + // Auto Resolve + + public void sendMessage(CommandSender receiver, String key, @Nullable UnaryOperator function, @Nullable String... args) { + if (receiver instanceof ConsoleCommandSender || receiver instanceof RemoteConsoleCommandSender) { + sendConsoleMessage(key, function, args); + } else if (receiver instanceof Player player) { + sendMessage(player.getUniqueId(), key, function, args); + } + } + + public void sendMessage(CommandSender receiver, String key, @Nullable UnaryOperator function) { + sendMessage(receiver, key, function, (String[]) null); + } + + public void sendMessage(CommandSender receiver, String key) { + sendMessage(receiver, key, null); + } + + public void sendMessage(String key, CommandSender... receivers) { + for (CommandSender receiver : receivers) { + sendMessage(receiver, key); + } + } + + // Loader + + /** + * Load all language files. + * + * @return Number of files loaded. + */ + public int load() { + File[] files = container.listFiles(); + if (files == null) { + throw new NullPointerException("No files in container!"); + } + + int count = 0; + for (File file : files) { + String name = file.getName(); + // If the file is not of YAML type, ignore it. + if (name.endsWith(".yml") || name.endsWith(".yaml")) { + data.put(name.substring(0, name.lastIndexOf(".")), YamlConfiguration.loadConfiguration(file)); + count++; + } + } + + return count; + } + + /** + * Create the default language files from your /resources folder. + * + * @throws URISyntaxException URI Syntax Error + * @throws IOException Error + */ + public void createDefaults() throws URISyntaxException, IOException { + URL url = plugin.getClass().getClassLoader().getResource(messagesPath); + if (url == null) { + throw new NullPointerException("No resource with path: " + messagesPath); + } + + if (!container.exists()) { + //noinspection ResultOfMethodCallIgnored + container.mkdir(); + } + + // This is required otherwise Files.walk will throw FileSystem Exception. + String[] array = url.toURI().toString().split("!"); + FileSystem fs = FileSystems.newFileSystem(URI.create(array[0]), new HashMap<>()); + + //noinspection resource + Files.walk(Paths.get(url.toURI())) + .forEach(path -> { + try { + File out = new File(container.getAbsolutePath() + "/" + path.getFileName()); + // If file is not of YAML type or if it already exists, ignore it. + if ((out.getName().endsWith(".yml") || out.getName().endsWith(".yaml")) && !out.exists()) { + Files.copy(path, out.toPath()); + } + } catch (IOException ex) { + LOGGER.error("Could not create file!", ex); + } + }); + + fs.close(); + } + + /** + * Retrieve the {@link JavaPlugin plugin} associated with this {@link Localization}. + * + * @return The plugin. + */ + @NotNull + public JavaPlugin getPlugin() { + return plugin; + } + + /** + * Retrieve the {@link File container} for the language files. + * + * @return The container. + */ + @NotNull + public File getContainer() { + return container; + } + + /** + * Retrieve the message file path. + * + * @return The message file path. + */ + @NotNull + public String getMessagesPath() { + return messagesPath; + } + + /** + * Retrieve a {@link Map} containing all language/message data. + * + * @return The language/message data. Format: Language, Messages + */ + public Map getData() { + return data; + } + + /** + * Retrieve the default language. + * + * @return The default language. + */ + @NotNull + public String getDefaultLanguage() { + return defaultLanguage; + } + + /** + * Retrieve whether messages are colorized before sending. + * + * @return Colorize option value + */ + public boolean isColorize() { + return colorize; + } + + /** + * Set whether messages should be colorized before sending. + * + * @param colorize Colorize option + */ + public void setColorize(boolean colorize) { + this.colorize = colorize; + } + + /** + * Retrieve the language for the console. + * + * @return The consoles' language. Default: Default Language + */ + @NotNull + public String getConsoleLanguage() { + return consoleLanguage; + } + + /** + * Set the language for the console. + * + * @param consoleLanguage The consoles' language. + */ + public void setConsoleLanguage(String consoleLanguage) { + notNull(consoleLanguage, "Language can not be null!"); + this.consoleLanguage = consoleLanguage; + } + + /** + * Just a not-null validator. + * + * @param object The object to validate. + * @param message The message sent if validation fails. + * @param Type + */ + private void notNull(T object, String message) { + //noinspection ConstantConditions + if (object == null) throw new NullPointerException(message); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/util/MenuSetup.java b/src/main/java/tsp/headdb/core/util/MenuSetup.java new file mode 100644 index 0000000..b6cad37 --- /dev/null +++ b/src/main/java/tsp/headdb/core/util/MenuSetup.java @@ -0,0 +1,303 @@ +package tsp.headdb.core.util; + +import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import tsp.headdb.HeadDB; +import tsp.headdb.api.model.Category; +import tsp.headdb.api.HeadAPI; +import tsp.headdb.api.model.Head; +import tsp.headdb.api.model.LocalHead; +import tsp.headdb.core.player.PlayerData; +import tsp.invlib.gui.GUI; +import tsp.invlib.gui.SimpleGUI; +import tsp.invlib.gui.button.SimpleButton; +import tsp.invlib.gui.page.PageBuilder; + +import java.util.*; + +/** + * @author TheSilentPro (Silent) + */ +@SuppressWarnings("DataFlowIssue") +public final class MenuSetup { + + public static final GUI mainGui = new SimpleGUI(); + public static final GUI allGui = new SimpleGUI(); + public static final Map categoryGuis = new HashMap<>(); + + private static final Localization localization = HeadDB.getInstance().getLocalization(); + private static final ItemStack FILLER; + private static final boolean categoryPermission = HeadDB.getInstance().getCfg().shouldRequireCategoryPermission(); + + static { + FILLER = new ItemStack(Material.BLACK_STAINED_GLASS_PANE); + ItemMeta fillerMeta = FILLER.getItemMeta(); + //noinspection DataFlowIssue + fillerMeta.setDisplayName(" "); + FILLER.setItemMeta(fillerMeta); + + // Prebuild main page + PageBuilder builder = new PageBuilder(mainGui) + .fill(new SimpleButton(FILLER, e -> e.setCancelled(true))) + .name(ChatColor.RED + "HeadDB" + ChatColor.DARK_GRAY + " (" + HeadAPI.getTotalHeads() + ")") + .onClick(event -> { + // Compare material type instead of ItemStack because performance. + if (event.getCurrentItem().getType() != FILLER.getType()) { + Sounds.PAGE_OPEN.play((Player) event.getWhoClicked()); + } + }) + .preventClick(); + + // Settings button + + + // Set all heads button in main page + ItemStack allItem = new ItemStack(Material.BOOK); + ItemMeta meta = allItem.getItemMeta(); + meta.setItemName(ChatColor.GOLD + "All Heads"); + meta.setLore(Collections.singletonList(ChatColor.GRAY + "Total heads » " + ChatColor.GOLD + HeadAPI.getTotalHeads())); + allItem.setItemMeta(meta); + builder.button(40, new SimpleButton(allItem, e -> { + Player player = (Player) e.getWhoClicked(); + if (!player.hasPermission("headdb.category.all") && !player.hasPermission("headdb.category.*")) { + localization.sendMessage(player, "noPermission"); + return; + } + + if (e.isLeftClick()) { + allGui.open(player); + } else if (e.isRightClick()) { + // todo: input specific page + } + })); + + // Set category buttons in main page + for (Category category : Category.VALUES) { + builder.button(category.getDefaultSlot(), new SimpleButton(category.getDisplayItem(), e -> { + e.setCancelled(true); + if (e.isLeftClick()) { + categoryGuis.get(category).open((Player) e.getWhoClicked()); + } else if (e.isRightClick()) { + // todo: input specific page + } + })); + } + + // Set Favorites button in main page + ItemStack favoritesHeadsItem = HeadAPI.getHeadByTexture("eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzZmZGQ0YjEzZDU0ZjZjOTFkZDVmYTc2NWVjOTNkZDk0NThiMTlmOGFhMzRlZWI1YzgwZjQ1NWIxMTlmMjc4In19fQ==") + .join() + .map(Head::getItem) // Get the ItemStack if the Head is present + .orElse(new ItemStack(Material.BOOK)); // Default to BOOK if not present + ItemMeta favoriteHeadsItemMeta = favoritesHeadsItem.getItemMeta(); + favoriteHeadsItemMeta.setDisplayName(ChatColor.GOLD + "Favorites"); + favoriteHeadsItemMeta.setLore(null); + favoritesHeadsItem.setItemMeta(favoriteHeadsItemMeta); + builder.button(39, new SimpleButton(favoritesHeadsItem, e -> { + Player player = (Player) e.getWhoClicked(); + if (!player.hasPermission("headdb.favorites")) { + localization.sendMessage(player, "noAccessFavorites"); + return; + } + + favorites(player, 0); + })); + + + // Set local heads button in main page + ItemStack localHeadsItem = new ItemStack(Material.COMPASS); + ItemMeta localHeadsItemMeta = localHeadsItem.getItemMeta(); + //noinspection DataFlowIssue + localHeadsItemMeta.setDisplayName(ChatColor.GOLD + "Local Heads"); + localHeadsItem.setItemMeta(localHeadsItemMeta); + builder.button(41, new SimpleButton(localHeadsItem, e -> { + // Local heads must be calculated on every opening since new players can join at any time. + List localHeadsList = new ArrayList<>(HeadAPI.getLocalHeads(e.getWhoClicked().hasPermission("headdb.admin")).join()); // Convert Set to List for indexed access + GUI localGui = new SimpleGUI(); + Player mainPlayer = (Player) e.getWhoClicked(); + + for (int i = 0; i < localHeadsList.size(); i += 45) { + int end = Math.min(i + 45, localHeadsList.size()); + List section = localHeadsList.subList(i, end); // Get the sublist for the current page + + PaginationBuilder localPageBuilder = new PaginationBuilder(localGui) + .parentGui(mainGui) + .name(ChatColor.RED + "Local Heads" + ChatColor.DARK_GRAY + " (" + HeadAPI.getLocalHeads().join().size() + ")"); + + // Iterate over the heads in the current section and add them to the inventory + for (int j = 0; j < section.size(); j++) { + LocalHead localHead = section.get(j); + localPageBuilder.button(j, new SimpleButton(localHead.getItem(), ice -> { + ice.setCancelled(true); + Player player = (Player) ice.getWhoClicked(); + if (categoryPermission && !player.hasPermission("headdb.category.local.*") && !player.hasPermission("headdb.category.local." + localHead.getUniqueId())) { + localization.sendMessage(player, "noPermission"); + return; + } + + handleClick(player, localHead, ice); + })); + } + + // Build the page and add it to the local GUI + localGui.addPage(localPageBuilder.build()); + localGui.open(mainPlayer); + } + })); + + mainGui.addPage(builder.build()); + + prebuildCategoryGuis(); + } + + private static void favorites(Player player, int page) { + Set favorites = HeadAPI.getFavoriteHeads(player.getUniqueId()).join(); + if (!favorites.isEmpty()) { + // Build favorites GUI + GUI favoritesGui = new SimpleGUI(); + List favoriteList = new ArrayList<>(favorites); // Copy to list for consistent indexing + + for (int i = 0; i < favoriteList.size(); i += 45) { + int end = Math.min(i + 45, favoriteList.size()); + List section = favoriteList.subList(i, end); + + PaginationBuilder favoritesPageBuilder = new PaginationBuilder(favoritesGui) + .parentGui(mainGui) + .name(ChatColor.GOLD + "Favorites " + ChatColor.DARK_GRAY + "(" + favoriteList.size() + ")"); + + for (int j = 0; j < section.size(); j++) { + Head head = section.get(j); + favoritesPageBuilder.button(j, new SimpleButton(head.getItem(), ice -> { + handleClick(player, head, ice); + + // Update favorites after removing the head + Set updatedFavorites = HeadAPI.getFavoriteHeads(player.getUniqueId()).join(); + + if (!updatedFavorites.isEmpty()) { + favorites(player, page); // Refresh the GUI + } else { + mainGui.open(player); + } + })); + } + + favoritesGui.addPage(favoritesPageBuilder.build()); + } + + favoritesGui.open(player, !favoritesGui.getPages().isEmpty() ? page : 0); + } else { + localization.sendMessage(player, "noFavorites"); + Sounds.FAIL.play(player); + } + } + + public static void prebuildCategoryGuis() { + // Prebuild category guis + for (Category category : Category.VALUES) { + List heads = HeadAPI.getHeads().stream().filter(head -> head.getCategory().orElse("N/A").equalsIgnoreCase(category.getName())).toList(); + GUI categoryGui = new SimpleGUI(); + + // Iterate over the heads, chunking them into pages + for (int i = 0; i < heads.size(); i += 45) { + int end = Math.min(i + 45, heads.size()); + List section = heads.subList(i, end); + + PaginationBuilder categoryPageBuilder = new PaginationBuilder(categoryGui) + .parentGui(mainGui) + .name(ChatColor.GOLD + category.getDisplayName() + ChatColor.DARK_GRAY + " (" + heads.size() + ")"); + + // Iterate over the heads in the current section and add them to the inventory + for (int j = 0; j < section.size(); j++) { + Head head = section.get(j); + categoryPageBuilder.button(j, new SimpleButton(head.getItem(), e -> { + Player player = (Player) e.getWhoClicked(); + if (categoryPermission && !player.hasPermission("headdb.category." + category.getName()) && !player.hasPermission("headdb.category.*")) { + localization.sendMessage(player, "noPermission"); + return; + } + + handleClick(player, head, e); + })); + } + + // Build the page and add it to the category GUI + categoryGui.addPage(categoryPageBuilder.build()); + } + + categoryGuis.put(category, categoryGui); + } + + // Prebuild ALL gui. + List heads = HeadAPI.getHeads(); + for (int i = 0; i < heads.size(); i += 45) { + int end = Math.min(i + 45, heads.size()); + List section = heads.subList(i, end); + PaginationBuilder allPageBuilder = new PaginationBuilder(allGui) + .parentGui(mainGui) + .name(ChatColor.GOLD + "All Heads" + ChatColor.DARK_GRAY + " (" + heads.size() + ")"); + + // Iterate over the heads in the current section and add them to the inventory + for (int j = 0; j < section.size(); j++) { + Head head = section.get(j); + // This is the slot for the current head, relative to the page (0-35) + allPageBuilder.button(j, new SimpleButton(head.getItem(), e -> { + Player player = (Player) e.getWhoClicked(); + if (categoryPermission && !player.hasPermission("headdb.category.all") && !player.hasPermission("headdb.category.*")) { + localization.sendMessage(player, "noPermission"); + return; + } + + handleClick(player, head, e); + })); + } + + // Build the page and add it to the category GUI + allGui.addPage(allPageBuilder.build()); + } + } + + public static void openSearch(List heads, String id, Player player) { + GUI gui = new SimpleGUI(); + for (int i = 0; i < heads.size(); i += 45) { + int end = Math.min(i + 45, heads.size()); + List section = heads.subList(i, end); // Get the sublist for the current page + + PaginationBuilder pageBuilder = new PaginationBuilder(gui) + .parentGui(mainGui) + .name(ChatColor.DARK_GRAY + "Search » " + ChatColor.GOLD + id); + + // Iterate over the heads in the current section and add them to the inventory + for (int j = 0; j < section.size(); j++) { + Head head = section.get(j); + pageBuilder.button(j, new SimpleButton(head.getItem(), ice -> { + handleClick(player, head, ice); + })); + } + + // Build the page and add it to the local GUI + gui.addPage(pageBuilder.build()); + gui.open(player); + } + } + + private static void handleClick(Player player, Head head, InventoryClickEvent ice) { + if (ice.isLeftClick()) { + Utils.purchaseHead(player, head, ice.isShiftClick() ? 64 : 1); // Give the player the head item + } else if (ice.isRightClick()) { + PlayerData playerData = HeadDB.getInstance().getPlayerDatabase().getOrCreate(player.getUniqueId()); + if (playerData.getFavorites().contains(head.getId())) { + playerData.getFavorites().remove(head.getId()); + localization.sendMessage(player, "removedFavorite", msg -> msg.replace("%name%", head.getName())); + Sounds.FAVORITE_REMOVE.play(player); + } else { + playerData.getFavorites().add(head.getId()); + localization.sendMessage(player, "addedFavorite", msg -> msg.replace("%name%", head.getName())); + Sounds.FAVORITE.play(player); + } + } + } + +} diff --git a/src/main/java/tsp/headdb/core/util/PaginationBuilder.java b/src/main/java/tsp/headdb/core/util/PaginationBuilder.java new file mode 100644 index 0000000..79abeb7 --- /dev/null +++ b/src/main/java/tsp/headdb/core/util/PaginationBuilder.java @@ -0,0 +1,47 @@ +package tsp.headdb.core.util; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; +import tsp.invlib.gui.GUI; +import tsp.invlib.gui.button.control.ControlButton; +import tsp.invlib.gui.page.Page; +import tsp.invlib.gui.page.PageBuilder; + +import java.util.function.BiConsumer; + +/** + * @author TheSilentPro (Silent) + */ +public class PaginationBuilder extends PageBuilder { + + public PaginationBuilder(GUI gui) { + super(gui); + } + + @Override + public PaginationBuilder parentGui(GUI gui) { + super.parentGui(gui); + return this; + } + + @Override + public PaginationBuilder name(String name) { + super.name(name); + return this; + } + + @Override + public PaginationBuilder onControlClick(BiConsumer event) { + super.onControlClick(event); + return this; + } + + @Override + public Page build() { + preventClick(); + includeControlButtons(); + onControlClick((button, event) -> Sounds.PAGE_CHANGE.play((Player) event.getWhoClicked())); + return super.build(); + } + +} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/core/util/Sounds.java b/src/main/java/tsp/headdb/core/util/Sounds.java new file mode 100644 index 0000000..c1875fb --- /dev/null +++ b/src/main/java/tsp/headdb/core/util/Sounds.java @@ -0,0 +1,36 @@ +package tsp.headdb.core.util; + +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +/** + * @author TheSilentPro (Silent) + */ +public enum Sounds { + + FAIL(Sound.BLOCK_ANVIL_LAND, 0.3f, 0.5f), + SUCCESS(Sound.ENTITY_PLAYER_LEVELUP, 2f), + PAGE_CHANGE(Sound.BLOCK_LEVER_CLICK, 0.5f, 1f), + PAGE_OPEN(Sound.ENTITY_BAT_TAKEOFF, 1f), + FAVORITE(Sound.ENTITY_ARROW_HIT_PLAYER, 1f), + FAVORITE_REMOVE(Sound.ENTITY_ARROW_HIT_PLAYER, 2f); + + private final Sound sound; + private final float volume; + private final float pitch; + + Sounds(Sound sound, float volume, float pitch) { + this.sound = sound; + this.volume = volume; + this.pitch = pitch; + } + + Sounds(Sound sound, float pitch) { + this(sound, 1f, pitch); + } + + public void play(Player player) { + player.playSound(player, sound, volume, pitch); + } + +} diff --git a/src/main/java/tsp/headdb/core/util/Utils.java b/src/main/java/tsp/headdb/core/util/Utils.java index a538d40..6b2a50b 100644 --- a/src/main/java/tsp/headdb/core/util/Utils.java +++ b/src/main/java/tsp/headdb/core/util/Utils.java @@ -1,357 +1,198 @@ 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 org.bukkit.inventory.meta.SkullMeta; import org.bukkit.profile.PlayerProfile; import org.bukkit.profile.PlayerTextures; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import tsp.headdb.HeadDB; -import tsp.headdb.core.api.HeadAPI; -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.helperlite.scheduler.promise.Promise; -import tsp.nexuslib.builder.ItemBuilder; -import tsp.nexuslib.inventory.Button; -import tsp.nexuslib.inventory.PagedPane; -import tsp.nexuslib.inventory.Pane; -import tsp.nexuslib.localization.TranslatableLocalization; -import tsp.nexuslib.server.ServerVersion; -import tsp.nexuslib.util.StringUtils; -import tsp.nexuslib.util.Validate; +import tsp.headdb.core.config.ConfigData; +import tsp.headdb.core.economy.EconomyProvider; +import tsp.headdb.api.model.Head; +import tsp.headdb.api.model.LocalHead; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import javax.annotation.ParametersAreNonnullByDefault; -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Field; -import java.math.BigDecimal; import java.net.MalformedURLException; -import java.net.URL; +import java.net.URI; +import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.concurrent.Executor; +import java.util.regex.Pattern; +/** + * @author TheSilentPro (Silent) + */ public class Utils { - private static final HeadDB instance = HeadDB.getInstance(); - private static Properties properties = null; + private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class); + private static final Pattern HEAD_PATTERN = Pattern.compile("[^a-zA-Z0-9]"); + public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("dd-MM-yyyy"); + public static final Executor SYNC = r -> Bukkit.getScheduler().runTask(HeadDB.getInstance(), r); + private static final ConfigData config = HeadDB.getInstance().getCfg(); - public static Optional getVersion() { - if (properties == null) { - InputStream is = instance.getResource("build.properties"); - if (is == null) { - return Optional.empty(); - } + @SuppressWarnings("DataFlowIssue") + public static ItemStack asItem(Head head) { + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + ItemMeta meta = item.getItemMeta(); + meta.setDisplayName(ChatColor.GOLD + head.getName()); - try { - properties = new Properties(); - properties.load(is); - } catch (IOException ex) { - instance.getLog().debug("Failed to load build properties: " + ex.getMessage()); - return Optional.empty(); - } - } + List lore = new ArrayList<>(); + lore.add(ChatColor.GRAY + "ID » " + ChatColor.GOLD + head.getId()); + head.getTags().ifPresent(tags -> lore.add(ChatColor.GRAY + "Tags » " + ChatColor.GOLD + String.join(", ", tags))); - return Optional.ofNullable(properties.getProperty("version")); - } - - public static String toString(Collection 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(); - } - - 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 DataFlowIssue - 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; - } - - public static void openFavoritesMenu(Player player) { - try (Promise> promise = HeadAPI.getFavoriteHeads(player.getUniqueId())) { - promise.thenAcceptSync(heads -> { - PagedPane main = Utils.createPaged(player, Utils.translateTitle(HeadDB.getInstance().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 (!player.hasPermission("headdb.favorites")) { - HeadDB.getInstance().getLocalization().sendMessage(player, "noAccessFavorites"); - return; - } - - if (fe.isLeftClick()) { - int amount = 1; - if (fe.isShiftClick()) { - amount = 64; - } - - Utils.purchase(player, head, amount); - } else if (fe.isRightClick()) { - HeadDB.getInstance().getStorage().getPlayerStorage().removeFavorite(player.getUniqueId(), head.getTexture()); - HeadDB.getInstance().getLocalization().sendMessage(player, "removedFavorite", msg -> msg.replace("%name%", head.getName())); - openFavoritesMenu(player); - } - })); + if (config.shouldIncludeMoreInfo()) { + head.getCategory().ifPresent(category -> { + if (category.isEmpty()) { + lore.add(ChatColor.GRAY + "Category » " + ChatColor.GOLD + category); } - - main.open(player); }); - } catch (Exception ex) { - ex.printStackTrace(); + head.getContributors().ifPresent(contributors -> { + if (contributors.length != 0) { + lore.add(ChatColor.GRAY + "Contributors » " + ChatColor.GOLD + String.join(", ", contributors)); + } + }); + head.getCollections().ifPresent(collections -> { + if (collections.length != 0) { + lore.add(ChatColor.GRAY + "Collections » " + ChatColor.GOLD + String.join(", ", collections)); + } + }); + head.getPublishDate().ifPresent(date -> lore.add(ChatColor.GRAY + "Published » " + ChatColor.GOLD + date)); } + + if (HeadDB.getInstance().getEconomyProvider() != null) { + lore.add(" "); + lore.add(ChatColor.GRAY + "Cost (x1) » " + ChatColor.GOLD + getHeadCost(head) + ChatColor.GRAY + " (Left-Click)"); + lore.add(ChatColor.GRAY + "Cost (x64) » " + ChatColor.GOLD + (getHeadCost(head) * 64) + ChatColor.GRAY + " (Shift-Left-Click)"); + } + + meta.setLore(lore); + item.setItemMeta(meta); + + PlayerProfile profile; + try { + profile = Bukkit.createPlayerProfile(null, head.getName()); + } catch (IllegalArgumentException ex) { + // Head may contain special characters(@,!,<,>) that are not allowed in a PlayerProfile. + // Additionally, spaces are also removed as the profile name should not be visible to players. + String name = HEAD_PATTERN.matcher(head.getName().trim()).replaceAll(""); + if (name.length() > 16) { // Profile names can not be longer than 16 characters + name = name.substring(0, 16); + } + profile = Bukkit.createPlayerProfile(null, name); + } + + PlayerTextures textures = profile.getTextures(); + String url = new String(Base64.getDecoder().decode(head.getTexture().orElseThrow(() -> new IllegalArgumentException("Head texture must not be null!")))); + try { + textures.setSkin(URI.create(url.substring("{\"textures\":{\"SKIN\":{\"url\":\"".length(), url.length() - "\"}}}".length())).toURL()); + } catch (MalformedURLException ex) { + throw new RuntimeException(ex); + } + profile.setTextures(textures); + + if (meta instanceof SkullMeta skullMeta) { + skullMeta.setOwnerProfile(profile); + item.setItemMeta(skullMeta); + } + return item; } - @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); + public static void purchaseHead(Player player, Head head, int amount) { + EconomyProvider economyProvider = HeadDB.getInstance().getEconomyProvider(); + if (economyProvider == null) { + ItemStack item = head.getItem().clone(); + item.setAmount(amount); + if (!HeadDB.getInstance().getCfg().shouldIncludeLore()) { + ItemMeta meta = item.getItemMeta(); + //noinspection DataFlowIssue + meta.setLore(null); + item.setItemMeta(meta); + } + player.getInventory().addItem(item); + Sounds.SUCCESS.play(player); + return; + } - if (category != null && instance.getConfig().getBoolean("requireCategoryPermission") && !player.hasPermission("headdb.category." + category.getName())) { - instance.getLocalization().sendMessage(player.getUniqueId(), "noPermission"); + double cost = getHeadCost(head); + economyProvider.purchase(player, cost * amount).whenComplete((success, ex) -> { + Bukkit.getScheduler().runTask(HeadDB.getInstance(), () -> { + if (ex != null) { + HeadDB.getInstance().getLocalization().sendMessage(player, "error"); + LOGGER.error("Purchasing head(s) failed!", ex); return; } - if (e.isLeftClick()) { - int amount = 1; - if (e.isShiftClick()) { - amount = 64; - } - - purchase(player, head, amount); - } else if (e.isRightClick()) { - if (player.hasPermission("headdb.favorites")) { - HeadDB.getInstance().getStorage().getPlayerStorage().addFavorite(player.getUniqueId(), head.getTexture()); - HeadDB.getInstance().getLocalization().sendMessage(player, "addedFavorite", msg -> msg.replace("%name%", head.getName())); - } else { - HeadDB.getInstance().getLocalization().sendMessage(player, "noAccessFavorites"); - } - } - })); - } - } - - private static Promise processPayment(Player player, Head head, int amount) { - Optional optional = HeadDB.getInstance().getEconomyProvider(); - if (optional.isEmpty()) { - return Promise.completed(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).thenApplyAsync(success -> { if (success) { - HeadDB.getInstance().getLocalization().sendMessage(player, "completePayment", msg -> msg - .replace("%name%", head.getName()) - .replace("%cost%", cost.toString())); + ItemStack item = head.getItem().clone(); + item.setAmount(amount); + if (!HeadDB.getInstance().getCfg().shouldIncludeLore()) { + ItemMeta meta = item.getItemMeta(); + //noinspection DataFlowIssue + meta.setLore(null); + item.setItemMeta(meta); + } + player.getInventory().addItem(item); + Sounds.SUCCESS.play(player); + + HeadDB.getInstance().getLocalization().sendMessage(player, "completePayment", msg -> msg.replace("%amount%", String.valueOf(amount)).replace("%name%", head.getName()).replace("%cost%", String.valueOf(cost * amount))); + + HeadDB.getInstance().getConfig().getStringList("commands.purchase").forEach(command -> { + if (command.isEmpty()) { + return; + } + if (HeadDB.getInstance().isPAPI()) { + command = PlaceholderAPI.setPlaceholders(player, command); + } + + Bukkit.dispatchCommand(Bukkit.getConsoleSender(), command); + }); } else { - HeadDB.getInstance().getLocalization().sendMessage(player, "invalidFunds", msg -> msg.replace("%name%", head.getName())); + Sounds.FAIL.play(player); + HeadDB.getInstance().getLocalization().sendMessage(player, "invalidFunds", msg -> msg.replace("%amount%", String.valueOf(amount)).replace("%name%", head.getName()).replace("%cost%", String.valueOf(cost * amount))); } - return success; }); - } - } - - public static void purchase(Player player, Head head, int amount) { - // Bukkit API - Has to be sync. - processPayment(player, head, amount).thenAcceptSync(success -> { - if (success) { - 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 double getHeadCost(Head head) { + if (head instanceof LocalHead) { // Local heads have only one cost + return config.getLocalCost(); + } else if (config.getCosts().containsKey(head)) { // Try get cost for specific head + return config.getCosts().get(head); + } else if (config.getCategoryCosts().containsKey(head.getCategory().orElse("?"))) { // Try get cost for specific category for the head + return config.getCategoryCosts().get(head.getCategory().orElse("?")); + } else { // Get the default cost for the head. + return config.getDefaultCost(); } } - public static ItemStack asItem(UUID receiver, Head head) { - TranslatableLocalization localization = HeadDB.getInstance().getLocalization(); - ItemStack item = new ItemBuilder(Material.PLAYER_HEAD) - .name(localization.getMessage(receiver, "menu.head.name").orElse("&e" + head.getName().toUpperCase(Locale.ROOT)).replace("%name%", head.getName())) - .setLore("&cID: " + head.getId(), "&7Tags: &e" + head.getTags()) - .build(); - - ItemMeta meta = item.getItemMeta(); - - // if version < 1.20.1 use reflection, else (1.20.2+) use PlayerProfile because spigot bitches otherwise. - // Assumes newer version has been released when optional is empty. - if (ServerVersion.getVersion().orElse(ServerVersion.v_1_20_2).isOlderThan(ServerVersion.v_1_20_1)) { - try { - GameProfile profile = new GameProfile(head.getUniqueId(), head.getName()); - profile.getProperties().put("textures", new Property("textures", head.getTexture())); - - //noinspection DataFlowIssue - Field profileField = meta.getClass().getDeclaredField("profile"); - profileField.setAccessible(true); - profileField.set(meta, profile); - item.setItemMeta(meta); - } catch (NoSuchFieldException | IllegalArgumentException | IllegalAccessException ex) { - //Log.error("Could not set skull owner for " + uuid.toString() + " | Stack Trace:"); - ex.printStackTrace(); - } - } else { - try { - PlayerProfile profile = Bukkit.createPlayerProfile(head.getUniqueId(), head.getName()); - PlayerTextures textures = profile.getTextures(); - String url = new String(Base64.getDecoder().decode(head.getTexture())); - textures.setSkin(new URL(url.substring("{\"textures\":{\"SKIN\":{\"url\":\"".length(), url.length() - "\"}}}".length()))); - profile.setTextures(textures); - - SkullMeta skullMeta = (SkullMeta) meta; - if (skullMeta != null) { - skullMeta.setOwnerProfile(profile); - } - item.setItemMeta(skullMeta); - } catch (MalformedURLException ex) { - ex.printStackTrace(); - } - } - - return item; + public static String colorize(String s) { + return ChatColor.translateAlternateColorCodes('&', s); } - public static int resolveInt(String raw) { - try { - return Integer.parseInt(raw); - } catch (NumberFormatException nfe) { - return 1; + private static String userAgent = null; + + public static String getUserAgent() { + if (userAgent == null) { + // Example output: HeadDB/5.0.0 (Windows 10; 10.0; amd64) Eclipse Adoptium/21.0.4 Paper/1.21.1 (1.21.1-40-2fdb2e9) + userAgent = "HeadDB/" + HeadDB.getInstance().getDescription().getVersion() + + " (" + System.getProperty("os.name") + + "; " + System.getProperty("os.version") + + "; " + System.getProperty("os.arch") + + ") " + System.getProperty("java.vendor") + "/" + System.getProperty("java.version") + + " " + Bukkit.getName() + "/" + Bukkit.getBukkitVersion().substring(0, Bukkit.getBukkitVersion().indexOf("-")) + " (" + Bukkit.getVersion().substring(0, Bukkit.getVersion().indexOf("(") - 1) + ")"; } + return userAgent; } - 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 DataFlowIssue - 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; + public static boolean matches(String provided, String query) { + return ChatColor.stripColor(provided.toLowerCase()).contains(query.toLowerCase()); } } diff --git a/src/main/java/tsp/headdb/implementation/category/Category.java b/src/main/java/tsp/headdb/implementation/category/Category.java deleted file mode 100644 index d7ceca3..0000000 --- a/src/main/java/tsp/headdb/implementation/category/Category.java +++ /dev/null @@ -1,85 +0,0 @@ -package tsp.headdb.implementation.category; - -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.ItemMeta; -import tsp.headdb.HeadDB; -import tsp.headdb.core.api.HeadAPI; -import tsp.headdb.core.util.Utils; -import tsp.nexuslib.builder.ItemBuilder; -import tsp.nexuslib.util.StringUtils; - -import javax.annotation.Nonnull; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; - -public enum Category { - - ALPHABET("alphabet", 20), - ANIMALS("animals", 21), - BLOCKS("blocks", 22), - DECORATION("decoration", 23), - FOOD_DRINKS("food-drinks", 24), - HUMANS("humans", 29), - HUMANOID("humanoid", 30), - MISCELLANEOUS("miscellaneous", 31), - MONSTERS("monsters", 32), - PLANTS("plants", 33); - - private final String name; - private final int defaultSlot; - private ItemStack item; - - public static final Category[] VALUES = values(); - - Category(String name, int slot) { - this.name = name; - this.defaultSlot = slot; - } - - public String getName() { - return name; - } - - public int getDefaultSlot() { - return defaultSlot; - } - - public static Optional getByName(String cname) { - for (Category value : VALUES) { - if (value.name.equalsIgnoreCase(cname) || value.getName().equalsIgnoreCase(cname)) { - return Optional.of(value); - } - } - - return Optional.empty(); - } - - @Nonnull - public ItemStack getItem(UUID receiver) { - if (item == null) { - HeadAPI.getHeads(this).stream().findFirst() - .ifPresentOrElse(head -> { - ItemStack retrieved = new ItemStack(head.getItem(receiver)); - ItemMeta meta = retrieved.getItemMeta(); - if (meta != null && meta.getLore() != null) { - meta.setDisplayName(Utils.translateTitle(HeadDB.getInstance().getLocalization().getMessage(receiver, "menu.main.category.name").orElse("&e" + getName()), HeadAPI.getHeads(this).size(), getName().toUpperCase(Locale.ROOT))); - meta.setLore(HeadDB.getInstance().getConfig().getStringList("menu.main.category.lore").stream() - .map(StringUtils::colorize) - .collect(Collectors.toList())); - retrieved.setItemMeta(meta); - item = retrieved; - } else { - item = new ItemStack(Material.PLAYER_HEAD); - HeadDB.getInstance().getLog().debug("Failed to get null-meta category item for: " + name()); - } - }, - () -> item = new ItemBuilder(Material.PLAYER_HEAD).name(getName().toUpperCase(Locale.ROOT)).build()); - } - - return item.clone(); // Return clone that changes are not reflected - } - -} diff --git a/src/main/java/tsp/headdb/implementation/head/Head.java b/src/main/java/tsp/headdb/implementation/head/Head.java deleted file mode 100644 index b049116..0000000 --- a/src/main/java/tsp/headdb/implementation/head/Head.java +++ /dev/null @@ -1,76 +0,0 @@ -package tsp.headdb.implementation.head; - -import org.bukkit.inventory.ItemStack; -import tsp.headdb.core.util.Utils; -import tsp.headdb.implementation.category.Category; -import tsp.nexuslib.util.Validate; - -import javax.annotation.ParametersAreNonnullByDefault; -import java.util.UUID; - -public class Head { - - private final int id; - private final UUID uniqueId; - private final String name; - private final String texture; - private final String tags; - private final String updated; - private final Category category; - private ItemStack item; - - @ParametersAreNonnullByDefault - public Head(int id, UUID uniqueId, String name, String texture, String tags, String updated, Category category) { - Validate.notNull(uniqueId, "Unique id can not be null!"); - Validate.notNull(name, "Name can not be null!"); - Validate.notNull(texture, "Texture can not be null!"); - Validate.notNull(tags, "Tags can not be null!"); - Validate.notNull(updated, "Updated can not be null!"); - Validate.notNull(category, "Category can not be null!"); - - this.id = id; - this.uniqueId = uniqueId; - this.name = name; - this.texture = texture; - this.tags = tags; - this.updated = updated; - this.category = category; - } - - public ItemStack getItem(UUID receiver) { - if (item == null) { - item = Utils.asItem(receiver, this); - } - - return item.clone(); // Return clone that changes are not reflected - } - - public int getId() { - return id; - } - - public UUID getUniqueId() { - return uniqueId; - } - - public String getName() { - return name; - } - - public String getTexture() { - return texture; - } - - public String getTags() { - return tags; - } - - public String getUpdated() { - return updated; - } - - public Category getCategory() { - return category; - } - -} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java b/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java deleted file mode 100644 index 607bd97..0000000 --- a/src/main/java/tsp/headdb/implementation/head/HeadDatabase.java +++ /dev/null @@ -1,72 +0,0 @@ -package tsp.headdb.implementation.head; - -import org.bukkit.plugin.java.JavaPlugin; -import tsp.headdb.implementation.category.Category; -import tsp.headdb.implementation.requester.HeadProvider; -import tsp.headdb.implementation.requester.Requester; -import tsp.helperlite.scheduler.promise.Promise; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class HeadDatabase { - - private final JavaPlugin plugin; - private final Requester requester; - private final ConcurrentHashMap> heads; - private long timestamp; - - public HeadDatabase(JavaPlugin plugin, HeadProvider provider) { - this.plugin = plugin; - this.requester = new Requester(plugin, provider); - this.heads = new ConcurrentHashMap<>(); - - // Fill empty - for (Category cat : Category.VALUES) { - heads.put(cat, new ArrayList<>()); - } - } - - public Map> getHeads() { - return heads; - } - - public Promise getHeadsNoCache() { - return Promise.supplyingAsync(() -> { - long start = System.currentTimeMillis(); - Map> result = new HashMap<>(); - for (Category category : Category.VALUES) { - result.put(category, requester.fetchAndResolve(category)); - } - - return new HeadResult(System.currentTimeMillis() - start, result); - }); - } - - public Promise update() { - return Promise.start() - .thenComposeAsync(compose -> getHeadsNoCache()) - .thenApplyAsync(result -> { - heads.clear(); - heads.putAll(result.heads()); - timestamp = System.currentTimeMillis(); - return result; - }); - } - - public long getTimestamp() { - return timestamp; - } - - public JavaPlugin getPlugin() { - return plugin; - } - - public Requester getRequester() { - return requester; - } - -} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/head/HeadResult.java b/src/main/java/tsp/headdb/implementation/head/HeadResult.java deleted file mode 100644 index 32ebd0b..0000000 --- a/src/main/java/tsp/headdb/implementation/head/HeadResult.java +++ /dev/null @@ -1,11 +0,0 @@ -package tsp.headdb.implementation.head; - -import tsp.headdb.implementation.category.Category; - -import java.util.List; -import java.util.Map; - -/** - * @author TheSilentPro (Silent) - */ -public record HeadResult(long elapsed, Map> heads) {} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/head/LocalHead.java b/src/main/java/tsp/headdb/implementation/head/LocalHead.java deleted file mode 100644 index 613b108..0000000 --- a/src/main/java/tsp/headdb/implementation/head/LocalHead.java +++ /dev/null @@ -1,28 +0,0 @@ -package tsp.headdb.implementation.head; - -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.Material; -import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.meta.SkullMeta; - -import java.util.Collections; -import java.util.UUID; - -public record LocalHead(UUID uniqueId, String name) { - - public ItemStack getItem() { - ItemStack item = new ItemStack(Material.PLAYER_HEAD); - SkullMeta meta = (SkullMeta) item.getItemMeta(); - if (meta != null) { - meta.setOwningPlayer(Bukkit.getOfflinePlayer(uniqueId)); - meta.setDisplayName(ChatColor.GOLD + name); - //noinspection UnnecessaryToStringCall - meta.setLore(Collections.singletonList(ChatColor.GRAY + "UUID: " + uniqueId.toString())); - item.setItemMeta(meta); - } - - return item.clone(); - } - -} \ No newline at end of file diff --git a/src/main/java/tsp/headdb/implementation/requester/HeadProvider.java b/src/main/java/tsp/headdb/implementation/requester/HeadProvider.java deleted file mode 100644 index 062330e..0000000 --- a/src/main/java/tsp/headdb/implementation/requester/HeadProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package tsp.headdb.implementation.requester; - -import tsp.headdb.implementation.category.Category; - -public enum HeadProvider { - - HEAD_API("https://minecraft-heads.com/scripts/api.php?cat=%s&tags=true"), // No ids - HEAD_STORAGE("https://raw.githubusercontent.com/TheSilentPro/HeadStorage/master/storage/%s.json"), - HEAD_WORKER(""), // Unimplemented yet. - HEAD_ARCHIVE("https://heads.pages.dev/archive/%s.json"); - - private final String url; - - HeadProvider(String url) { - this.url = url; - } - - public String getUrl() { - return url; - } - - public String getFormattedUrl(Category category) { - return String.format(getUrl(), category.getName()); - } - -} diff --git a/src/main/java/tsp/headdb/implementation/requester/Requester.java b/src/main/java/tsp/headdb/implementation/requester/Requester.java deleted file mode 100644 index a38d65c..0000000 --- a/src/main/java/tsp/headdb/implementation/requester/Requester.java +++ /dev/null @@ -1,111 +0,0 @@ -package tsp.headdb.implementation.requester; - -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import org.bukkit.plugin.java.JavaPlugin; -import tsp.headdb.HeadDB; -import tsp.headdb.core.util.Utils; -import tsp.headdb.implementation.category.Category; -import tsp.headdb.implementation.head.Head; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; -import java.util.UUID; - -/** - * Responsible for requesting heads from providers. - * - * @author TheSilentPro - * @see tsp.headdb.core.api.HeadAPI - * @see tsp.headdb.implementation.head.HeadDatabase - */ -public class Requester { - - private final JavaPlugin plugin; - private HeadProvider provider; - - public Requester(JavaPlugin plugin, HeadProvider provider) { - this.plugin = plugin; - this.provider = provider; - } - - public List fetchAndResolve(Category category) { - try { - Response response = fetch(category); - List result = new ArrayList<>(); - if (response.code() != 200) { - return result; - } - - JsonArray main = JsonParser.parseString(response.response()).getAsJsonArray(); - for (JsonElement entry : main) { - JsonObject obj = entry.getAsJsonObject(); - int id = obj.get("id").getAsInt(); - - if (plugin.getConfig().contains("blockedHeads.ids")) { - List blockedIds = plugin.getConfig().getIntegerList("blockedHeads.ids"); - if (blockedIds.contains(id)) { - HeadDB.getInstance().getLog().debug("Skipped blocked head: " + obj.get("name").getAsString() + "(" + id + ")"); - continue; - } - } - - result.add(new Head( - id, - Utils.validateUniqueId(obj.get("uuid").getAsString()).orElse(UUID.randomUUID()), - obj.get("name").getAsString(), - obj.get("value").getAsString(), - obj.get("tags").getAsString(), - response.date(), - category - )); - } - - return result; - } catch (IOException ex) { - HeadDB.getInstance().getLog().debug("Failed to load from provider: " + provider.name()); - if (HeadDB.getInstance().getConfig().getBoolean("fallback") && provider != HeadProvider.HEAD_ARCHIVE) { // prevent recursion. Maybe switch to an attempts counter down in the future - provider = HeadProvider.HEAD_ARCHIVE; - return fetchAndResolve(category); - } else { - HeadDB.getInstance().getLog().error("Could not fetch heads from any provider!"); - return new ArrayList<>(); - } - } - } - - public Response fetch(Category category) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new URL(provider.getFormattedUrl(category)).openConnection(); - connection.setConnectTimeout(5000); - connection.setRequestMethod("GET"); - connection.setRequestProperty("User-Agent", plugin.getName() + "/" + Utils.getVersion().orElse(plugin.getDescription().getVersion())); - connection.setRequestProperty("Accept", "application/json"); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - StringBuilder builder = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line); - } - - connection.disconnect(); - return new Response(builder.toString(), connection.getResponseCode(), connection.getHeaderField("date")); - } - } - - public HeadProvider getProvider() { - return provider; - } - - public JavaPlugin getPlugin() { - return plugin; - } - -} diff --git a/src/main/java/tsp/headdb/implementation/requester/Response.java b/src/main/java/tsp/headdb/implementation/requester/Response.java deleted file mode 100644 index 9d21755..0000000 --- a/src/main/java/tsp/headdb/implementation/requester/Response.java +++ /dev/null @@ -1,3 +0,0 @@ -package tsp.headdb.implementation.requester; - -public record Response(String response, int code, String date) {} \ No newline at end of file diff --git a/src/main/resources/build.properties b/src/main/resources/build.properties deleted file mode 100644 index e5683df..0000000 --- a/src/main/resources/build.properties +++ /dev/null @@ -1 +0,0 @@ -version=${project.version} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index eea01be..33d1035 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,20 +1,15 @@ -# How often the database should be updated in seconds. -refresh: 86400 - -# If local heads should be enabled. -# Local heads are heads from players that have joined your server at least once. -localHeads: true - -# If enabled categories will require a permission to be used. -# Permission: headdb.category. -requireCategoryPermission: false - # Economy Options economy: enabled: false provider: "VAULT" # Supported: VAULT - format: "##.##" - cost: + # The cost of a local (Player) head. + localCost: 100 + # Default cost for head. + defaultCost: 100 + # Default category cost. + defaultCategoryCost: 100 + # Cost of categories. + categoryCost: alphabet: 100 animals: 100 blocks: 100 @@ -25,70 +20,36 @@ economy: miscellaneous: 100 monsters: 100 plants: 100 + # Cost of individual heads by either number ID or texture value. + cost: + # Example. + # The key(-2) would be the ID or TEXTURE VALUE of the head. + # The value(0) would be the cost of the head. + #-2: 0 # Command Configuration. Supports PlaceholderAPI. commands: # Commands to run ONLY if the purchase is successful. - # They are run as CONSOLE after the player has receiver the head in their inventory. + # They are run as CONSOLE after the player has received the head in their inventory. purchase: - "" -# Graphical User Interface customization -gui: - main: - fill: - material: "BLACK_STAINED_GLASS_PANE" - name: "" - lore: [] - # Categories are set in the slots 20-24 & 29 - 33. You can add specific ones here to relocate them. - # Note: that the slots start from 0 - category: - alphabet: 20 - animals: 21 - blocks: 22 - decoration: 23 - food-drinks: 24 - humans: 29 - humanoid: 30 - miscellaneous: 31 - monsters: 32 - plants: 33 - meta: - favorites: - slot: 39 - item: - material: "BOOK" - name: "&6Favorites" - lore: - - "&7Click to view your favorite heads." - search: - slot: 40 - item: - material: "DARK_OAK_SIGN" - name: "&6Search" - lore: - - "&7Click to search for a specific head." - local: - slot: 41 - item: - material: "COMPASS" - name: "&6Local Heads" - lore: - - "&7Click to view Local heads." +# Block heads from showing up in the menu. +blockedHeads: + - "" + +# If enabled categories will require a permission to be used. +# Permission: headdb.category. +# Local Heads permission: headdb.category.local.* OR headdb.category.local. +requireCategoryPermission: false + +# If the menu should show more info on the head such as: category, contributors, collections, publish date +moreInfo: true + +# If the lore (ID,Tags) should be included in the inventory item given to players. +includeLore: false # If the original fetching fails and this is enabled, # the plugin will attempt to fetch the heads from an archive. # The archive is static so some heads may be missing, this will only be used when all else fails. -fallback: true - -# Shows more plugin information. (/hdb info) -showAdvancedPluginInfo: true - -# Block heads from all database instances. -blockedHeads: - # List of head ids to block - ids: - - -1 - -# Debug Mode -debug: false \ No newline at end of file +fallback: true \ No newline at end of file diff --git a/src/main/resources/messages/en.yml b/src/main/resources/messages/en.yml index 0f0a6b4..44d5afa 100644 --- a/src/main/resources/messages/en.yml +++ b/src/main/resources/messages/en.yml @@ -1,68 +1,36 @@ -noConsole: "&cOnly for in-game players!" +error: "&cAn error occurred! Please contact an administrator." +noConsole: "&cThis command can not be used by non players!" noPermission: "&cNo permission!" invalidSubCommand: "&cInvalid sub-command! Run &e/hdb help&c for help." -giveCommandInvalid: "&cInvalid head: &e%name%" invalidArguments: "&cInvalid Arguments! Use &e/hdb help&c for help." invalidTarget: "&cInvalid target: &e%name%" -invalidCategory: "&cInvalid Category!" +invalidCategory: "&cInvalid category!" invalidNumber: "&e%name% &cis not a number!" invalidPageIndex: "&cThat page is out of bounds! Max: %pages%" noAccessFavorites: "&cYou do not have access to favorites!" -openDatabase: "" # Intentionally empty. Sent when the main gui is opened -updateDatabase: "&7Updating..." -updateDatabaseDone: "&7Done! Total Heads: &6%size%" -reloadCommand: "&7Reloading, please wait before using the plugin..." -reloadCommandDone: "&7Reload Complete!" -searchCommand: "&7Searching for heads matching: &6%query%" -searchCommandResults: "&7Found &6%size% &7matches!" -giveCommand: "&7Gave &6x%size% %name% &7to &6%receiver%" -itemTexture: "&7Texture: &6%texture%" -itemNoTexture: "&cThis item does not have a texture!" -copyTexture: "&6Click to copy texture!" +command: + search: + wait: "&7Searching for: &6%name%&7! Please wait..." + done: "&7Found &6%size% &7heads!" + invalid: "&cNo heads matching: &e%name%" + give: + invalid: "&cInvalid head: &e%name%" + done: "&7Gave &a%amount%&7x &6%name% &7to &6%player%" + +openDatabase: "" # Intentionally empty by default. Sent when the main gui is opened. +notReadyDatabase: "&cPlease wait a few seconds..." +updateStarted: "&7Updating..." +updateFinished: "&7Done! Total Heads: &6%size%" + addedFavorite: "&7Added &6%name% &7to your favorites!" removedFavorite: "&7Removed &6%name% &7from your favorites!" +noFavorites: "&cYou have no favorite heads!" -# Only shown if economy is enabled -processPayment: "&7Purchasing &6%name% &7for &6%cost%&7! Please wait..." -completePayment: "&7Received &6%name% &7for &6%cost%" -invalidFunds: "&cYou do not have enough to buy &6%name%&c!" +# Only used if economy is enabled. +processPayment: "&7Purchasing &a%amount% &7x &6%name% &7for &6%cost%&7..." +completePayment: "&7Bought &a%amount%&7x &6%name% &7for &6%cost%" +invalidFunds: "&cYou cannot afford &6%name%&c!" invalidLanguage: "&cInvalid Language! Available: &e%languages%" -languageChanged: "&7Your language was set to: &6%language%" - -menu: - main: - title: "&cHeadDB &7(%size%)" - category: - name: "&6&l%category%" - page: - name: "&7Go to specific page" - lore: - - "&7Left-Click to open" - - "&7Right-Click to open specific page" - favorites: - name: "&6Favorites" - search: - name: "&6Search" - local: - name: "&6Local Heads" - category: - name: "&cHeadDB &7- &6%category% &7(%size%)" - head: - name: "&6%name%" - search: - name: "&cHeadDB &7- &6Search: %query% &7(%size%)" - page: - name: "&cHeadDB &7- &6Enter page number" - settings: - name: "&cHeadDB &7- &6Settings" - language: - # Setting name - name: "&cLanguage" - # Available languages lore - available: "&7Languages Available: &6%size%" - # Title of the language selection inventory - title: "&cHeadDB &7- &6Languages &7(%size%)" - # Language item name - format: "&6%language%" \ No newline at end of file +languageChanged: "&7Your language was set to: &6%language%" \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 087e1b6..e5dcdcd 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,11 +1,15 @@ +# noinspection YAMLSchemaValidation name: ${project.name} description: ${project.description} +author: TheSilentPro (Silent) main: tsp.headdb.HeadDB version: ${project.version} -softdepend: ["Vault"] +softdepend: ["PlaceholderAPI", "Vault"] api-version: 1.19 -author: TheSilentPro (Silent) +libraries: + - "org.xerial:sqlite-jdbc:3.47.0.0" + spigot-id: 84967 commands: @@ -24,11 +28,11 @@ permissions: headdb.command.update: true headdb.command.reload: true headdb.command.language: true - headdb.command.settings: true headdb.command.texture: true headdb.favorites: true - headdb.local: true + headdb.category.local.*: true headdb.category.*: true + headdb.category.all: true headdb.command.open: default: op headdb.command.search: @@ -41,13 +45,11 @@ permissions: default: op headdb.command.language: default: op - headdb.command.settings: - default: op headdb.command.texture: default: op headdb.favorites: default: op - headdb.local: + headdb.category.local.*: default: op headdb.category.*: default: op