diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/HeadDB.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/HeadDB.java index 1066eaa..51f9c9b 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/HeadDB.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/HeadDB.java @@ -1,5 +1,16 @@ package com.github.thesilentpro.headdb.core; +import java.util.List; +import java.util.function.BiConsumer; + +import org.bukkit.Bukkit; +import org.bukkit.command.PluginCommand; +import org.bukkit.inventory.InventoryView; +import org.bukkit.plugin.ServicePriority; +import org.bukkit.plugin.java.JavaPlugin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.github.thesilentpro.grim.listener.PageListeners; import com.github.thesilentpro.grim.page.registry.PageRegistry; import com.github.thesilentpro.headdb.api.HeadAPI; @@ -21,16 +32,6 @@ import com.github.thesilentpro.headdb.implementation.BaseHeadAPI; import com.github.thesilentpro.headdb.implementation.BaseHeadDatabase; import com.github.thesilentpro.inputs.paper.PaperInputListener; -import org.bukkit.Bukkit; -import org.bukkit.command.PluginCommand; -import org.bukkit.inventory.InventoryView; -import org.bukkit.plugin.ServicePriority; -import org.bukkit.plugin.java.JavaPlugin; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.function.BiConsumer; public class HeadDB extends JavaPlugin { @@ -158,26 +159,24 @@ public Config getCfg() { return this.configManager.getConfig(); } + public com.github.thesilentpro.headdb.core.config.MenuConfig getMenuConfig() { + return this.configManager.getMenuConfig(); + } + public HeadAPI getHeadApi() { return headApi; } private static final BiConsumer> DATABASE_UPDATE_ACTION = (config, heads) -> { - LOGGER.info("Loaded {} heads!", heads.size()); - - if (config.isPreloadHeads()) { - int total = heads.size(); - if (total == 0) { - LOGGER.info("No heads to preload."); - return; - } + int total = heads.size(); + LOGGER.info("Loaded {} heads!", total); + if (config.isPreloadHeads() && total > 0) { int[] milestones = {25, 50, 75, 100}; int nextMilestoneIndex = 0; for (int i = 0; i < total; i++) { - Head head = heads.get(i); - head.getItem(); // Loads the ItemStack in memory + heads.get(i).getItem(); // Loads the ItemStack in memory int percent = (int) (((i + 1) / (double) total) * 100); if (nextMilestoneIndex < milestones.length && percent >= milestones[nextMilestoneIndex]) { diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/HDBSubCommandManager.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/HDBSubCommandManager.java index 894ab19..c973eaa 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/HDBSubCommandManager.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/HDBSubCommandManager.java @@ -1,16 +1,16 @@ package com.github.thesilentpro.headdb.core.command; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + import com.github.thesilentpro.headdb.core.HeadDB; import com.github.thesilentpro.headdb.core.command.sub.HDBCommandGive; import com.github.thesilentpro.headdb.core.command.sub.HDBCommandInfo; import com.github.thesilentpro.headdb.core.command.sub.HDBCommandOpen; import com.github.thesilentpro.headdb.core.command.sub.HDBCommandSearch; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - public class HDBSubCommandManager { private final int totalCommandEntries = 1 + 1; // realCommandCount + totalAliasCount @@ -37,9 +37,12 @@ public void registerDefaults() { } public void register(HDBSubCommand command) { - this.commands.put(command.getName(), command); - this.realNames.add(command.getName()); - for (String alias : command.getAliases()) { + String name = command.getName(); + this.commands.put(name, command); + this.realNames.add(name); + + String[] aliases = command.getAliases(); + for (String alias : aliases) { this.commands.put(alias, command); } } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandOpen.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandOpen.java index a89b90c..7a6568e 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandOpen.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandOpen.java @@ -1,15 +1,16 @@ package com.github.thesilentpro.headdb.core.command.sub; -import com.github.thesilentpro.headdb.core.HeadDB; -import com.github.thesilentpro.headdb.core.command.HDBSubCommand; -import com.github.thesilentpro.headdb.core.menu.gui.HeadsGUI; -import com.github.thesilentpro.headdb.core.util.Compatibility; +import java.util.Arrays; +import java.util.List; + import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.jetbrains.annotations.Nullable; -import java.util.Arrays; -import java.util.List; +import com.github.thesilentpro.headdb.core.HeadDB; +import com.github.thesilentpro.headdb.core.command.HDBSubCommand; +import com.github.thesilentpro.headdb.core.menu.gui.HeadsGUI; +import com.github.thesilentpro.headdb.core.util.Compatibility; public class HDBCommandOpen extends HDBSubCommand { @@ -17,39 +18,85 @@ public class HDBCommandOpen extends HDBSubCommand { private List completions; public HDBCommandOpen(HeadDB plugin) { - super("open", "Open the database.", "[category]", "o"); + super("open", "Open the database.", "[category] [player]", "o"); this.plugin = plugin; } @Override public void handle(CommandSender sender, String[] args) { - if (!(sender instanceof Player player)) { - plugin.getLocalization().sendMessage(sender, "noConsole"); - return; + // Определяем целевого игрока + Player targetPlayer = null; + int categoryEndIndex = args.length; + + // Проверяем, указан ли игрок в конце команды + if (args.length >= 2) { + Player possiblePlayer = plugin.getServer().getPlayer(args[args.length - 1]); + if (possiblePlayer != null) { + targetPlayer = possiblePlayer; + categoryEndIndex = args.length - 1; + + // Проверка прав на открытие меню другому игроку + if (!sender.hasPermission("headdb.command.open.others")) { + plugin.getLocalization().sendMessage(sender, "noPermission"); + if (sender instanceof Player) { + Compatibility.playSound(sender, plugin.getSoundConfig().get("noPermission")); + } + return; + } + } } - - if (args.length == 1) { - this.plugin.getMenuManager().getMainMenu().open(player); - plugin.getLocalization().sendMessage(sender, "command.open.opening", msg -> msg.replaceText(builder -> builder.matchLiteral("{category}").replacement("Main"))); - Compatibility.playSound(player, plugin.getSoundConfig().get("menu.open")); + + // Если целевой игрок не указан, используем отправителя команды + if (targetPlayer == null) { + if (!(sender instanceof Player)) { + plugin.getLocalization().sendMessage(sender, "noConsole"); + return; + } + targetPlayer = (Player) sender; + } + + final Player finalTarget = targetPlayer; + + // Открытие главного меню + if (categoryEndIndex == 1) { + this.plugin.getMenuManager().getMainMenu().open(finalTarget); + if (finalTarget.equals(sender)) { + plugin.getLocalization().sendMessage(sender, "command.open.opening"); + } else { + plugin.getLocalization().sendMessage(sender, "command.open.openingFor", + msg -> msg.replaceText(builder -> builder.matchLiteral("{menu}").replacement("Main")) + .replaceText(builder -> builder.matchLiteral("{target}").replacement(finalTarget.getName()))); + } + Compatibility.playSound(finalTarget, plugin.getSoundConfig().get("menu.open")); return; } - String category = String.join(" ", String.join(" ", Arrays.copyOfRange(args, 1, args.length))); - HeadsGUI categoryGui = plugin.getMenuManager().get(category.replace(" ", "_").replace("&", "_")); + // Открытие категории + final String categoryName = String.join(" ", Arrays.copyOfRange(args, 1, categoryEndIndex)); + HeadsGUI categoryGui = plugin.getMenuManager().get(categoryName.replace(" ", "_").replace("&", "_")); if (categoryGui == null) { - plugin.getLocalization().sendMessage(sender, "command.open.invalidCategory", msg -> msg.replaceText(builder -> builder.matchLiteral("{category}").replacement(category))); - Compatibility.playSound(player, plugin.getSoundConfig().get("failed")); + plugin.getLocalization().sendMessage(sender, "command.open.invalidCategory", + msg -> msg.replaceText(builder -> builder.matchLiteral("{category}").replacement(categoryName))); + if (sender instanceof Player) { + Compatibility.playSound(sender, plugin.getSoundConfig().get("failed")); + } return; } int pageIndex = 0; if (plugin.getCfg().isTrackPage()) { - pageIndex = categoryGui.getGuiRegistry().getCurrentPage(player.getUniqueId(), categoryGui.getKey()).orElse(0); + pageIndex = categoryGui.getGuiRegistry().getCurrentPage(finalTarget.getUniqueId(), categoryGui.getKey()).orElse(0); } - categoryGui.open(player, pageIndex); - plugin.getLocalization().sendMessage(sender, "command.open.opening", msg -> msg.replaceText(builder -> builder.matchLiteral("{category}").replacement(category))); - Compatibility.playSound(player, plugin.getSoundConfig().get("menu.open")); + categoryGui.open(finalTarget, pageIndex); + + if (finalTarget.equals(sender)) { + plugin.getLocalization().sendMessage(sender, "command.open.opening"); + } else { + plugin.getLocalization().sendMessage(sender, "command.open.openingFor", + msg -> msg.replaceText(builder -> builder.matchLiteral("{menu}").replacement(categoryName)) + .replaceText(builder -> builder.matchLiteral("{target}").replacement(finalTarget.getName()))); + } + Compatibility.playSound(finalTarget, plugin.getSoundConfig().get("menu.open")); } @Override @@ -57,6 +104,14 @@ public void handle(CommandSender sender, String[] args) { if (completions == null) { completions = plugin.getHeadApi().findKnownCategories(); } + + // Если это последний аргумент и у отправителя есть права, предлагаем имена игроков + if (args.length >= 2 && sender.hasPermission("headdb.command.open.others")) { + return plugin.getServer().getOnlinePlayers().stream() + .map(org.bukkit.entity.Player::getName) + .toList(); + } + return completions; } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandSearch.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandSearch.java index d49aaae..da31c7f 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandSearch.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/command/sub/HDBCommandSearch.java @@ -1,17 +1,23 @@ package com.github.thesilentpro.headdb.core.command.sub; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + import com.github.thesilentpro.headdb.api.model.Head; import com.github.thesilentpro.headdb.core.HeadDB; import com.github.thesilentpro.headdb.core.command.HDBSubCommand; import com.github.thesilentpro.headdb.core.menu.gui.HeadsGUI; import com.github.thesilentpro.headdb.core.util.Compatibility; -import net.kyori.adventure.text.Component; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; +import net.kyori.adventure.text.Component; public class HDBCommandSearch extends HDBSubCommand { @@ -34,8 +40,16 @@ public void handle(CommandSender sender, String[] args) { CompletableFuture.supplyAsync(() -> { // detect & strip --any // Enables loose search (match if any filter passes instead of all). - boolean any = Arrays.stream(args).anyMatch(a -> a.equalsIgnoreCase("--any")); - List parts = Arrays.stream(args, 1, args.length).filter(a -> !a.equalsIgnoreCase("--any")).toList(); + boolean any = false; + List parts = new ArrayList<>(args.length - 1); + for (int i = 1; i < args.length; i++) { + if (args[i].equalsIgnoreCase("--any")) { + any = true; + } else { + parts.add(args[i]); + } + } + final boolean finalAny = any; // parse filters String category = null; @@ -79,20 +93,23 @@ public void handle(CommandSender sender, String[] args) { .replaceText(builder -> builder.matchLiteral("{category}").replacement(finalCategory != null ? finalCategory : "/")) .replaceText(builder -> builder.matchLiteral("{tags}").replacement(!tags.isEmpty() ? String.join(",", tags) : "/")) .replaceText(builder -> builder.matchLiteral("{ids}").replacement(!ids.isEmpty() ? String.join(",", ids.stream().map(String::valueOf).toArray(String[]::new)) : "/")) - .replaceText(builder -> builder.matchLiteral("{mode}").replacement(any ? "ANY" : "ALL")) + .replaceText(builder -> builder.matchLiteral("{mode}").replacement(finalAny ? "ANY" : "ALL")) ); }); // lower all your query bits once String qCat = category == null ? null : category.toLowerCase(Locale.ROOT); String qName = nameQuery.trim().toLowerCase(Locale.ROOT); - Set tagSet = tags.stream().map(t -> t.toLowerCase(Locale.ROOT)).collect(Collectors.toSet()); + Set tagSet = new HashSet<>(tags.size()); + for (String tag : tags) { + tagSet.add(tag.toLowerCase(Locale.ROOT)); + } Set idSet = new HashSet<>(ids); List allHeads = plugin.getHeadApi().getHeads().join(); List result = new ArrayList<>(); - if (any) { + if (finalAny) { // ANY‑mode: match if _one_ of the filters hits for (Head h : allHeads) { // grab once per head diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/Config.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/Config.java index 987ff84..93fa2aa 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/Config.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/Config.java @@ -1,11 +1,12 @@ package com.github.thesilentpro.headdb.core.config; -import com.github.thesilentpro.headdb.api.model.Head; -import com.github.thesilentpro.headdb.core.HeadDB; -import com.github.thesilentpro.headdb.core.util.Compatibility; -import com.github.thesilentpro.headdb.implementation.Index; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.minimessage.MiniMessage; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + import org.bukkit.Material; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; @@ -15,8 +16,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.util.*; +import com.github.thesilentpro.headdb.api.model.Head; +import com.github.thesilentpro.headdb.core.HeadDB; +import com.github.thesilentpro.headdb.core.util.Compatibility; +import com.github.thesilentpro.headdb.implementation.Index; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; public class Config { @@ -26,8 +32,6 @@ public class Config { private static final String DEFAULT_BACK_TEXTURE = "e5da4847272582265bdaca367237c96122b139f4e597fbc6667d3fb75fea7cf6"; private static final String DEFAULT_INFO_TEXTURE = "93e5cb83cfdf42e9c4d8a3ecb4f889f6a5f418dce0a894c97e416a0eaf0d58"; private static final String DEFAULT_NEXT_TEXTURE = "62bfb7ed2bd9f1d1f85c3d6ffb1626f252c5ecfd79d51a3f56ebf8e0c3c91"; - private static final String DEFAULT_CUSTOM_CATEGORY_TEXTURE = "62bfb7ed2bd9f1d1f85c3d6ffb1626f252c5ecfd79d51a3f56ebf8e0c3c91"; - private static final String DEFAULT_SEARCH_TEXTURE = "9d9cc58ad25a1ab16d36bb5d6d493c8f5898c2bf302b64e325921c41c35867"; private final HeadDB plugin; private final FileConfiguration config; @@ -38,6 +42,7 @@ public class Config { private boolean preloadHeads, trackPage, updaterEnabled; private int maxBuyAmount; private List omit; + private boolean dropOnFullInventory; // Indexing private boolean indexingEnabled, indexById, indexByTexture, indexByCategory, indexByTag; @@ -51,8 +56,8 @@ public class Config { private Component headName; // Control items (back, next, etc.) - private String backTexture, infoTexture, nextTexture, customCategoryTexture, searchTexture; - private Material backItem, infoItem, nextItem, customCategoryItem, searchItem; + private String backTexture, infoTexture, nextTexture; + private Material backItem, infoItem, nextItem; // Economy private String economyProvider; @@ -82,6 +87,7 @@ private void loadGeneral() { apiThreads = config.getInt("database.apiThreads", 1); maxBuyAmount = config.getInt("maxBuyAmount", 2304); omit = config.getIntegerList("head.omit"); + dropOnFullInventory = config.getBoolean("head.dropOnFullInventory", true); LOGGER.trace("Loaded General Config:"); LOGGER.trace(" - playerStorageSaveInterval = {}", playerStorageSaveInterval); @@ -91,6 +97,7 @@ private void loadGeneral() { LOGGER.trace(" - databaseThreads = {}", databaseThreads); LOGGER.trace(" - apiThreads = {}", apiThreads); LOGGER.trace(" - maxBuyAmount = {}", maxBuyAmount); + LOGGER.trace(" - dropOnFullInventory = {}", dropOnFullInventory); } private void loadIndexing() { @@ -127,20 +134,20 @@ private void loadHeadsMenu() { } private void loadHeadsLore() { - List lore; - if (plugin.getCfg().getEconomyProvider() == null || plugin.getCfg().getEconomyProvider().isBlank()) { - headName = MiniMessage.miniMessage().deserialize(config.getString("head.name.default", "{name}")); - lore = config.getStringList("head.lore.default"); - } else { - headName = MiniMessage.miniMessage().deserialize(config.getString("head.name.economy", "{name}")); - lore = config.getStringList("head.lore.economy"); - } - - LOGGER.trace("Loaded heads lore template ({}):", plugin.getEconomyProvider() == null ? "no-econ" : "econ"); + boolean hasEconomy = economyProvider != null && !economyProvider.isBlank(); + + String nameKey = hasEconomy ? "head.name.economy" : "head.name.default"; + String loreKey = hasEconomy ? "head.lore.economy" : "head.lore.default"; + + headName = MiniMessage.miniMessage().deserialize(config.getString(nameKey, "{name}")); + List lore = config.getStringList(loreKey); + + LOGGER.trace("Loaded heads lore template ({}):", hasEconomy ? "econ" : "no-econ"); + MiniMessage miniMessage = MiniMessage.miniMessage(); for (String line : lore) { - this.headsLore.add(MiniMessage.miniMessage().deserialize(line)); + this.headsLore.add(miniMessage.deserialize(line)); LOGGER.trace(line); - }; + } } private void loadControls() { @@ -153,12 +160,6 @@ private void loadControls() { nextTexture = config.getString("controls.next.head", DEFAULT_NEXT_TEXTURE); nextItem = parseMaterial("controls.next.item", "ARROW"); - customCategoryTexture = config.getString("headsMenu.customCategories.head", DEFAULT_CUSTOM_CATEGORY_TEXTURE); - customCategoryItem = parseMaterial("headsMenu.customCategories.item", "BOOKSHELF"); - - searchTexture = config.getString("headsMenu.search.head", DEFAULT_SEARCH_TEXTURE); - searchItem = parseMaterial("headsMenu.search.item", "SPYGLASS"); - LOGGER.trace("Loaded Controls Config:"); LOGGER.trace(" - backTexture = {}", backTexture); LOGGER.trace(" - backItem = {}", backItem); @@ -166,16 +167,14 @@ private void loadControls() { LOGGER.trace(" - infoItem = {}", infoItem); LOGGER.trace(" - nextTexture = {}", nextTexture); LOGGER.trace(" - nextItem = {}", nextItem); - LOGGER.trace(" - customCategoryTexture = {}", customCategoryTexture); - LOGGER.trace(" - customCategoryItem = {}", customCategoryItem); - LOGGER.trace(" - searchTexture = {}", searchTexture); - LOGGER.trace(" - searchItem = {}", searchItem); } private void loadEconomy() { economyProvider = config.getString("economy.provider", null); categoryPrices.clear(); - if (!economyProvider.isEmpty() && !economyProvider.equalsIgnoreCase("NONE")) { + headPrices.clear(); + + if (economyProvider != null && !economyProvider.isEmpty() && !economyProvider.equalsIgnoreCase("NONE")) { ConfigurationSection section = config.getConfigurationSection("economy.cost.category"); if (section != null) { for (String category : section.getKeys(false)) { @@ -184,12 +183,14 @@ private void loadEconomy() { LOGGER.trace("Loaded price: category='{}' price={}", category, price); } } + ConfigurationSection headSection = config.getConfigurationSection("economy.cost.head"); if (headSection != null) { for (String headId : headSection.getKeys(false)) { try { + int id = Integer.parseInt(headId); double price = headSection.getDouble(headId, 0D); - headPrices.put(Integer.parseInt(headId), price); + headPrices.put(id, price); LOGGER.trace("Loaded price: head='{}' price='{}'", headId, price); } catch (NumberFormatException nfe) { LOGGER.error("Invalid head id '{}' in config", headId); @@ -280,15 +281,17 @@ public List resolveCustomCategories() { } public @Nullable Index[] resolveEnabledIndexes() { - if (!indexingEnabled) return null; + if (!indexingEnabled) { + return null; + } - List indexes = new ArrayList<>(); + List indexes = new ArrayList<>(4); if (indexById) indexes.add(Index.ID); if (indexByCategory) indexes.add(Index.CATEGORY); if (indexByTexture) indexes.add(Index.TEXTURE); if (indexByTag) indexes.add(Index.TAG); - return indexes.isEmpty() ? null : indexes.toArray(new Index[0]); + return indexes.isEmpty() ? null : indexes.toArray(Index[]::new); } // === Getters === @@ -308,6 +311,7 @@ public List resolveCustomCategories() { public int getApiThreads() { return apiThreads; } public int getMaxBuyAmount() { return maxBuyAmount; } public List getOmit() { return omit; } + public boolean isDropOnFullInventory() { return dropOnFullInventory; } public boolean isTrackPage() { return trackPage; } public boolean isPreloadHeads() { return preloadHeads; } @@ -324,10 +328,6 @@ public List resolveCustomCategories() { public Material getInfoItem() { return infoItem; } public String getNextTexture() { return nextTexture; } public Material getNextItem() { return nextItem; } - public String getCustomCategoryTexture() { return customCategoryTexture; } - public Material getCustomCategoryItem() { return customCategoryItem; } - public String getSearchTexture() { return searchTexture; } - public Material getSearchItem() { return searchItem; } public @Nullable String getEconomyProvider() { return economyProvider; } public double getCategoryPrice(String id) { return categoryPrices.getOrDefault(id, 0D); } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/ConfigManager.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/ConfigManager.java index 29184f4..0191796 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/ConfigManager.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/ConfigManager.java @@ -1,12 +1,13 @@ package com.github.thesilentpro.headdb.core.config; -import com.github.thesilentpro.headdb.core.HeadDB; +import java.io.File; + import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.plugin.java.JavaPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; +import com.github.thesilentpro.headdb.core.HeadDB; public class ConfigManager { @@ -14,10 +15,12 @@ public class ConfigManager { private final Config config; private final SoundConfig soundConfig; + private final MenuConfig menuConfig; public ConfigManager(HeadDB plugin) { this.config = new Config(plugin); this.soundConfig = new SoundConfig(); + this.menuConfig = new MenuConfig(plugin.getDataFolder()); } public void loadAll(JavaPlugin plugin) { @@ -34,6 +37,24 @@ public void loadAll(JavaPlugin plugin) { soundConfig.load(YamlConfiguration.loadConfiguration(soundFile)); LOGGER.debug("Loaded sounds.yml in {}ms", System.currentTimeMillis() - soundsStart); + + // Load menu configs + long menusStart = System.currentTimeMillis(); + loadMenuConfigs(plugin); + LOGGER.debug("Loaded menu configs in {}ms", System.currentTimeMillis() - menusStart); + } + + private void loadMenuConfigs(JavaPlugin plugin) { + String[] menuFiles = {"main", "category", "favorites", "local", "custom_categories", "purchase", "search"}; + File menusFolder = new File(plugin.getDataFolder(), "menus"); + + for (String menuFile : menuFiles) { + File file = new File(menusFolder, menuFile + ".yml"); + if (!file.exists()) { + plugin.saveResource("menus/" + menuFile + ".yml", false); + } + menuConfig.load(menuFile); + } } public Config getConfig() { @@ -44,4 +65,8 @@ public SoundConfig getSoundConfig() { return soundConfig; } + public MenuConfig getMenuConfig() { + return menuConfig; + } + } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/MenuConfig.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/MenuConfig.java new file mode 100644 index 0000000..6fde749 --- /dev/null +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/config/MenuConfig.java @@ -0,0 +1,180 @@ +package com.github.thesilentpro.headdb.core.config; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; + +public class MenuConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(MenuConfig.class); + + private final Map configs = new HashMap<>(); + private final File menusFolder; + + public MenuConfig(File dataFolder) { + this.menusFolder = new File(dataFolder, "menus"); + if (!menusFolder.exists()) { + menusFolder.mkdirs(); + } + } + + public void load(String menuName) { + File file = new File(menusFolder, menuName + ".yml"); + if (file.exists()) { + try { + YamlConfiguration config = YamlConfiguration.loadConfiguration(file); + configs.put(menuName, config); + LOGGER.debug("Loaded menu config: {}", menuName); + } catch (Exception e) { + LOGGER.error("Failed to load menu config: {}", menuName, e); + } + } + } + + public YamlConfiguration get(String menuName) { + return configs.get(menuName); + } + + public String getTitle(String menuName, String defaultTitle) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains("title")) { + return config.getString("title", defaultTitle); + } + return defaultTitle; + } + + public int getSize(String menuName, int defaultSize) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains("size")) { + return config.getInt("size", defaultSize); + } + return defaultSize; + } + + public List getSlots(String menuName, String path, List defaultSlots) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains(path)) { + return config.getIntegerList(path); + } + return defaultSlots; + } + + public boolean isButtonEnabled(String menuName, String buttonPath) { + YamlConfiguration config = configs.get(menuName); + if (config != null) { + return config.getBoolean(buttonPath + ".enabled", true); + } + return true; + } + + public int getButtonSlot(String menuName, String buttonPath, int defaultSlot) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains(buttonPath + ".slot")) { + return config.getInt(buttonPath + ".slot", defaultSlot); + } + return defaultSlot; + } + + public String getButtonName(String menuName, String buttonPath, String defaultName) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains(buttonPath + ".name")) { + return config.getString(buttonPath + ".name", defaultName); + } + return defaultName; + } + + public List getButtonLore(String menuName, String buttonPath) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains(buttonPath + ".lore")) { + return config.getStringList(buttonPath + ".lore"); + } + return new ArrayList<>(); + } + + public Material getButtonMaterial(String menuName, String buttonPath, Material defaultMaterial) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains(buttonPath + ".material")) { + try { + return Material.valueOf(config.getString(buttonPath + ".material")); + } catch (IllegalArgumentException e) { + LOGGER.warn("Invalid material in {}.{}: {}", menuName, buttonPath, config.getString(buttonPath + ".material")); + } + } + return defaultMaterial; + } + + public String getButtonTexture(String menuName, String buttonPath) { + YamlConfiguration config = configs.get(menuName); + if (config != null && config.contains(buttonPath + ".texture")) { + return config.getString(buttonPath + ".texture", ""); + } + return ""; + } + + public ConfigurationSection getSection(String menuName, String path) { + YamlConfiguration config = configs.get(menuName); + if (config != null) { + return config.getConfigurationSection(path); + } + return null; + } + + public boolean isBorderEnabled(String menuName) { + YamlConfiguration config = configs.get(menuName); + if (config != null) { + return config.getBoolean("border.enabled", true); + } + return true; + } + + public Material getBorderMaterial(String menuName, Material defaultMaterial) { + return getButtonMaterial(menuName, "border", defaultMaterial); + } + + public String getBorderName(String menuName) { + return getButtonName(menuName, "border", ""); + } + + public Component parseComponent(String text) { + if (text == null || text.isEmpty()) { + return Component.empty(); + } + try { + return MiniMessage.miniMessage().deserialize(text); + } catch (Exception e) { + LOGGER.warn("Failed to parse MiniMessage: {}", text, e); + return Component.text(text); + } + } + + public List parseComponentList(List texts) { + return texts.stream() + .map(this::parseComponent) + .collect(Collectors.toList()); + } + + public Component getButtonNameComponent(String menuName, String buttonPath, Component defaultName) { + String name = getButtonName(menuName, buttonPath, null); + if (name != null) { + return parseComponent(name); + } + return defaultName; + } + + public List getButtonLoreComponents(String menuName, String buttonPath) { + List lore = getButtonLore(menuName, buttonPath); + return parseComponentList(lore); + } +} diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/factory/ItemFactory.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/factory/ItemFactory.java index 449a3cd..4145178 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/factory/ItemFactory.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/factory/ItemFactory.java @@ -1,7 +1,15 @@ package com.github.thesilentpro.headdb.core.factory; -import com.github.thesilentpro.headdb.api.model.Head; -import net.kyori.adventure.text.Component; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +import javax.annotation.Nullable; + import org.bukkit.Material; import org.bukkit.OfflinePlayer; import org.bukkit.entity.Player; @@ -9,8 +17,9 @@ import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.ApiStatus; -import javax.annotation.Nullable; -import java.util.*; +import com.github.thesilentpro.headdb.api.model.Head; + +import net.kyori.adventure.text.Component; /** * Represents an item factory for per server implementation creation of ItemStacks. @@ -35,7 +44,7 @@ public interface ItemFactory { ItemStack newItem(Material material, Component name, Component... lore); - default void giveItem(Player player, Collection omit, ItemStack... items) { + default void giveItem(Player player, Collection omit, boolean dropOnFullInventory, ItemStack... items) { // Use a HashSet for O(1) lookups if omit is not null and not empty Set omitSet = (omit != null && !omit.isEmpty()) ? (omit instanceof Set ? (Set) omit : new HashSet<>(omit)) : null; @@ -68,19 +77,23 @@ default void giveItem(Player player, Collection omit, ItemStack... item } } - Map leftovers = player.getInventory().addItem(items); - for (ItemStack missed : leftovers.values()) { - int leftover = missed.getAmount(); - final int maxStack = missed.getMaxStackSize(); - - // Drop items in maxStack-size chunks efficiently - while (leftover > 0) { - int dropAmount = (leftover > maxStack) ? maxStack : leftover; - ItemStack toDrop = missed.clone(); - toDrop.setAmount(dropAmount); - player.getWorld().dropItemNaturally(player.getLocation(), toDrop); - leftover -= dropAmount; + if (dropOnFullInventory) { + Map leftovers = player.getInventory().addItem(items); + for (ItemStack missed : leftovers.values()) { + int leftover = missed.getAmount(); + final int maxStack = missed.getMaxStackSize(); + + // Drop items in maxStack-size chunks efficiently + while (leftover > 0) { + int dropAmount = (leftover > maxStack) ? maxStack : leftover; + ItemStack toDrop = missed.clone(); + toDrop.setAmount(dropAmount); + player.getWorld().dropItemNaturally(player.getLocation(), toDrop); + leftover -= dropAmount; + } } + } else { + player.getInventory().addItem(items); } } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/CustomCategoriesMenu.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/CustomCategoriesMenu.java index de93c34..a24aebb 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/CustomCategoriesMenu.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/CustomCategoriesMenu.java @@ -1,21 +1,28 @@ package com.github.thesilentpro.headdb.core.menu; +import java.util.List; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; + import com.github.thesilentpro.grim.button.SimpleButton; import com.github.thesilentpro.grim.gui.GUI; import com.github.thesilentpro.grim.page.PaginatedSimplePage; import com.github.thesilentpro.headdb.core.HeadDB; import com.github.thesilentpro.headdb.core.config.CustomCategory; import com.github.thesilentpro.headdb.core.util.Compatibility; -import net.kyori.adventure.text.Component; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import java.util.List; +import net.kyori.adventure.text.Component; public class CustomCategoriesMenu extends PaginatedSimplePage { public CustomCategoriesMenu(HeadDB plugin, GUI gui, Component title, List categories) { - super(gui, title, 6, 48, 49, 50); + super(gui, title, + plugin.getMenuConfig().getSize("custom_categories", 6), + plugin.getMenuConfig().getButtonSlot("custom_categories", "controls.back", 48), + plugin.getMenuConfig().getButtonSlot("custom_categories", "controls.info", 49), + plugin.getMenuConfig().getButtonSlot("custom_categories", "controls.next", 50) + ); preventInteraction(); for (CustomCategory category : categories) { addButton(new SimpleButton(category.getIcon(), ctx -> { diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/FavoritesHeadsMenu.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/FavoritesHeadsMenu.java index 26d5a4d..9997e97 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/FavoritesHeadsMenu.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/FavoritesHeadsMenu.java @@ -1,5 +1,12 @@ package com.github.thesilentpro.headdb.core.menu; +import java.util.List; +import java.util.UUID; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + import com.github.thesilentpro.grim.button.SimpleButton; import com.github.thesilentpro.grim.gui.GUI; import com.github.thesilentpro.grim.page.PaginatedSimplePage; @@ -8,18 +15,18 @@ import com.github.thesilentpro.headdb.core.factory.ItemFactoryRegistry; import com.github.thesilentpro.headdb.core.storage.PlayerData; import com.github.thesilentpro.headdb.core.util.Compatibility; -import net.kyori.adventure.text.Component; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.inventory.ItemStack; -import java.util.List; -import java.util.UUID; +import net.kyori.adventure.text.Component; public class FavoritesHeadsMenu extends PaginatedSimplePage { public FavoritesHeadsMenu(HeadDB plugin, GUI gui, Component title, List heads, List items) { - super(gui, title, 6, 48, 49, 50); + super(gui, title, + plugin.getMenuConfig().getSize("favorites", 6), + plugin.getMenuConfig().getButtonSlot("favorites", "controls.back", 48), + plugin.getMenuConfig().getButtonSlot("favorites", "controls.info", 49), + plugin.getMenuConfig().getButtonSlot("favorites", "controls.next", 50) + ); preventInteraction(); for (Head head : heads) { @@ -52,7 +59,7 @@ public FavoritesHeadsMenu(HeadDB plugin, GUI gui, Component title, List new PurchaseHeadMenu(plugin, player, head, this).open(player); } else { ItemStack item = head.getItem(); - ItemFactoryRegistry.get().giveItem((Player) ctx.event().getWhoClicked(), plugin.getCfg().getOmit(), item); + ItemFactoryRegistry.get().giveItem((Player) ctx.event().getWhoClicked(), plugin.getCfg().getOmit(), plugin.getCfg().isDropOnFullInventory(), item); plugin.getLocalization().sendMessage(player, "purchase.noEconomy", msg -> msg.replaceText(builder -> builder .matchLiteral("{amount}").replacement(String.valueOf(item.getAmount()))) @@ -91,7 +98,7 @@ public FavoritesHeadsMenu(HeadDB plugin, GUI gui, Component title, List return; } - ItemFactoryRegistry.get().giveItem(player, plugin.getCfg().getOmit(), item); + ItemFactoryRegistry.get().giveItem(player, plugin.getCfg().getOmit(), plugin.getCfg().isDropOnFullInventory(), item); Compatibility.playSound(player, plugin.getSoundConfig().get("head.take")); })); } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/HeadsMenu.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/HeadsMenu.java index 7264928..5d6d35d 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/HeadsMenu.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/HeadsMenu.java @@ -1,5 +1,12 @@ package com.github.thesilentpro.headdb.core.menu; +import java.util.List; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + import com.github.thesilentpro.grim.button.SimpleButton; import com.github.thesilentpro.grim.gui.GUI; import com.github.thesilentpro.grim.page.PaginatedSimplePage; @@ -8,17 +15,18 @@ import com.github.thesilentpro.headdb.core.factory.ItemFactoryRegistry; import com.github.thesilentpro.headdb.core.storage.PlayerData; import com.github.thesilentpro.headdb.core.util.Compatibility; -import net.kyori.adventure.text.Component; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.inventory.ItemStack; -import java.util.List; +import net.kyori.adventure.text.Component; public class HeadsMenu extends PaginatedSimplePage { public HeadsMenu(HeadDB plugin, GUI gui, Component title, List heads) { - super(gui, title, 6, 48, 49, 50); + super(gui, title, + plugin.getMenuConfig().getSize("category", 6), + plugin.getMenuConfig().getButtonSlot("category", "controls.back", 48), + plugin.getMenuConfig().getButtonSlot("category", "controls.info", 49), + plugin.getMenuConfig().getButtonSlot("category", "controls.next", 50) + ); preventInteraction(); for (Head head : heads) { addButton(new SimpleButton(head.getItem(), ctx -> { @@ -46,12 +54,30 @@ public HeadsMenu(HeadDB plugin, GUI gui, Component title, List he Compatibility.playSound((Player) ctx.event().getWhoClicked(), plugin.getSoundConfig().get("menu.open")); } else { ItemStack item = head.getItem(); - ItemFactoryRegistry.get().giveItem((Player) ctx.event().getWhoClicked(), plugin.getCfg().getOmit(), item); + ItemFactoryRegistry.get().giveItem((Player) ctx.event().getWhoClicked(), plugin.getCfg().getOmit(), plugin.getCfg().isDropOnFullInventory(), item); plugin.getLocalization().sendMessage(ctx.event().getWhoClicked(), "purchase.noEconomy", msg -> msg.replaceText(builder -> builder.matchLiteral("{amount}").replacement(String.valueOf(item.getAmount()))).replaceText(builder -> builder.matchLiteral("{name}").replacement(head.getName()))); Compatibility.playSound((Player) ctx.event().getWhoClicked(), plugin.getSoundConfig().get("head.take")); } })); } + + // Apply border if enabled in config + if (plugin.getMenuConfig().isBorderEnabled("category")) { + Material borderMaterial = plugin.getMenuConfig().getBorderMaterial("category", Material.GRAY_STAINED_GLASS_PANE); + String borderName = plugin.getMenuConfig().getBorderName("category"); + Component name = borderName.isEmpty() ? Component.text("") : plugin.getMenuConfig().parseComponent(borderName); + + ItemStack borderItem = Compatibility.newItem(borderMaterial, name); + SimpleButton borderButton = new SimpleButton(borderItem); + + // Fill empty slots with border + for (int i = 0; i < getSize(); i++) { + if (getButton(i).isEmpty()) { + setButton(i, borderButton); + } + } + } + reRender(); } diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/LocalHeadsMenu.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/LocalHeadsMenu.java index c447e3f..bf38eff 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/LocalHeadsMenu.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/LocalHeadsMenu.java @@ -1,5 +1,12 @@ package com.github.thesilentpro.headdb.core.menu; +import java.util.List; +import java.util.UUID; + +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.inventory.ItemStack; + import com.github.thesilentpro.grim.button.SimpleButton; import com.github.thesilentpro.grim.gui.GUI; import com.github.thesilentpro.grim.page.PaginatedSimplePage; @@ -7,18 +14,18 @@ import com.github.thesilentpro.headdb.core.factory.ItemFactoryRegistry; import com.github.thesilentpro.headdb.core.storage.PlayerData; import com.github.thesilentpro.headdb.core.util.Compatibility; -import net.kyori.adventure.text.Component; -import org.bukkit.entity.Player; -import org.bukkit.event.inventory.ClickType; -import org.bukkit.inventory.ItemStack; -import java.util.List; -import java.util.UUID; +import net.kyori.adventure.text.Component; public class LocalHeadsMenu extends PaginatedSimplePage { public LocalHeadsMenu(HeadDB plugin, GUI gui, Component title, List items) { - super(gui, title, 6, 48, 49, 50); + super(gui, title, + plugin.getMenuConfig().getSize("local", 6), + plugin.getMenuConfig().getButtonSlot("local", "controls.back", 48), + plugin.getMenuConfig().getButtonSlot("local", "controls.info", 49), + plugin.getMenuConfig().getButtonSlot("local", "controls.next", 50) + ); preventInteraction(); for (ItemStack item : items) { @@ -45,7 +52,7 @@ public LocalHeadsMenu(HeadDB plugin, GUI gui, Component title, List slotsList = plugin.getMenuConfig().getSlots("main", "categorySlots", + Arrays.asList(11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 29, 30, 31, 32, 33)); + this.categorySlots = slotsList.stream().mapToInt(Integer::intValue).toArray(); plugin.getHeadApi().onReady().thenAccept(heads -> { LOGGER.debug("RENDER THREAD = {}", Thread.currentThread().getName()); @@ -42,13 +66,20 @@ public MainMenu(HeadDB plugin) { renderCustomCategoriesButton(plugin); renderSearchButton(plugin); renderInfoButton(plugin); - fillBorder(this); + fillBorder(plugin, this); reRender(); }); } private void renderCategoryButtons(HeadDB plugin, List heads) { - List preferredOrder = List.of("Alphabet", "Animals", "Blocks", "Decoration", "Food & Drinks", "Humanoid", "Humans", "Miscellaneous", "Monsters", "Plants"); + // Get preferred order from config + YamlConfiguration config = plugin.getMenuConfig().get("main"); + List preferredOrder; + if (config != null && config.contains("preferredCategoryOrder")) { + preferredOrder = config.getStringList("preferredCategoryOrder"); + } else { + preferredOrder = List.of("Alphabet", "Animals", "Blocks", "Decoration", "Food & Drinks", "Humanoid", "Humans", "Miscellaneous", "Monsters", "Plants"); + } Map headByCategory = new HashMap<>(); for (Head head : heads) { @@ -63,18 +94,46 @@ private void renderCategoryButtons(HeadDB plugin, List heads) { }); headByCategory.keySet().stream().filter(seen::add).forEach(orderedCategories::add); - for (int i = 0; i < Math.min(CATEGORY_SLOTS.length, orderedCategories.size()); i++) { + for (int i = 0; i < Math.min(categorySlots.length, orderedCategories.size()); i++) { String category = orderedCategories.get(i); + + // Check if category button is enabled in config + String categoryPath = "categoryButtons." + category; + if (!plugin.getMenuConfig().isButtonEnabled("main", categoryPath)) { + continue; + } + + // Get slot from config or use default from categorySlots + int slot = plugin.getMenuConfig().getButtonSlot("main", categoryPath, categorySlots[i]); + + // Get texture from config or use head from database + String texture = plugin.getMenuConfig().getButtonTexture("main", categoryPath); Head head = headByCategory.get(category); - if (head == null) { + + // Get name from config or use localization + Component name = plugin.getMenuConfig().getButtonNameComponent("main", categoryPath, + plugin.getLocalization().getConsoleMessage("menu.main.category." + category.toLowerCase(Locale.ROOT) + ".name") + .orElse(Component.text(category).color(NamedTextColor.GOLD))); + + // Get lore from config + List loreList = plugin.getMenuConfig().getButtonLoreComponents("main", categoryPath); + Component[] lore = loreList.isEmpty() ? new Component[]{Component.text("")} : loreList.toArray(new Component[0]); + + ItemStack item; + if (!texture.isEmpty()) { + // Use custom texture from config + Material fallbackMaterial = plugin.getMenuConfig().getButtonMaterial("main", categoryPath, Material.PLAYER_HEAD); + item = plugin.getHeadApi().findByTexture(texture).join() + .map(h -> Compatibility.setItemDetails(h.getItem(), name, lore)) + .orElse(Compatibility.newItem(fallbackMaterial, name, lore)); + } else if (head != null) { + // Use head from database + item = Compatibility.setItemDetails(head.getItem(), name, lore); + } else { continue; } - Component name = plugin.getLocalization().getConsoleMessage("menu.main.category." + category.toLowerCase(Locale.ROOT) + ".name") - .orElse(Component.text(category).color(NamedTextColor.GOLD)); - ItemStack item = Compatibility.setItemDetails(head.getItem(), name, Component.text("")); - - setButton(CATEGORY_SLOTS[i], new SimpleButton(item, ctx -> { + setButton(slot, new SimpleButton(item, ctx -> { Player player = (Player) ctx.event().getWhoClicked(); if (!player.hasPermission("headdb.category." + category)) { plugin.getLocalization().sendMessage(player, "noPermission"); @@ -91,13 +150,29 @@ private void renderCategoryButtons(HeadDB plugin, List heads) { } private void renderLocalButton(HeadDB plugin) { + if (!plugin.getMenuConfig().isButtonEnabled("main", "buttons.local")) { + return; + } + + int slot = plugin.getMenuConfig().getButtonSlot("main", "buttons.local", 41); + String texture = plugin.getMenuConfig().getButtonTexture("main", "buttons.local"); + if (texture.isEmpty()) { + texture = "7f6bf958abd78295eed6ffc293b1aa59526e80f54976829ea068337c2f5e8"; + } + + Material fallbackMaterial = plugin.getMenuConfig().getButtonMaterial("main", "buttons.local", Material.COMPASS); + Component name = plugin.getMenuConfig().getButtonNameComponent("main", "buttons.local", + getMsg(plugin, "menu.main.local.name", "Local Heads", NamedTextColor.AQUA)); + List loreList = plugin.getMenuConfig().getButtonLoreComponents("main", "buttons.local"); + Component[] lore = loreList.isEmpty() ? new Component[]{Component.text("")} : loreList.toArray(new Component[0]); + ItemStack item = plugin.getHeadApi() - .findByTexture("7f6bf958abd78295eed6ffc293b1aa59526e80f54976829ea068337c2f5e8") + .findByTexture(texture) .join() - .map(head -> Compatibility.setItemDetails(head.getItem(), getMsg(plugin, "menu.main.local.name", "Local Heads", NamedTextColor.AQUA), Component.text(""))) - .orElse(Compatibility.newItem(Material.COMPASS, getMsg(plugin, "menu.main.local.name", "Local Heads", NamedTextColor.AQUA), Component.text(""))); + .map(head -> Compatibility.setItemDetails(head.getItem(), name, lore)) + .orElse(Compatibility.newItem(fallbackMaterial, name, lore)); - setButton(41, new SimpleButton(item, ctx -> { + setButton(slot, new SimpleButton(item, ctx -> { Player player = (Player) ctx.event().getWhoClicked(); if (!player.hasPermission("headdb.category.local")) { plugin.getLocalization().sendMessage(player, "noPermission"); @@ -112,7 +187,12 @@ private void renderLocalButton(HeadDB plugin) { return; } - LocalHeadsGUI gui = new LocalHeadsGUI(plugin, "local_" + player.getUniqueId(), getMsg(plugin, "menu.local.name", "HeadDB » Local", NamedTextColor.GOLD), localHeads); + Component localTitle = plugin.getMenuConfig().parseComponent( + plugin.getMenuConfig().getTitle("local", + getMsg(plugin, "menu.local.name", "HeadDB » Local", NamedTextColor.GOLD).toString() + ) + ); + LocalHeadsGUI gui = new LocalHeadsGUI(plugin, "local_" + player.getUniqueId(), localTitle, localHeads); int page = plugin.getCfg().isTrackPage() ? gui.getGuiRegistry().getCurrentPage(player.getUniqueId(), gui.getKey()).orElse(0) : 0; gui.open(player, page); Compatibility.playSound(player, plugin.getSoundConfig().get("menu.open")); @@ -120,13 +200,29 @@ private void renderLocalButton(HeadDB plugin) { } private void renderFavoritesButton(HeadDB plugin) { + if (!plugin.getMenuConfig().isButtonEnabled("main", "buttons.favorites")) { + return; + } + + int slot = plugin.getMenuConfig().getButtonSlot("main", "buttons.favorites", 42); + String texture = plugin.getMenuConfig().getButtonTexture("main", "buttons.favorites"); + if (texture.isEmpty()) { + texture = "76fdd4b13d54f6c91dd5fa765ec93dd9458b19f8aa34eeb5c80f455b119f278"; + } + + Material fallbackMaterial = plugin.getMenuConfig().getButtonMaterial("main", "buttons.favorites", Material.BOOK); + Component name = plugin.getMenuConfig().getButtonNameComponent("main", "buttons.favorites", + getMsg(plugin, "menu.main.favorites.name", "Favorites", NamedTextColor.YELLOW)); + List loreList = plugin.getMenuConfig().getButtonLoreComponents("main", "buttons.favorites"); + Component[] lore = loreList.isEmpty() ? new Component[]{Component.text("")} : loreList.toArray(new Component[0]); + ItemStack item = plugin.getHeadApi() - .findByTexture("76fdd4b13d54f6c91dd5fa765ec93dd9458b19f8aa34eeb5c80f455b119f278") + .findByTexture(texture) .join() - .map(head -> Compatibility.setItemDetails(head.getItem(), getMsg(plugin, "menu.main.favorites.name", "Favorites", NamedTextColor.YELLOW), Component.text(""))) - .orElse(Compatibility.newItem(Material.BOOK, getMsg(plugin, "menu.main.favorites.name", "Favorites", NamedTextColor.YELLOW), Component.text(""))); + .map(head -> Compatibility.setItemDetails(head.getItem(), name, lore)) + .orElse(Compatibility.newItem(fallbackMaterial, name, lore)); - setButton(42, new SimpleButton(item, ctx -> { + setButton(slot, new SimpleButton(item, ctx -> { Player player = (Player) ctx.event().getWhoClicked(); if (!player.hasPermission("headdb.category.favorites")) { plugin.getLocalization().sendMessage(player, "noPermission"); @@ -153,7 +249,12 @@ private void renderFavoritesButton(HeadDB plugin) { Compatibility.playSound(player, plugin.getSoundConfig().get("menu.none")); return; } - FavoritesHeadsGUI gui = new FavoritesHeadsGUI(plugin, "favorites_" + playerId, getMsg(plugin, "menu.favorites.name", "HeadDB » Favorites", NamedTextColor.GOLD), favoriteHeads, localItems); + Component favTitle = plugin.getMenuConfig().parseComponent( + plugin.getMenuConfig().getTitle("favorites", + getMsg(plugin, "menu.favorites.name", "HeadDB » Favorites", NamedTextColor.GOLD).toString() + ) + ); + FavoritesHeadsGUI gui = new FavoritesHeadsGUI(plugin, "favorites_" + playerId, favTitle, favoriteHeads, localItems); int page = plugin.getCfg().isTrackPage() ? gui.getGuiRegistry().getCurrentPage(playerId, gui.getKey()).orElse(0) : 0; gui.open(player, page); Compatibility.playSound(player, plugin.getSoundConfig().get("menu.open")); @@ -166,11 +267,27 @@ private void renderFavoritesButton(HeadDB plugin) { } private void renderCustomCategoriesButton(HeadDB plugin) { - ItemStack item = plugin.getHeadApi().findByTexture(plugin.getCfg().getCustomCategoryTexture()).join() - .map(head -> Compatibility.setItemDetails(head.getItem(), getMsg(plugin, "menu.main.customCategories.name", "More Categories", NamedTextColor.DARK_PURPLE), Component.text(""))) - .orElse(Compatibility.newItem(plugin.getCfg().getCustomCategoryItem(), getMsg(plugin, "menu.main.customCategories.name", "More Categories", NamedTextColor.DARK_PURPLE), Component.text(""))); + if (!plugin.getMenuConfig().isButtonEnabled("main", "buttons.customCategories")) { + return; + } + + int slot = plugin.getMenuConfig().getButtonSlot("main", "buttons.customCategories", 38); + String texture = plugin.getMenuConfig().getButtonTexture("main", "buttons.customCategories"); + if (texture.isEmpty()) { + texture = "e7bc251a6cb0d6d9f05c5711911a6ec24b209dbe64267901a4b03761debcf738"; + } + + Material fallbackMaterial = plugin.getMenuConfig().getButtonMaterial("main", "buttons.customCategories", Material.NETHER_STAR); + Component name = plugin.getMenuConfig().getButtonNameComponent("main", "buttons.customCategories", + getMsg(plugin, "menu.main.customCategories.name", "More Categories", NamedTextColor.DARK_PURPLE)); + List loreList = plugin.getMenuConfig().getButtonLoreComponents("main", "buttons.customCategories"); + Component[] lore = loreList.isEmpty() ? new Component[]{Component.text("")} : loreList.toArray(new Component[0]); + + ItemStack item = plugin.getHeadApi().findByTexture(texture).join() + .map(head -> Compatibility.setItemDetails(head.getItem(), name, lore)) + .orElse(Compatibility.newItem(fallbackMaterial, name, lore)); - setButton(38, new SimpleButton(item, ctx -> { + setButton(slot, new SimpleButton(item, ctx -> { Player player = (Player) ctx.event().getWhoClicked(); if (!player.hasPermission("headdb.category.custom")) { plugin.getLocalization().sendMessage(player, "noPermission"); @@ -193,11 +310,27 @@ private void renderCustomCategoriesButton(HeadDB plugin) { } private void renderSearchButton(HeadDB plugin) { - ItemStack item = plugin.getHeadApi().findByTexture(plugin.getCfg().getSearchTexture()).join() - .map(head -> Compatibility.setItemDetails(head.getItem(), getMsg(plugin, "menu.main.search.name", "Search", NamedTextColor.GREEN), Component.text(""))) - .orElse(Compatibility.newItem(plugin.getCfg().getSearchItem(), getMsg(plugin, "menu.main.search.name", "Search", NamedTextColor.GREEN), Component.text(""))); + if (!plugin.getMenuConfig().isButtonEnabled("main", "buttons.search")) { + return; + } + + int slot = plugin.getMenuConfig().getButtonSlot("main", "buttons.search", 39); + String texture = plugin.getMenuConfig().getButtonTexture("main", "buttons.search"); + if (texture.isEmpty()) { + texture = "9d9cc58ad25a1ab16d36bb5d6d493c8f5898c2bf302b64e325921c41c35867"; + } + + Material fallbackMaterial = plugin.getMenuConfig().getButtonMaterial("main", "buttons.search", Material.COMPASS); + Component name = plugin.getMenuConfig().getButtonNameComponent("main", "buttons.search", + getMsg(plugin, "menu.main.search.name", "Search", NamedTextColor.GREEN)); + List loreList = plugin.getMenuConfig().getButtonLoreComponents("main", "buttons.search"); + Component[] lore = loreList.isEmpty() ? new Component[]{Component.text("")} : loreList.toArray(new Component[0]); + + ItemStack item = plugin.getHeadApi().findByTexture(texture).join() + .map(head -> Compatibility.setItemDetails(head.getItem(), name, lore)) + .orElse(Compatibility.newItem(fallbackMaterial, name, lore)); - setButton(39, new SimpleButton(item, ctx -> { + setButton(slot, new SimpleButton(item, ctx -> { if (!ctx.event().getWhoClicked().hasPermission("headdb.command.search")) { plugin.getLocalization().sendMessage(ctx.event().getWhoClicked(), "noPermission"); Compatibility.playSound(ctx.event().getWhoClicked(), plugin.getSoundConfig().get("noPermission")); @@ -219,31 +352,41 @@ private void renderSearchButton(HeadDB plugin) { } private void renderInfoButton(HeadDB plugin) { - if (!plugin.getCfg().isShowInfoItem()) { + if (!plugin.getMenuConfig().isButtonEnabled("main", "buttons.info")) { return; } - Component[] lore = new Component[]{ - Component.text("❓ Didn't spot the perfect head in our collection?").color(NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false), - Component.text("🎯 We're always adding more — and you can help!").color(NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false), - Component.text(""), - Component.text("📥 Submit your favorite or original heads").color(NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false), - Component.text("✨ Directly through our community Discord!").color(NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false), - Component.text(""), - Component.text("🔗 Discord > https://discord.gg/RJsVvVd").color(NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false) - }; + int slot = plugin.getMenuConfig().getButtonSlot("main", "buttons.info", 53); + String texture = plugin.getMenuConfig().getButtonTexture("main", "buttons.info"); + if (texture.isEmpty()) { + texture = "16439d2e306b225516aa9a6d007a7e75edd2d5015d113b42f44be62a517e574f"; + } + + Material fallbackMaterial = plugin.getMenuConfig().getButtonMaterial("main", "buttons.info", Material.BOOK); + Component name = plugin.getMenuConfig().getButtonNameComponent("main", "buttons.info", + Component.text("Can't find the head you're looking for?").color(NamedTextColor.RED)); + List loreList = plugin.getMenuConfig().getButtonLoreComponents("main", "buttons.info"); + Component[] lore = loreList.toArray(new Component[0]); ItemStack item = plugin.getHeadApi() - .findByTexture("16439d2e306b225516aa9a6d007a7e75edd2d5015d113b42f44be62a517e574f") + .findByTexture(texture) .join() - .map(head -> Compatibility.setItemDetails(head.getItem(), Component.text("Can't find the head you're looking for?").color(NamedTextColor.RED), lore)) - .orElse(Compatibility.newItem(Material.BOOK, Component.text("Can't find the head you're looking for?").color(NamedTextColor.RED), lore)); + .map(head -> Compatibility.setItemDetails(head.getItem(), name, lore)) + .orElse(Compatibility.newItem(fallbackMaterial, name, lore)); - setButton(53, new SimpleButton(item, ctx -> Compatibility.sendMessage(ctx.event().getWhoClicked(), Component.text("Click to join: https://discord.gg/RJsVvVd").color(NamedTextColor.AQUA)))); + setButton(slot, new SimpleButton(item, ctx -> Compatibility.sendMessage(ctx.event().getWhoClicked(), Component.text("Click to join: https://discord.gg/RJsVvVd").color(NamedTextColor.AQUA)))); } - private void fillBorder(Page page) { - Button filler = new SimpleButton(Compatibility.newItem(Material.BLACK_STAINED_GLASS_PANE, Component.text(""))); + private void fillBorder(HeadDB plugin, Page page) { + if (!plugin.getMenuConfig().isBorderEnabled("main")) { + return; + } + + Material borderMaterial = plugin.getMenuConfig().getBorderMaterial("main", Material.BLACK_STAINED_GLASS_PANE); + String borderName = plugin.getMenuConfig().getBorderName("main"); + Component name = borderName.isEmpty() ? Component.text("") : plugin.getMenuConfig().parseComponent(borderName); + + Button filler = new SimpleButton(Compatibility.newItem(borderMaterial, name)); for (int i = 0; i < page.getSize(); i++) { if (page.getButton(i).isEmpty()) { page.setButton(i, filler); diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/MenuManager.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/MenuManager.java index 32adeae..9085d8a 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/MenuManager.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/MenuManager.java @@ -1,21 +1,23 @@ package com.github.thesilentpro.headdb.core.menu; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.github.thesilentpro.headdb.core.HeadDB; import com.github.thesilentpro.headdb.core.config.CustomCategory; import com.github.thesilentpro.headdb.core.menu.gui.CustomCategoriesGUI; import com.github.thesilentpro.headdb.core.menu.gui.HeadsGUI; import com.github.thesilentpro.headdb.core.util.Compatibility; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.minimessage.MiniMessage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.stream.Collectors; public class MenuManager { @@ -34,7 +36,33 @@ public void registerDefaults(HeadDB plugin) { plugin.getHeadApi().findByCategory(knownCategory).thenAcceptAsync(heads -> { try { String key = knownCategory.replace(" ", "_").replace("&", "_"); - register(key, new HeadsGUI(plugin, key, plugin.getLocalization().getConsoleMessage("menu.category." + knownCategory.toLowerCase(Locale.ROOT)).orElseGet(() -> Component.text("HeadDB » " + knownCategory).color(NamedTextColor.GOLD)), heads)); + + // Try to get title from categoryTitles first + String titleStr = plugin.getMenuConfig().getButtonName("category", "categoryTitles." + knownCategory, null); + Component title; + + if (titleStr != null) { + // Use specific category title + title = plugin.getMenuConfig().parseComponent(titleStr); + } else { + // Use template with {category} placeholder + titleStr = plugin.getMenuConfig().getTitle("category", "HeadDB » {category}"); + + if (titleStr.contains("{category}")) { + // Get localized category name component + Component categoryNameComponent = plugin.getMenuConfig().getButtonNameComponent("main", "categoryButtons." + knownCategory, + plugin.getLocalization().getConsoleMessage("menu.main.category." + knownCategory.toLowerCase(Locale.ROOT) + ".name") + .orElse(Component.text(knownCategory)) + ); + + // Parse title template and replace {category} with the component + Component titleTemplate = plugin.getMenuConfig().parseComponent(titleStr.replace("{category}", "CATEGORY_PLACEHOLDER")); + title = titleTemplate.replaceText(builder -> builder.matchLiteral("CATEGORY_PLACEHOLDER").replacement(categoryNameComponent)); + } else { + title = plugin.getMenuConfig().parseComponent(titleStr); + } + } + register(key, new HeadsGUI(plugin, key, title, heads)); } catch (Throwable ex) { LOGGER.error("Failed to register known category: {}", knownCategory, ex); } @@ -49,10 +77,25 @@ public void registerDefaults(HeadDB plugin) { } }) .filter(CustomCategory::isEnabled) - .peek(category -> register(category.getIdentifier(), new HeadsGUI(plugin, "custom_" + category.getIdentifier(), plugin.getLocalization().getConsoleMessage("menu.category." + category.getIdentifier()).orElseGet(() -> MiniMessage.miniMessage().deserialize("HeadDB » " + category.getName())), category.getHeads()))) + .peek(category -> { + String titleStr = plugin.getMenuConfig().getTitle("category", + plugin.getLocalization().getConsoleMessage("menu.category." + category.getIdentifier()) + .orElseGet(() -> MiniMessage.miniMessage().deserialize("HeadDB » " + category.getName())).toString() + ); + // Replace {category} placeholder with actual category name + titleStr = titleStr.replace("{category}", category.getName()); + Component title = plugin.getMenuConfig().parseComponent(titleStr); + register(category.getIdentifier(), new HeadsGUI(plugin, "custom_" + category.getIdentifier(), title, category.getHeads())); + }) .collect(Collectors.toList()); - customCategoriesGui = new CustomCategoriesGUI(plugin, "custom_categories", plugin.getLocalization().getConsoleMessage("menu.customCategories.name").orElseGet(() -> Component.text("HeadDB » More Categories").color(NamedTextColor.GOLD)), customCategories); + Component customCatTitle = plugin.getMenuConfig().parseComponent( + plugin.getMenuConfig().getTitle("custom_categories", + plugin.getLocalization().getConsoleMessage("menu.customCategories.name") + .orElseGet(() -> Component.text("HeadDB » More Categories").color(NamedTextColor.GOLD)).toString() + ) + ); + customCategoriesGui = new CustomCategoriesGUI(plugin, "custom_categories", customCatTitle, customCategories); } public void register(String key, HeadsGUI menu) { diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/PurchaseHeadMenu.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/PurchaseHeadMenu.java index 2c4cff8..dcb5cfd 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/PurchaseHeadMenu.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/menu/PurchaseHeadMenu.java @@ -1,5 +1,12 @@ package com.github.thesilentpro.headdb.core.menu; +import java.util.Locale; +import java.util.function.Consumer; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + import com.github.thesilentpro.grim.button.SimpleButton; import com.github.thesilentpro.grim.page.Page; import com.github.thesilentpro.grim.page.SimplePage; @@ -9,14 +16,9 @@ import com.github.thesilentpro.headdb.core.factory.ItemFactoryRegistry; import com.github.thesilentpro.headdb.core.util.Compatibility; import com.github.thesilentpro.inputs.paper.PaperInput; + import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; -import org.bukkit.Material; -import org.bukkit.entity.Player; -import org.bukkit.inventory.ItemStack; - -import java.util.Locale; -import java.util.function.Consumer; public class PurchaseHeadMenu extends SimplePage { @@ -24,7 +26,8 @@ public class PurchaseHeadMenu extends SimplePage { private final Head head; public PurchaseHeadMenu(HeadDB plugin, Player player, Head head, Page parentPage) { - super(plugin.getLocalization().getMessage(player.getUniqueId(), "menu.purchase.name").orElseGet(() -> Component.text("HeadDB » " + head.getName() + " » Purchase")).replaceText(builder -> builder.matchLiteral("{name}").replacement(head.getName())), 6); + super(plugin.getLocalization().getMessage(player.getUniqueId(), "menu.purchase.name").orElseGet(() -> Component.text("HeadDB » " + head.getName() + " » Purchase")).replaceText(builder -> builder.matchLiteral("{name}").replacement(head.getName())), + plugin.getMenuConfig().getSize("purchase", 3)); this.plugin = plugin; this.head = head; preventInteraction(); @@ -93,7 +96,7 @@ private Consumer handlePurchase(double cost, int amount) { ItemStack item = head.getItem(); item.setAmount(amount); - ItemFactoryRegistry.get().giveItem((Player) ctx.event().getWhoClicked(), plugin.getCfg().getOmit(), item); + ItemFactoryRegistry.get().giveItem((Player) ctx.event().getWhoClicked(), plugin.getCfg().getOmit(), plugin.getCfg().isDropOnFullInventory(), item); plugin.getLocalization().sendMessage(ctx.event().getWhoClicked(), "purchase.success", msg -> msg.replaceText(builder -> builder.matchLiteral("{amount}").replacement(String.valueOf(amount))) .replaceText(builder -> builder.matchLiteral("{name}").replacement(head.getName())) diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/storage/PlayerDAO.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/storage/PlayerDAO.java index 3a25c50..39c9baa 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/storage/PlayerDAO.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/storage/PlayerDAO.java @@ -1,12 +1,21 @@ package com.github.thesilentpro.headdb.core.storage; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.sql.*; -import java.util.*; -import java.util.stream.Collectors; - public class PlayerDAO { private static final Logger LOGGER = LoggerFactory.getLogger(PlayerDAO.class); @@ -21,6 +30,10 @@ public void createTable() { } public void saveAllPlayers(Map dataMap) { + if (dataMap.isEmpty()) { + return; + } + try (Connection conn = PlayerStorage.getConnection(); PreparedStatement stmt = conn.prepareStatement(SqlUtils.INSERT_OR_REPLACE)) { @@ -28,14 +41,16 @@ public void saveAllPlayers(Map dataMap) { stmt.setString(1, data.getUniqueId().toString()); stmt.setString(2, data.getLanguage()); - String favorites = data.getFavorites() == null ? "" : - data.getFavorites().stream().map(String::valueOf).collect(Collectors.joining(",")); + List favs = data.getFavorites(); + String favorites = (favs == null || favs.isEmpty()) ? "" : + favs.stream().map(String::valueOf).collect(Collectors.joining(",")); stmt.setString(3, favorites); - String localFavs = data.getLocalFavorites() == null ? "" : - data.getLocalFavorites().stream().map(UUID::toString).collect(Collectors.joining(",")); + List localFavs = data.getLocalFavorites(); + String localFavorites = (localFavs == null || localFavs.isEmpty()) ? "" : + localFavs.stream().map(UUID::toString).collect(Collectors.joining(",")); - stmt.setString(4, localFavs); + stmt.setString(4, localFavorites); stmt.setInt(5, data.isSoundEnabled() ? 1 : 0); stmt.addBatch(); @@ -58,23 +73,23 @@ public Map loadAllPlayers() { while (rs.next()) { UUID uuid = UUID.fromString(rs.getString("uuid")); String lang = rs.getString("language"); - String favs = rs.getString("favorites"); boolean sound = rs.getInt("sound_enabled") == 1; - List favorites = favs == null || favs.isEmpty() + + String favs = rs.getString("favorites"); + List favorites = (favs == null || favs.isEmpty()) ? new ArrayList<>() - : new ArrayList<>(Arrays.stream(favs.split(",")) - .map(Integer::parseInt) - .toList()); + : Arrays.stream(favs.split(",")) + .map(Integer::parseInt) + .collect(Collectors.toCollection(ArrayList::new)); + String localFavs = rs.getString("local_favorites"); - List localFavorites = localFavs == null || localFavs.isEmpty() + List localFavorites = (localFavs == null || localFavs.isEmpty()) ? new ArrayList<>() - : new ArrayList<>(Arrays.stream(localFavs.split(",")) - .map(UUID::fromString) - .toList()); - - PlayerData data = new PlayerData(uuid, lang, sound, favorites, localFavorites); + : Arrays.stream(localFavs.split(",")) + .map(UUID::fromString) + .collect(Collectors.toCollection(ArrayList::new)); - dataMap.put(uuid, data); + dataMap.put(uuid, new PlayerData(uuid, lang, sound, favorites, localFavorites)); } } catch (SQLException ex) { diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/util/Utils.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/util/Utils.java index b32448d..77399a2 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/util/Utils.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/core/util/Utils.java @@ -1,6 +1,5 @@ package com.github.thesilentpro.headdb.core.util; -import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; @@ -9,10 +8,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; +import javax.annotation.Nullable; + public class Utils { private static final Pattern SPACE_PATTERN = Pattern.compile(" "); - private static final AtomicInteger poolNumber = new AtomicInteger(1); + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); /** * Splits the given list into sublists of the given chunkSize. @@ -51,8 +52,7 @@ public static boolean matches(@Nullable String text, @Nullable String query) { public static ExecutorService executorService(int nThreads, String namePrefix) { ThreadFactory factory = r -> { - Thread t = new Thread(r); - t.setName(namePrefix + " #" + poolNumber.getAndIncrement()); + Thread t = new Thread(r, namePrefix + " #" + POOL_NUMBER.getAndIncrement()); t.setDaemon(true); return t; }; diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadAPI.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadAPI.java index dc89a15..388f0b2 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadAPI.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadAPI.java @@ -1,20 +1,27 @@ package com.github.thesilentpro.headdb.implementation; -import com.github.thesilentpro.headdb.api.HeadAPI; -import com.github.thesilentpro.headdb.api.HeadDatabase; -import com.github.thesilentpro.headdb.api.model.Head; -import com.github.thesilentpro.headdb.core.factory.ItemFactoryRegistry; -import com.github.thesilentpro.headdb.core.util.Utils; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.stream.Collectors; + import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.stream.Collectors; +import com.github.thesilentpro.headdb.api.HeadAPI; +import com.github.thesilentpro.headdb.api.HeadDatabase; +import com.github.thesilentpro.headdb.api.model.Head; +import com.github.thesilentpro.headdb.core.factory.ItemFactoryRegistry; +import com.github.thesilentpro.headdb.core.util.Utils; /** * Default implementation of {@link HeadAPI} using BaseHeadDatabase. @@ -48,18 +55,29 @@ public CompletableFuture> onReady() { @NotNull @Override public CompletableFuture> searchByName(@NotNull String name, boolean lenient) { - return getHeads().thenApplyAsync(heads -> - heads.stream() - .filter(h -> lenient ? Utils.matches(h.getName(), name) : h.getName().equalsIgnoreCase(name)) - .collect(Collectors.toList()), executor); + return CompletableFuture.supplyAsync(() -> { + List heads = database.getHeads(); + if (heads == null || heads.isEmpty()) { + return Collections.emptyList(); + } + return heads.stream() + .filter(h -> lenient ? Utils.matches(h.getName(), name) : h.getName().equalsIgnoreCase(name)) + .collect(Collectors.toList()); + }, executor); } @NotNull @Override public CompletableFuture> findByName(@NotNull String name, boolean lenient) { - return getHeads().thenApplyAsync(heads -> heads.stream() - .filter(h -> lenient ? Utils.matches(h.getName(), name) : h.getName().equalsIgnoreCase(name)) - .findAny(), executor); + return CompletableFuture.supplyAsync(() -> { + List heads = database.getHeads(); + if (heads == null || heads.isEmpty()) { + return Optional.empty(); + } + return heads.stream() + .filter(h -> lenient ? Utils.matches(h.getName(), name) : h.getName().equalsIgnoreCase(name)) + .findAny(); + }, executor); } @NotNull @@ -95,17 +113,16 @@ public CompletableFuture> getHeads() { @NotNull @Override public List findKnownCategories() { - if (database.getHeads() == null) { + List heads = database.getHeads(); + if (heads == null || heads.isEmpty()) { return Collections.emptyList(); } - List result = new ArrayList<>(); - for (Head head : database.getHeads()) { - if (!result.contains(head.getCategory())) { - result.add(head.getCategory()); - } + Set categories = new LinkedHashSet<>(); + for (Head head : heads) { + categories.add(head.getCategory()); } - return result; + return new ArrayList<>(categories); } @NotNull diff --git a/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadDatabase.java b/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadDatabase.java index bb51fee..27fb074 100644 --- a/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadDatabase.java +++ b/headdb-core/src/main/java/com/github/thesilentpro/headdb/implementation/BaseHeadDatabase.java @@ -1,15 +1,5 @@ package com.github.thesilentpro.headdb.implementation; -import com.github.thesilentpro.headdb.api.HeadDatabase; -import com.github.thesilentpro.headdb.api.model.Head; -import com.github.thesilentpro.headdb.implementation.model.HeadMapper; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -18,11 +8,34 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URL; -import java.util.*; -import java.util.concurrent.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.thesilentpro.headdb.api.HeadDatabase; +import com.github.thesilentpro.headdb.api.model.Head; +import com.github.thesilentpro.headdb.implementation.model.HeadMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + public class BaseHeadDatabase implements HeadDatabase { private static final Logger LOGGER = LoggerFactory.getLogger(BaseHeadDatabase.class); @@ -215,30 +228,27 @@ public CompletableFuture> onReady() { @Override @Nullable public List getHeads() { - if (!isReady()) { - return Collections.emptyList(); - } - if (this.heads == null) { + List currentHeads = this.heads; + if (currentHeads == null) { return null; } - return Collections.unmodifiableList(this.heads); + return Collections.unmodifiableList(currentHeads); } @Override @NotNull public List getByCategory(String category) { - if (!isReady()) { - return Collections.emptyList(); - } if (byCategory != null) { return byCategory.getOrDefault(category, Collections.emptyList()); } - if (heads == null) { + + List currentHeads = heads; + if (currentHeads == null || currentHeads.isEmpty()) { return Collections.emptyList(); } List result = new ArrayList<>(); - for (Head head : heads) { + for (Head head : currentHeads) { if (category.equals(head.getCategory())) { result.add(head); } @@ -249,9 +259,6 @@ public List getByCategory(String category) { @Override @NotNull public List getByTags(String... tags) { - if (!isReady()) { - return Collections.emptyList(); - } if (tags == null || tags.length == 0) { return Collections.emptyList(); } @@ -266,12 +273,20 @@ public List getByTags(String... tags) { return new ArrayList<>(resultSet); } - if (heads == null) { + List currentHeads = heads; + if (currentHeads == null || currentHeads.isEmpty()) { return Collections.emptyList(); } - Set tagSet = Arrays.stream(tags).filter(Objects::nonNull).collect(Collectors.toSet()); + + Set tagSet = new HashSet<>(tags.length); + for (String tag : tags) { + if (tag != null) { + tagSet.add(tag); + } + } + List result = new ArrayList<>(); - for (Head head : heads) { + for (Head head : currentHeads) { for (String hTag : head.getTags()) { if (tagSet.contains(hTag)) { result.add(head); @@ -285,16 +300,16 @@ public List getByTags(String... tags) { @Override @Nullable public Head getById(int id) { - if (!isReady()) { - return null; - } if (byId != null) { return byId.get(id); } - if (heads == null) { + + List currentHeads = heads; + if (currentHeads == null) { return null; } - for (Head head : heads) { + + for (Head head : currentHeads) { if (head.getId() == id) { return head; } @@ -305,17 +320,17 @@ public Head getById(int id) { @Override @Nullable public Head getByTexture(String texture) { - if (!isReady()) { - return null; - } if (byTexture != null) { return byTexture.get(texture); } - if (heads == null) { + + List currentHeads = heads; + if (currentHeads == null) { return null; } - for (Head head : heads) { - if (head.getTexture().equals(texture)) { + + for (Head head : currentHeads) { + if (texture.equals(head.getTexture())) { return head; } } diff --git a/headdb-core/src/main/resources/config.yml b/headdb-core/src/main/resources/config.yml index 8ccc2b2..753368f 100644 --- a/headdb-core/src/main/resources/config.yml +++ b/headdb-core/src/main/resources/config.yml @@ -10,11 +10,11 @@ preloadHeads: true # Example: Player is on page 10 of the category, next time they open that category they will start from that page instead of the first. trackPage: true -# If enabled shows the info item on how you can add heads to the database.- -showInfoItem: true +# Menu configuration moved to menus/ folder +# Edit menus/main.yml, menus/category.yml, etc. to customize menus # Limits the maximum amount of heads you can buy at once. 2304 is a full inventory of 64 heads. -# Note that extra heads that do not fit in the inventory are dropped when purchasing. +# Note that extra heads that do not fit in the inventory are dropped when purchasing if dropOnFullInventory is true. maxBuyAmount: 2304 # Head name and lore. @@ -22,6 +22,8 @@ head: # Which lines of the lore should be removed when giving the head to a player. # Example: [1,2,3,4,5] omit: [] + # Whether to drop items on the ground if the player's inventory is full + dropOnFullInventory: true # Configure the name of the heads. name: # When there is no economy installed this is the name @@ -68,31 +70,9 @@ economy: head: 98: 100 # Example for Gold Block +# Menu rows configuration (for category menus) headsMenu: rows: 4 - customCategories: - head: "e7bc251a6cb0d6d9f05c5711911a6ec24b209dbe64267901a4b03761debcf738" - item: "BOOKSHELF" - search: - head: "9d9cc58ad25a1ab16d36bb5d6d493c8f5898c2bf302b64e325921c41c35867" - item: "SPYGLASS" - divider: - enabled: true - row: 5 - item: - material: BLACK_STAINED_GLASS_PANE - name: " " - -controls: - back: - head: "e5da4847272582265bdaca367237c96122b139f4e597fbc6667d3fb75fea7cf6" - item: "ARROW" - info: - head: "16439d2e306b225516aa9a6d007a7e75edd2d5015d113b42f44be62a517e574f" - item: "PAPER" - next: - head: "6527ebae9f153154a7ed49c88c02b5a9a9ca7cb1618d9914a3d9df8ccb3c84" - item: "ARROW" # Database Configuration database: diff --git a/headdb-core/src/main/resources/menus/README.md b/headdb-core/src/main/resources/menus/README.md new file mode 100644 index 0000000..2599192 --- /dev/null +++ b/headdb-core/src/main/resources/menus/README.md @@ -0,0 +1,229 @@ +# HeadDB Menu Configuration / Конфигурация меню HeadDB + +## English + +### Overview +This folder contains configuration files for customizing HeadDB menus. You can fully customize the appearance, layout, and text of all menus. + +### Files +- `main.yml` - Main menu configuration ✅ **FULLY CONFIGURABLE** +- `category.yml` - Category menu examples/documentation +- `favorites.yml` - Favorites menu examples/documentation +- `local.yml` - Local heads menu examples/documentation +- `custom_categories.yml` - Custom categories menu examples/documentation +- `purchase.yml` - Purchase menu examples/documentation +- `search.yml` - Search results menu examples/documentation + +**Note:** Full configuration support is currently implemented only for the main menu. +Other files serve as documentation showing possible settings. +Menu titles for all menus can be configured in `messages/` files. + +### Features +1. **Full Menu Customization**: Change titles, sizes, button positions, and more +2. **Multi-language Support**: All text supports MiniMessage format for colors and formatting +3. **Flexible Layout**: Configure slot positions for all menu elements +4. **Custom Textures**: Use custom player head textures for buttons + +### Configuration Format + +#### Main Menu (main.yml) +```yaml +title: "HeadDB" # Menu title +size: 6 # Number of rows (1-6) +categorySlots: [11, 12, 13, ...] # Slots for category buttons +buttons: + local: + enabled: true + slot: 41 + texture: "..." # Player head texture value + material: "COMPASS" # Fallback material + name: "Local Heads" + lore: [] + permission: "headdb.category.local" +``` + +#### Category Menu (category.yml) +```yaml +size: 6 +headSlots: [10, 11, 12, ...] # Slots where heads are displayed +controls: + back: + slot: 45 + name: "◀ Back" + lore: ["Go to previous page"] +``` + +#### Favorites Menu (favorites.yml) +```yaml +title: "HeadDB » Favorites" +size: 6 +headSlots: [10, 11, 12, ...] +emptyMessage: + slot: 22 + name: "No favorites" +``` + +#### Local Heads Menu (local.yml) +```yaml +title: "HeadDB » Local Heads" +size: 6 +headDisplay: + showPlayerName: true + showUUID: false +``` + +#### Purchase Menu (purchase.yml) +```yaml +title: "HeadDB » {name} » Purchase" +size: 3 +purchaseButtons: + buy1: + slot: 10 + name: "Buy x1" +``` + +#### Search Menu (search.yml) +```yaml +title: "HeadDB » Search » {query}" +size: 6 +resultSlots: [10, 11, 12, ...] +``` + +#### Custom Categories Menu (custom_categories.yml) +```yaml +title: "HeadDB » More Categories" +size: 6 +categoryDisplay: + showHeadCount: true +``` + +### Opening Menus for Other Players +Use the command: `/hdb open ` + +Examples: +- `/hdb open Main Steve` - Opens main menu for Steve +- `/hdb open Animals Alex` - Opens Animals category for Alex + +**Required Permission**: `headdb.command.open.others` + +### Language Files +Language files are located in `messages/` folder: +- `en.yml` - English (default) +- `ru.yml` - Russian + +To add a new language, copy `en.yml` and translate all values. + +--- + +## Русский + +### Обзор +Эта папка содержит файлы конфигурации для настройки меню HeadDB. Вы можете полностью настроить внешний вид, расположение и текст всех меню. + +### Файлы +- `main.yml` - Конфигурация главного меню ✅ **ПОЛНОСТЬЮ НАСТРАИВАЕТСЯ** +- `category.yml` - Примеры/документация меню категорий +- `favorites.yml` - Примеры/документация меню избранного +- `local.yml` - Примеры/документация меню локальных голов +- `custom_categories.yml` - Примеры/документация меню дополнительных категорий +- `purchase.yml` - Примеры/документация меню покупки +- `search.yml` - Примеры/документация меню результатов поиска + +**Примечание:** Полная поддержка конфигурации реализована только для главного меню. +Остальные файлы служат документацией, показывая возможные настройки. +Названия всех меню настраиваются в файлах `messages/`. + +### Возможности +1. **Полная настройка меню**: Изменяйте названия, размеры, позиции кнопок и многое другое +2. **Поддержка нескольких языков**: Весь текст поддерживает формат MiniMessage для цветов и форматирования +3. **Гибкая компоновка**: Настраивайте позиции слотов для всех элементов меню +4. **Пользовательские текстуры**: Используйте пользовательские текстуры голов игроков для кнопок + +### Формат конфигурации + +#### Главное меню (main.yml) +```yaml +title: "HeadDB" # Название меню +size: 6 # Количество строк (1-6) +categorySlots: [11, 12, 13, ...] # Слоты для кнопок категорий +buttons: + local: + enabled: true + slot: 41 + texture: "..." # Значение текстуры головы игрока + material: "COMPASS" # Резервный материал + name: "Локальные головы" + lore: [] + permission: "headdb.category.local" +``` + +#### Меню категории (category.yml) +```yaml +size: 6 +headSlots: [10, 11, 12, ...] # Слоты, где отображаются головы +controls: + back: + slot: 45 + name: "◀ Назад" + lore: ["Перейти на предыдущую страницу"] +``` + +#### Меню избранного (favorites.yml) +```yaml +title: "HeadDB » Избранное" +size: 6 +headSlots: [10, 11, 12, ...] +emptyMessage: + slot: 22 + name: "Избранное пусто" +``` + +#### Меню локальных голов (local.yml) +```yaml +title: "HeadDB » Локальные головы" +size: 6 +headDisplay: + showPlayerName: true + showUUID: false +``` + +#### Меню покупки (purchase.yml) +```yaml +title: "HeadDB » {name} » Покупка" +size: 3 +purchaseButtons: + buy1: + slot: 10 + name: "Купить x1" +``` + +#### Меню поиска (search.yml) +```yaml +title: "HeadDB » Поиск » {query}" +size: 6 +resultSlots: [10, 11, 12, ...] +``` + +#### Меню дополнительных категорий (custom_categories.yml) +```yaml +title: "HeadDB » Дополнительные категории" +size: 6 +categoryDisplay: + showHeadCount: true +``` + +### Открытие меню для других игроков +Используйте команду: `/hdb open <меню> <игрок>` + +Примеры: +- `/hdb open Main Steve` - Открывает главное меню для Steve +- `/hdb open Animals Alex` - Открывает категорию Животные для Alex + +**Требуемое право**: `headdb.command.open.others` + +### Языковые файлы +Языковые файлы находятся в папке `messages/`: +- `en.yml` - Английский (по умолчанию) +- `ru.yml` - Русский + +Чтобы добавить новый язык, скопируйте `en.yml` и переведите все значения. diff --git a/headdb-core/src/main/resources/menus/category.yml b/headdb-core/src/main/resources/menus/category.yml new file mode 100644 index 0000000..196a520 --- /dev/null +++ b/headdb-core/src/main/resources/menus/category.yml @@ -0,0 +1,96 @@ +# Конфигурация меню категорий HeadDB +# Category menu configuration for HeadDB + +# Название меню по умолчанию (поддерживает MiniMessage формат) +# Default menu title (supports MiniMessage format) +# {category} будет заменено на название категории +title: "HeadDB » {category}" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 6 + +# Названия для конкретных категорий +# Titles for specific categories +categoryTitles: + Alphabet: "HeadDB » Алфавит" + Animals: "HeadDB » Животные" + Blocks: "HeadDB » Блоки" + Decoration: "HeadDB » Декорации" + "Food & Drinks": "HeadDB » Еда и напитки" + Humanoid: "HeadDB » Гуманоиды" + Humans: "HeadDB » Люди" + Miscellaneous: "HeadDB » Разное" + Monsters: "HeadDB » Монстры" + Plants: "HeadDB » Растения" + +# Слоты для отображения голов (начиная с 0) +# Slots for displaying heads (starting from 0) +headSlots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43] + +# Элементы управления +# Control buttons +controls: + # Кнопка "Назад" (предыдущая страница) + # Back button (previous page) + back: + enabled: true + slot: 45 + material: "ARROW" + name: "◀ Назад" + lore: + - "Перейти на предыдущую страницу: ${{BACK}}" + + # Информация о странице + # Page info + info: + enabled: true + slot: 49 + material: "PAPER" + name: "ℹ Страница ${{CURRENT}}/${{MAX}}" + lore: + - "Нажмите, чтобы вернуться в главное меню." + + # Кнопка "Далее" (следующая страница) + # Next button (next page) + next: + enabled: true + slot: 53 + material: "ARROW" + name: "Далее ▶" + lore: + - "Перейти на следующую страницу: ${{NEXT}}" + +# Заполнитель границ +# Border filler +border: + enabled: true + material: "GRAY_STAINED_GLASS_PANE" + name: "" + lore: [] + +# Настройки отображения голов +# Head display settings +headDisplay: + # Показывать ID головы в описании + # Show head ID in lore + showId: true + + # Показывать категорию в описании + # Show category in lore + showCategory: true + + # Показывать теги в описании + # Show tags in lore + showTags: true + + # Формат описания головы + # Head lore format + loreFormat: + - "" + - "ID: {id}" + - "Категория: {category}" + - "Теги: {tags}" + - "" + - "ЛКМ - Получить голову" + - "ПКМ - Добавить в избранное" diff --git a/headdb-core/src/main/resources/menus/custom_categories.yml b/headdb-core/src/main/resources/menus/custom_categories.yml new file mode 100644 index 0000000..a5dd478 --- /dev/null +++ b/headdb-core/src/main/resources/menus/custom_categories.yml @@ -0,0 +1,81 @@ +# Конфигурация меню дополнительных категорий +# Custom categories menu configuration + +# Название меню (поддерживает MiniMessage формат) +# Menu title (supports MiniMessage format) +title: "HeadDB » Дополнительные категории" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 6 + +# Слоты для отображения категорий (начиная с 0) +# Slots for displaying categories (starting from 0) +categorySlots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43] + +# Элементы управления +# Control buttons +controls: + # Кнопка "Назад" (предыдущая страница) + # Back button (previous page) + back: + enabled: true + slot: 45 + material: "ARROW" + name: "◀ Назад" + lore: + - "Перейти на предыдущую страницу: ${{BACK}}" + + # Информация о странице + # Page info + info: + enabled: true + slot: 49 + material: "PAPER" + name: "ℹ Страница ${{CURRENT}}/${{MAX}}" + lore: + - "Нажмите, чтобы вернуться в главное меню." + + # Кнопка "Далее" (следующая страница) + # Next button (next page) + next: + enabled: true + slot: 53 + material: "ARROW" + name: "Далее ▶" + lore: + - "Перейти на следующую страницу: ${{NEXT}}" + +# Заполнитель границ +# Border filler +border: + enabled: true + material: "PURPLE_STAINED_GLASS_PANE" + name: "" + lore: [] + +# Настройки отображения категорий +# Category display settings +categoryDisplay: + # Показывать количество голов в категории + # Show head count in category + showHeadCount: true + + # Формат описания категории + # Category lore format + loreFormat: + - "" + - "Голов в категории: {count}" + - "" + - "Нажмите, чтобы открыть" + +# Сообщение при отсутствии дополнительных категорий +# Empty custom categories message +emptyMessage: + enabled: true + slot: 22 + material: "BARRIER" + name: "Нет дополнительных категорий" + lore: + - "Дополнительные категории не настроены" + - "Настройте их в categories.yml" diff --git a/headdb-core/src/main/resources/menus/favorites.yml b/headdb-core/src/main/resources/menus/favorites.yml new file mode 100644 index 0000000..8bdaeea --- /dev/null +++ b/headdb-core/src/main/resources/menus/favorites.yml @@ -0,0 +1,92 @@ +# Конфигурация меню избранного +# Favorites menu configuration + +# Название меню (поддерживает MiniMessage формат) +# Menu title (supports MiniMessage format) +title: "HeadDB » Избранное" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 6 + +# Слоты для отображения голов (начиная с 0) +# Slots for displaying heads (starting from 0) +headSlots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43] + +# Элементы управления +# Control buttons +controls: + # Кнопка "Назад" (предыдущая страница) + # Back button (previous page) + back: + enabled: true + slot: 45 + material: "ARROW" + name: "◀ Назад" + lore: + - "Перейти на предыдущую страницу: ${{BACK}}" + + # Информация о странице + # Page info + info: + enabled: true + slot: 49 + material: "PAPER" + name: "ℹ Страница ${{CURRENT}}/${{MAX}}" + lore: + - "Нажмите, чтобы вернуться в главное меню." + + # Кнопка "Далее" (следующая страница) + # Next button (next page) + next: + enabled: true + slot: 53 + material: "ARROW" + name: "Далее ▶" + lore: + - "Перейти на следующую страницу: ${{NEXT}}" + +# Заполнитель границ +# Border filler +border: + enabled: true + material: "YELLOW_STAINED_GLASS_PANE" + name: "" + lore: [] + +# Настройки отображения голов +# Head display settings +headDisplay: + # Показывать ID головы в описании + # Show head ID in lore + showId: true + + # Показывать категорию в описании + # Show category in lore + showCategory: true + + # Показывать теги в описании + # Show tags in lore + showTags: true + + # Формат описания головы + # Head lore format + loreFormat: + - "" + - "ID: {id}" + - "Категория: {category}" + - "Теги: {tags}" + - "" + - "ЛКМ - Получить голову" + - "ПКМ - Удалить из избранного" + +# Сообщение при пустом избранном +# Empty favorites message +emptyMessage: + enabled: true + slot: 22 + material: "BARRIER" + name: "Избранное пусто" + lore: + - "У вас пока нет избранных голов" + - "Добавьте головы, нажав ПКМ" diff --git a/headdb-core/src/main/resources/menus/local.yml b/headdb-core/src/main/resources/menus/local.yml new file mode 100644 index 0000000..42a9e19 --- /dev/null +++ b/headdb-core/src/main/resources/menus/local.yml @@ -0,0 +1,85 @@ +# Конфигурация меню локальных голов +# Local heads menu configuration + +# Название меню (поддерживает MiniMessage формат) +# Menu title (supports MiniMessage format) +title: "HeadDB » Локальные головы" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 6 + +# Слоты для отображения голов (начиная с 0) +# Slots for displaying heads (starting from 0) +headSlots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43] + +# Элементы управления +# Control buttons +controls: + # Кнопка "Назад" (предыдущая страница) + # Back button (previous page) + back: + enabled: true + slot: 45 + material: "ARROW" + name: "◀ Назад" + lore: + - "Перейти на предыдущую страницу: ${{BACK}}" + + # Информация о странице + # Page info + info: + enabled: true + slot: 49 + material: "PAPER" + name: "ℹ Страница ${{CURRENT}}/${{MAX}}" + lore: + - "Нажмите, чтобы вернуться в главное меню." + + # Кнопка "Далее" (следующая страница) + # Next button (next page) + next: + enabled: true + slot: 53 + material: "ARROW" + name: "Далее ▶" + lore: + - "Перейти на следующую страницу: ${{NEXT}}" + +# Заполнитель границ +# Border filler +border: + enabled: true + material: "LIGHT_BLUE_STAINED_GLASS_PANE" + name: "" + lore: [] + +# Настройки отображения голов +# Head display settings +headDisplay: + # Показывать имя игрока + # Show player name + showPlayerName: true + + # Показывать UUID + # Show UUID + showUUID: false + + # Формат описания головы + # Head lore format + loreFormat: + - "" + - "Игрок: {playerName}" + - "" + - "ЛКМ - Получить голову" + - "ПКМ - Добавить в избранное" + +# Сообщение при отсутствии локальных голов +# Empty local heads message +emptyMessage: + enabled: true + slot: 22 + material: "BARRIER" + name: "Нет локальных голов" + lore: + - "На сервере нет игроков" diff --git a/headdb-core/src/main/resources/menus/main.yml b/headdb-core/src/main/resources/menus/main.yml new file mode 100644 index 0000000..3bca21b --- /dev/null +++ b/headdb-core/src/main/resources/menus/main.yml @@ -0,0 +1,184 @@ +# Конфигурация главного меню HeadDB +# Main menu configuration for HeadDB + +# Название меню (поддерживает MiniMessage формат) +# Menu title (supports MiniMessage format) +title: "HeadDB" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 6 + +# Слоты для категорий (начиная с 0) +# Category slots (starting from 0) +categorySlots: [11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 29, 30, 31, 32, 33] + +# Предпочитаемый порядок категорий +# Preferred category order +preferredCategoryOrder: + - "Alphabet" + - "Animals" + - "Blocks" + - "Decoration" + - "Food & Drinks" + - "Humanoid" + - "Humans" + - "Miscellaneous" + - "Monsters" + - "Plants" + +# Настройка кнопок категорий +# Category buttons configuration +categoryButtons: + Alphabet: + enabled: true + slot: 11 + texture: "" # Пусто = использовать голову из базы данных + material: "PLAYER_HEAD" + name: "Алфавит" + lore: [] + + Animals: + enabled: true + slot: 12 + texture: "" + material: "PLAYER_HEAD" + name: "Животные" + lore: [] + + Blocks: + enabled: true + slot: 13 + texture: "" + material: "PLAYER_HEAD" + name: "Блоки" + lore: [] + + Decoration: + enabled: true + slot: 14 + texture: "" + material: "PLAYER_HEAD" + name: "Декор" + lore: [] + + "Food & Drinks": + enabled: true + slot: 15 + texture: "" + material: "PLAYER_HEAD" + name: "Еда и напитки" + lore: [] + + Humanoid: + enabled: true + slot: 20 + texture: "" + material: "PLAYER_HEAD" + name: "Гуманоиды" + lore: [] + + Humans: + enabled: true + slot: 21 + texture: "" + material: "PLAYER_HEAD" + name: "Люди" + lore: [] + + Miscellaneous: + enabled: true + slot: 22 + texture: "" + material: "PLAYER_HEAD" + name: "Разное" + lore: [] + + Monsters: + enabled: true + slot: 23 + texture: "" + material: "PLAYER_HEAD" + name: "Монстры" + lore: [] + + Plants: + enabled: true + slot: 24 + texture: "" + material: "PLAYER_HEAD" + name: "Растения" + lore: [] + +# Кнопки меню +# Menu buttons +buttons: + # Локальные головы + # Local heads + local: + enabled: true + slot: 41 + texture: "7f6bf958abd78295eed6ffc293b1aa59526e80f54976829ea068337c2f5e8" + material: "COMPASS" # Используется если текстура не найдена / Used if texture not found + name: "Локальные головы" + lore: [] + permission: "headdb.category.local" + + # Избранное + # Favorites + favorites: + enabled: true + slot: 42 + texture: "76fdd4b13d54f6c91dd5fa765ec93dd9458b19f8aa34eeb5c80f455b119f278" + material: "BOOK" + name: "Избранное" + lore: [] + permission: "headdb.category.favorites" + + # Дополнительные категории + # Custom categories + customCategories: + enabled: true + slot: 38 + texture: "" # Берётся из config.yml / Taken from config.yml + material: "NETHER_STAR" + name: "Дополнительные категории" + lore: [] + permission: "headdb.category.custom" + + # Поиск + # Search + search: + enabled: true + slot: 39 + texture: "" # Берётся из config.yml / Taken from config.yml + material: "COMPASS" + name: "Поиск" + lore: + - "Нажмите, чтобы найти голову" + permission: "headdb.command.search" + + # Информация + # Info + info: + enabled: true + slot: 53 + texture: "16439d2e306b225516aa9a6d007a7e75edd2d5015d113b42f44be62a517e574f" + material: "BOOK" + name: "Не можете найти нужную голову?" + lore: + - "❓ Не нашли идеальную голову в нашей коллекции?" + - "🎯 Мы постоянно добавляем новые — и вы можете помочь!" + - "" + - "📥 Отправьте свои любимые или оригинальные головы" + - "✨ Прямо через наш Discord сервер!" + - "" + - "🔗 Discord > https://discord.gg/RJsVvVd" + +# Заполнитель границ +# Border filler +border: + enabled: true + material: "BLACK_STAINED_GLASS_PANE" + name: "" + lore: [] diff --git a/headdb-core/src/main/resources/menus/purchase.yml b/headdb-core/src/main/resources/menus/purchase.yml new file mode 100644 index 0000000..a35c139 --- /dev/null +++ b/headdb-core/src/main/resources/menus/purchase.yml @@ -0,0 +1,139 @@ +# Конфигурация меню покупки +# Purchase menu configuration + +# Название меню (поддерживает MiniMessage формат) +# Menu title (supports MiniMessage format) +# {name} - название головы, {cost} - стоимость +# Примечание: title для purchase берется из messages/ (menu.purchase.name) +title: "HeadDB » {name} » Покупка" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 3 + +# Позиция предмета для покупки +# Item position +itemSlot: 13 + +# Кнопки покупки +# Purchase buttons +purchaseButtons: + # Купить 1 штуку + # Buy 1 item + buy1: + enabled: true + slot: 10 + material: "LIME_STAINED_GLASS_PANE" + name: "Купить x1" + lore: + - "" + - "Стоимость: {cost}" + - "" + - "Нажмите, чтобы купить" + + # Купить 8 штук + # Buy 8 items + buy8: + enabled: true + slot: 11 + material: "LIME_STAINED_GLASS_PANE" + name: "Купить x8" + lore: + - "" + - "Стоимость: {cost}" + - "" + - "Нажмите, чтобы купить" + + # Купить 16 штук + # Buy 16 items + buy16: + enabled: true + slot: 12 + material: "LIME_STAINED_GLASS_PANE" + name: "Купить x16" + lore: + - "" + - "Стоимость: {cost}" + - "" + - "Нажмите, чтобы купить" + + # Купить 32 штуки + # Buy 32 items + buy32: + enabled: true + slot: 14 + material: "LIME_STAINED_GLASS_PANE" + name: "Купить x32" + lore: + - "" + - "Стоимость: {cost}" + - "" + - "Нажмите, чтобы купить" + + # Купить 64 штуки + # Buy 64 items + buy64: + enabled: true + slot: 15 + material: "LIME_STAINED_GLASS_PANE" + name: "Купить x64" + lore: + - "" + - "Стоимость: {cost}" + - "" + - "Нажмите, чтобы купить" + + # Купить стак (64 штуки) + # Buy stack (64 items) + buyStack: + enabled: true + slot: 16 + material: "LIME_STAINED_GLASS_PANE" + name: "Купить стак" + lore: + - "" + - "Количество: 64" + - "Стоимость: {cost}" + - "" + - "Нажмите, чтобы купить" + +# Кнопка отмены +# Cancel button +cancelButton: + enabled: true + slot: 22 + material: "RED_STAINED_GLASS_PANE" + name: "Отмена" + lore: + - "" + - "Вернуться назад" + +# Заполнитель +# Filler +border: + enabled: true + material: "GRAY_STAINED_GLASS_PANE" + name: "" + lore: [] + +# Настройки отображения предмета +# Item display settings +itemDisplay: + # Показывать стоимость в описании + # Show cost in lore + showCost: true + + # Показывать ID + # Show ID + showId: true + + # Формат описания + # Lore format + loreFormat: + - "" + - "ID: {id}" + - "Категория: {category}" + - "" + - "Стоимость за 1 шт: {cost}" + - "" + - "Выберите количество для покупки" diff --git a/headdb-core/src/main/resources/menus/search.yml b/headdb-core/src/main/resources/menus/search.yml new file mode 100644 index 0000000..b1a41ae --- /dev/null +++ b/headdb-core/src/main/resources/menus/search.yml @@ -0,0 +1,132 @@ +# Конфигурация меню результатов поиска +# Search results menu configuration + +# Название меню (поддерживает MiniMessage формат) +# Menu title (supports MiniMessage format) +# {query} - поисковый запрос, {count} - количество результатов +# Примечание: title для search берется из messages/ (menu.search.name) +title: "HeadDB » Поиск » {query}" + +# Размер меню (количество строк: 1-6) +# Menu size (number of rows: 1-6) +size: 6 + +# Слоты для отображения результатов (начиная с 0) +# Slots for displaying results (starting from 0) +resultSlots: [10, 11, 12, 13, 14, 15, 16, 19, 20, 21, 22, 23, 24, 25, 28, 29, 30, 31, 32, 33, 34, 37, 38, 39, 40, 41, 42, 43] + +# Элементы управления +# Control buttons +controls: + # Кнопка "Назад" (предыдущая страница) + # Back button (previous page) + back: + enabled: true + slot: 45 + material: "ARROW" + name: "◀ Назад" + lore: + - "Перейти на предыдущую страницу: ${{BACK}}" + + # Информация о странице и результатах + # Page and results info + info: + enabled: true + slot: 49 + material: "PAPER" + name: "ℹ Найдено: {count}" + lore: + - "Страница: ${{CURRENT}}/${{MAX}}" + - "" + - "Нажмите, чтобы вернуться в главное меню." + + # Кнопка "Далее" (следующая страница) + # Next button (next page) + next: + enabled: true + slot: 53 + material: "ARROW" + name: "Далее ▶" + lore: + - "Перейти на следующую страницу: ${{NEXT}}" + + # Кнопка нового поиска + # New search button + newSearch: + enabled: true + slot: 48 + material: "COMPASS" + name: "Новый поиск" + lore: + - "" + - "Нажмите, чтобы начать новый поиск" + +# Заполнитель границ +# Border filler +border: + enabled: true + material: "GREEN_STAINED_GLASS_PANE" + name: "" + lore: [] + +# Настройки отображения результатов +# Result display settings +resultDisplay: + # Показывать ID головы + # Show head ID + showId: true + + # Показывать категорию + # Show category + showCategory: true + + # Показывать теги + # Show tags + showTags: true + + # Показывать релевантность (если применимо) + # Show relevance (if applicable) + showRelevance: false + + # Формат описания результата + # Result lore format + loreFormat: + - "" + - "ID: {id}" + - "Категория: {category}" + - "Теги: {tags}" + - "" + - "ЛКМ - Получить голову" + - "ПКМ - Добавить в избранное" + +# Сообщение при отсутствии результатов +# No results message +noResultsMessage: + enabled: true + slot: 22 + material: "BARRIER" + name: "Ничего не найдено" + lore: + - "" + - "По запросу {query}" + - "ничего не найдено" + - "" + - "Попробуйте изменить запрос" + +# Подсказки по поиску +# Search hints +searchHints: + enabled: true + slot: 50 + material: "BOOK" + name: "💡 Подсказки по поиску" + lore: + - "" + - "Используйте фильтры:" + - "category:Animals" + - "tag:red" + - "id:123" + - "" + - "Режимы поиска:" + - "mode:exact - точное совпадение" + - "mode:fuzzy - нечёткий поиск" diff --git a/headdb-core/src/main/resources/messages/en.yml b/headdb-core/src/main/resources/messages/en.yml index e20e365..4e6b01b 100644 --- a/headdb-core/src/main/resources/messages/en.yml +++ b/headdb-core/src/main/resources/messages/en.yml @@ -74,6 +74,7 @@ command: open: opening: "Opening the Head Database." + openingFor: "Opening menu {menu} for player {target}." invalidCategory: "Invalid category: {category}" give: diff --git a/headdb-core/src/main/resources/messages/ru.yml b/headdb-core/src/main/resources/messages/ru.yml new file mode 100644 index 0000000..1ec946a --- /dev/null +++ b/headdb-core/src/main/resources/messages/ru.yml @@ -0,0 +1,92 @@ +noPermission: "Нет прав доступа!" +noConsole: "Только для игроков!" +invalidSubCommand: "Неверная подкоманда » ${1}" +commandUsage: "Использование » {usage}" +invalidTarget: "Игрок не найден » {target}" +invalidNumber: "Неверное число » {number}" + +favoritesNone: "У вас нет избранных голов!" +localNone: "Нет локальных голов!" +customCategoriesNone: "Нет дополнительных категорий!" + +purchase: + invalidFunds: "У вас недостаточно денег!" + success: "Вы купили {amount}x {name} за {cost}" + noEconomy: "Вы получили {amount}x {name}" + +menu: + local: + name: "HeadDB » Локальные головы" + favorites: + name: "HeadDB » Избранное" + add: "{name} был ДОБАВЛЕН в избранное!" + remove: "{name} был УДАЛЁН из избранного!" + search: + name: "HeadDB » Поиск » {name}" + customCategories: + name: "HeadDB » Дополнительные категории" + purchase: + name: "HeadDB » {name} » Покупка" + + main: + name: "HeadDB" + local: + name: "Локальные головы" + favorites: + name: "Избранное" + search: + name: "Поиск" + customCategories: + name: "Дополнительные категории" + category: + decoration: + name: "Декорации" + + category: + alphabet: "HeadDB » Алфавит" + animals: "HeadDB » Животные" + blocks: "HeadDB » Блоки" + decoration: "HeadDB » Декорации" + food & drinks: "HeadDB » Еда и напитки" + humanoid: "HeadDB » Гуманоиды" + humans: "HeadDB » Люди" + miscellaneous: "HeadDB » Разное" + monsters: "HeadDB » Монстры" + plants: "HeadDB » Растения" + + controls: + back: + name: "◀ Назад" + lore: "Перейти на предыдущую страницу: ${{BACK}}" + info: + name: "ℹ Страница ${{CURRENT}}/${{MAX}}" + lore: "Нажмите, чтобы вернуться в главное меню." + next: + name: "Далее ▶" + lore: "Перейти на следующую страницу: ${{NEXT}}" + +command: + + open: + opening: "Открытие базы данных голов." + openingFor: "Открытие меню {menu} для игрока {target}." + invalidCategory: "Неверная категория: {category}" + + give: + invalidId: "Неверный ID головы » {id}" + success: "Выдано {amount}x {name} игроку {target}" + + search: + input: "Введите поисковый запрос и фильтры:" + start: " ⏳ Поиск..." + none: "Головы не найдены!" + found: "Найдено {amount} голов!" + filter: | + + 🔎 Фильтры + ─────────────── + Название » {name} + Категория » {category} + Теги » {tags} + ID » {ids} + Режим » {mode}