diff --git a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java index e54cac38..e227d360 100644 --- a/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java +++ b/core/src/main/java/github/nighter/smartspawner/SmartSpawner.java @@ -367,6 +367,10 @@ public SpawnerStorageUI getSpawnerStorageUI() { return spawnerStorageUI; } + public GuiLayoutConfig getGuiLayoutConfig() { + return guiLayoutConfig; + } + public SpawnerManager getSpawnerManager() { return spawnerManager; } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiButton.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiButton.java index b4a499a2..5ac2af98 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiButton.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiButton.java @@ -3,18 +3,36 @@ import lombok.Getter; import org.bukkit.Material; +import java.util.Map; + @Getter public class GuiButton { private final String buttonType; private final int slot; private final Material material; private final boolean enabled; + private final String condition; + private final Map actions; public GuiButton(String buttonType, int slot, Material material, boolean enabled) { + this(buttonType, slot, material, enabled, null, null); + } + + public GuiButton(String buttonType, int slot, Material material, boolean enabled, String condition, Map actions) { this.buttonType = buttonType; this.slot = slot; this.material = material; this.enabled = enabled; + this.condition = condition; + this.actions = actions; + } + + public String getAction(String clickType) { + return actions != null ? actions.get(clickType) : null; + } + + public boolean hasCondition() { + return condition != null && !condition.isEmpty(); } @Override @@ -24,6 +42,8 @@ public String toString() { ", slot=" + slot + ", material=" + material + ", enabled=" + enabled + + ", condition='" + condition + '\'' + + ", actions=" + actions + '}'; } } \ No newline at end of file diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayout.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayout.java index 467e0b7c..2d92bde0 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayout.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayout.java @@ -28,6 +28,11 @@ public Optional getButtonTypeAtSlot(int slot) { return Optional.ofNullable(slotToButtonType.get(slot)); } + public Optional getButtonAtSlot(int slot) { + String buttonType = slotToButtonType.get(slot); + return buttonType != null ? Optional.ofNullable(buttons.get(buttonType)) : Optional.empty(); + } + public boolean hasButton(String buttonType) { return buttons.containsKey(buttonType) && buttons.get(buttonType).isEnabled(); } diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayoutConfig.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayoutConfig.java index ba631c6e..45a745d5 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayoutConfig.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/layout/GuiLayoutConfig.java @@ -1,36 +1,50 @@ package github.nighter.smartspawner.spawner.gui.layout; import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.updates.GuiLayoutUpdater; import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import java.io.File; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; public class GuiLayoutConfig { private static final String GUI_LAYOUTS_DIR = "gui_layouts"; private static final String STORAGE_GUI_FILE = "storage_gui.yml"; + private static final String MAIN_GUI_FILE = "main_gui.yml"; private static final String DEFAULT_LAYOUT = "default"; private static final int MIN_SLOT = 1; private static final int MAX_SLOT = 9; private static final int SLOT_OFFSET = 44; + private static final int MAIN_GUI_SIZE = 27; private final SmartSpawner plugin; private final File layoutsDir; + private final GuiLayoutUpdater layoutUpdater; private String currentLayout; - private GuiLayout currentGuiLayout; + private GuiLayout currentStorageLayout; + private GuiLayout currentMainLayout; public GuiLayoutConfig(SmartSpawner plugin) { this.plugin = plugin; this.layoutsDir = new File(plugin.getDataFolder(), GUI_LAYOUTS_DIR); + this.layoutUpdater = new GuiLayoutUpdater(plugin); loadLayout(); } public void loadLayout() { this.currentLayout = plugin.getConfig().getString("gui_layout", DEFAULT_LAYOUT); initializeLayoutsDirectory(); - this.currentGuiLayout = loadCurrentLayout(); + + // Check and update layout files before loading + layoutUpdater.checkAndUpdateLayouts(); + + this.currentStorageLayout = loadCurrentStorageLayout(); + this.currentMainLayout = loadCurrentMainLayout(); } private void initializeLayoutsDirectory() { @@ -50,15 +64,29 @@ private void autoSaveLayoutFiles() { layoutDir.mkdirs(); } + // Save storage GUI layout File storageFile = new File(layoutDir, STORAGE_GUI_FILE); - String resourcePath = GUI_LAYOUTS_DIR + "/" + layoutName + "/" + STORAGE_GUI_FILE; + String storageResourcePath = GUI_LAYOUTS_DIR + "/" + layoutName + "/" + STORAGE_GUI_FILE; if (!storageFile.exists()) { try { - plugin.saveResource(resourcePath, false); + plugin.saveResource(storageResourcePath, false); } catch (Exception e) { plugin.getLogger().log(Level.WARNING, - "Failed to auto-save layout resource for " + layoutName + ": " + e.getMessage(), e); + "Failed to auto-save storage layout resource for " + layoutName + ": " + e.getMessage(), e); + } + } + + // Save main GUI layout + File mainFile = new File(layoutDir, MAIN_GUI_FILE); + String mainResourcePath = GUI_LAYOUTS_DIR + "/" + layoutName + "/" + MAIN_GUI_FILE; + + if (!mainFile.exists()) { + try { + plugin.saveResource(mainResourcePath, false); + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, + "Failed to auto-save main layout resource for " + layoutName + ": " + e.getMessage(), e); } } } @@ -67,14 +95,22 @@ private void autoSaveLayoutFiles() { } } - private GuiLayout loadCurrentLayout() { + private GuiLayout loadCurrentStorageLayout() { + return loadLayoutFromFile(STORAGE_GUI_FILE, "storage"); + } + + private GuiLayout loadCurrentMainLayout() { + return loadLayoutFromFile(MAIN_GUI_FILE, "main"); + } + + private GuiLayout loadLayoutFromFile(String fileName, String layoutType) { File layoutDir = new File(layoutsDir, currentLayout); - File storageFile = new File(layoutDir, STORAGE_GUI_FILE); + File layoutFile = new File(layoutDir, fileName); - if (storageFile.exists()) { - GuiLayout layout = loadStorageLayout(storageFile); + if (layoutFile.exists()) { + GuiLayout layout = loadLayout(layoutFile, layoutType); if (layout != null) { - plugin.getLogger().info("Loaded GUI layout: " + currentLayout); + plugin.getLogger().info("Loaded " + layoutType + " GUI layout: " + currentLayout); return layout; } } @@ -82,22 +118,22 @@ private GuiLayout loadCurrentLayout() { if (!currentLayout.equals(DEFAULT_LAYOUT)) { plugin.getLogger().warning("Layout '" + currentLayout + "' not found. Attempting to use default layout."); File defaultLayoutDir = new File(layoutsDir, DEFAULT_LAYOUT); - File defaultStorageFile = new File(defaultLayoutDir, STORAGE_GUI_FILE); + File defaultLayoutFile = new File(defaultLayoutDir, fileName); - if (defaultStorageFile.exists()) { - GuiLayout defaultLayout = loadStorageLayout(defaultStorageFile); + if (defaultLayoutFile.exists()) { + GuiLayout defaultLayout = loadLayout(defaultLayoutFile, layoutType); if (defaultLayout != null) { - plugin.getLogger().info("Loaded default layout as fallback"); + plugin.getLogger().info("Loaded default " + layoutType + " layout as fallback"); return defaultLayout; } } } - plugin.getLogger().severe("No valid layout found! Creating empty layout as fallback."); + plugin.getLogger().severe("No valid " + layoutType + " layout found! Creating empty layout as fallback."); return new GuiLayout(); } - private GuiLayout loadStorageLayout(File file) { + private GuiLayout loadLayout(File file, String layoutType) { try { FileConfiguration config = YamlConfiguration.loadConfiguration(file); GuiLayout layout = new GuiLayout(); @@ -108,7 +144,7 @@ private GuiLayout loadStorageLayout(File file) { } for (String buttonKey : config.getConfigurationSection("buttons").getKeys(false)) { - if (!loadButton(config, layout, buttonKey)) { + if (!loadButton(config, layout, buttonKey, layoutType)) { continue; } } @@ -116,12 +152,12 @@ private GuiLayout loadStorageLayout(File file) { return layout; } catch (Exception e) { plugin.getLogger().log(Level.WARNING, - "Failed to load storage layout from " + file.getName() + ": " + e.getMessage(), e); + "Failed to load " + layoutType + " layout from " + file.getName() + ": " + e.getMessage(), e); return null; } } - private boolean loadButton(FileConfiguration config, GuiLayout layout, String buttonKey) { + private boolean loadButton(FileConfiguration config, GuiLayout layout, String buttonKey, String layoutType) { String path = "buttons." + buttonKey; if (!config.getBoolean(path + ".enabled", true)) { @@ -130,24 +166,68 @@ private boolean loadButton(FileConfiguration config, GuiLayout layout, String bu int slot = config.getInt(path + ".slot", -1); String materialName = config.getString(path + ".material", "STONE"); + String condition = config.getString(path + ".condition", null); - if (!isValidSlot(slot)) { + // Validate slot based on layout type + if (!isValidSlot(slot, layoutType)) { plugin.getLogger().warning(String.format( - "Invalid slot %d for button %s. Must be between %d and %d.", - slot, buttonKey, MIN_SLOT, MAX_SLOT)); + "Invalid slot %d for button %s in %s layout. Must be between %d and %d.", + slot, buttonKey, layoutType, getMinSlot(layoutType), getMaxSlot(layoutType))); + return false; + } + + // Check condition if present + if (condition != null && !evaluateCondition(condition)) { return false; } Material material = parseMaterial(materialName, buttonKey); - int actualSlot = SLOT_OFFSET + slot; + int actualSlot = calculateActualSlot(slot, layoutType); - GuiButton button = new GuiButton(buttonKey, actualSlot, material, true); + // Load actions + Map actions = new HashMap<>(); + ConfigurationSection actionsSection = config.getConfigurationSection(path + ".actions"); + if (actionsSection != null) { + for (String actionKey : actionsSection.getKeys(false)) { + actions.put(actionKey, actionsSection.getString(actionKey)); + } + } + + GuiButton button = new GuiButton(buttonKey, actualSlot, material, true, condition, actions); layout.addButton(buttonKey, button); return true; } - private boolean isValidSlot(int slot) { - return slot >= MIN_SLOT && slot <= MAX_SLOT; + private boolean isValidSlot(int slot, String layoutType) { + return slot >= getMinSlot(layoutType) && slot <= getMaxSlot(layoutType); + } + + private int getMinSlot(String layoutType) { + return "storage".equals(layoutType) ? MIN_SLOT : 1; + } + + private int getMaxSlot(String layoutType) { + return "storage".equals(layoutType) ? MAX_SLOT : MAIN_GUI_SIZE; + } + + private int calculateActualSlot(int slot, String layoutType) { + if ("storage".equals(layoutType)) { + return SLOT_OFFSET + slot; + } else { + return slot - 1; // Convert 1-based to 0-based indexing for main GUI + } + } + + private boolean evaluateCondition(String condition) { + switch (condition) { + case "shop_integration": + return plugin.hasSellIntegration(); + case "no_shop_integration": + return !plugin.hasSellIntegration(); + default: + plugin.getLogger().warning("Unknown condition: " + condition); + return true; + } } private Material parseMaterial(String materialName, String buttonKey) { @@ -162,7 +242,15 @@ private Material parseMaterial(String materialName, String buttonKey) { } public GuiLayout getCurrentLayout() { - return currentGuiLayout; + return getCurrentStorageLayout(); + } + + public GuiLayout getCurrentStorageLayout() { + return currentStorageLayout; + } + + public GuiLayout getCurrentMainLayout() { + return currentMainLayout; } public void reloadLayouts() { diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuAction.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuAction.java index e6ce4894..54959635 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuAction.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuAction.java @@ -93,8 +93,16 @@ public void onMenuClick(InventoryClickEvent event) { return; } - Material itemType = clickedItem.getType(); + // Use layout-based action handling + int slot = event.getRawSlot(); + String clickType = getClickTypeString(event.getClick()); + + if (handleLayoutAction(player, spawner, slot, clickType)) { + return; + } + // Fallback to legacy material-based handling for backward compatibility + Material itemType = clickedItem.getType(); if (itemType == Material.CHEST) { handleChestClick(player, spawner); } else if (SPAWNER_INFO_MATERIALS.contains(itemType)) { @@ -104,6 +112,73 @@ public void onMenuClick(InventoryClickEvent event) { } } + private boolean handleLayoutAction(Player player, SpawnerData spawner, int slot, String clickType) { + var layoutConfig = plugin.getGuiLayoutConfig(); + var layout = layoutConfig.getCurrentMainLayout(); + + if (layout == null) { + return false; + } + + var buttonOpt = layout.getButtonAtSlot(slot); + if (buttonOpt.isEmpty()) { + return false; + } + + var button = buttonOpt.get(); + String action = button.getAction(clickType); + + if (action == null) { + return false; + } + + switch (action) { + case "open_storage": + handleChestClick(player, spawner); + return true; + case "open_stacker": + if (isClickTooFrequent(player)) { + return true; + } + // Check stacker permission and open stacker GUI + if (!player.hasPermission("smartspawner.stack")) { + messageService.sendMessage(player, "no_permission"); + return true; + } + spawnerStackerUI.openStackerGui(player, spawner); + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + return true; + case "sell_inventory": + if (isClickTooFrequent(player)) { + return true; + } + // Check permissions for selling (same logic as handleSpawnerInfoClick) + if (!plugin.hasSellIntegration() || !player.hasPermission("smartspawner.sellall")) { + messageService.sendMessage(player, "no_permission"); + return true; + } + // Collect EXP and sell items in storage + handleExpBottleClick(player, spawner, true); + handleSellAllItems(player, spawner); + return true; + case "collect_exp": + handleExpBottleClick(player, spawner, false); + return true; + default: + return false; + } + } + + private String getClickTypeString(ClickType clickType) { + return switch (clickType) { + case LEFT -> "left_click"; + case RIGHT -> "right_click"; + case SHIFT_LEFT -> "shift_left_click"; + case SHIFT_RIGHT -> "shift_right_click"; + default -> "left_click"; + }; + } + public void handleChestClick(Player player, SpawnerData spawner) { String title = languageManager.getGuiTitle("gui_title_storage"); Inventory pageInventory = spawnerStorageUI.createInventory(spawner, title, 1, -1); diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java index aecbefb2..1eeed437 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java @@ -1,6 +1,8 @@ package github.nighter.smartspawner.spawner.gui.main; import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.spawner.gui.layout.GuiLayout; +import github.nighter.smartspawner.spawner.gui.layout.GuiButton; import github.nighter.smartspawner.spawner.loot.EntityLootConfig; import github.nighter.smartspawner.spawner.loot.LootItem; import github.nighter.smartspawner.spawner.utils.SpawnerMobHeadTexture; @@ -20,9 +22,7 @@ public class SpawnerMenuUI { private static final int INVENTORY_SIZE = 27; - private static final int CHEST_SLOT = 11; - private static final int SPAWNER_INFO_SLOT = 13; - private static final int EXP_SLOT = 15; + // Remove hardcoded slot constants - now using layout config private static final int TICKS_PER_SECOND = 20; private static final Map EMPTY_PLACEHOLDERS = Collections.emptyMap(); @@ -48,12 +48,28 @@ public SpawnerMenuUI(SmartSpawner plugin) { public void openSpawnerMenu(Player player, SpawnerData spawner, boolean refresh) { Inventory menu = createMenu(spawner); + GuiLayout layout = plugin.getGuiLayoutConfig().getCurrentMainLayout(); - // Populate menu items - create all items before opening to avoid multiple inventory updates + // Populate menu items based on layout configuration ItemStack[] items = new ItemStack[INVENTORY_SIZE]; - items[CHEST_SLOT] = createLootStorageItem(spawner); - items[SPAWNER_INFO_SLOT] = createSpawnerInfoItem(player, spawner); - items[EXP_SLOT] = createExpItem(spawner); + + // Add storage button if enabled in layout + GuiButton storageButton = layout.getButton("storage"); + if (storageButton != null) { + items[storageButton.getSlot()] = createLootStorageItem(spawner); + } + + // Add spawner info button if enabled in layout + GuiButton spawnerInfoButton = layout.getButton("spawner_info"); + if (spawnerInfoButton != null) { + items[spawnerInfoButton.getSlot()] = createSpawnerInfoItem(player, spawner); + } + + // Add exp button if enabled in layout + GuiButton expButton = layout.getButton("exp"); + if (expButton != null) { + items[expButton.getSlot()] = createExpItem(spawner); + } // Set all items at once instead of one by one for (int i = 0; i < items.length; i++) { @@ -228,6 +244,11 @@ private String buildLootItemsText(EntityType entityType, Map buttonOpt = layout.getButtonAtSlot(slot); + if (buttonOpt.isEmpty()) { + return; + } + + GuiButton button = buttonOpt.get(); + + // For now, we'll use left_click as the default action since storage GUI + // currently doesn't differentiate between click types in most cases + String action = button.getAction("left_click"); + if (action == null) { + // Fallback to legacy button type handling + handleLegacyButtonType(player, slot, holder, spawner, inventory, layout); + return; + } + + switch (action) { + case "discard_all": + handleDiscardAllItems(player, spawner, inventory); + break; + case "item_filter": + openFilterConfig(player, spawner); + break; + case "previous_page": + if (holder.getCurrentPage() > 1) { + updatePageContent(player, spawner, holder.getCurrentPage() - 1, inventory, true); + } + break; + case "take_all": + handleTakeAllItems(player, inventory); + break; + case "next_page": + if (holder.getCurrentPage() < holder.getTotalPages()) { + updatePageContent(player, spawner, holder.getCurrentPage() + 1, inventory, true); + } + break; + case "drop_page": + handleDropPageItems(player, spawner, inventory); + break; + case "sell_all": + if (plugin.hasSellIntegration()) { + if (!player.hasPermission("smartspawner.sellall")) { + messageService.sendMessage(player, "no_permission"); + return; + } + if (isClickTooFrequent(player)) { + return; + } + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + spawnerSellManager.sellAllItems(player, spawner); + } + break; + case "return": + openMainMenu(player, spawner); + break; + default: + // Fallback to legacy handling for unknown actions + handleLegacyButtonType(player, slot, holder, spawner, inventory, layout); + break; + } + } + + private void handleLegacyButtonType(Player player, int slot, StoragePageHolder holder, + SpawnerData spawner, Inventory inventory, GuiLayout layout) { Optional buttonTypeOpt = layout.getButtonTypeAtSlot(slot); if (buttonTypeOpt.isEmpty()) { return; diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java index a590acc4..7cb17f1e 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java @@ -119,6 +119,16 @@ private void initializeStaticButtons() { languageManager.getGuiItemLoreAsList("item_filter_button.lore") )); } + + // Create sell button + GuiButton sellButton = layout.getButton("sell"); + if (sellButton != null) { + staticButtons.put("sell", createButton( + sellButton.getMaterial(), + languageManager.getGuiItemName("sell_button.name"), + languageManager.getGuiItemLoreAsList("sell_button.lore") + )); + } } public Inventory createInventory(SpawnerData spawner, String title, int page, int totalPages) { @@ -278,15 +288,15 @@ private void addNavigationButtons(Map updates, SpawnerData s updates.put(returnButton.getSlot(), staticButtons.get("return")); } - // Add shop page indicator only if shop integration is available and button is enabled - if (plugin.hasSellIntegration() && layout.hasButton("shop_indicator")) { - GuiButton shopButton = layout.getButton("shop_indicator"); + // Add sell button if shop integration is available and button is enabled + if (layout.hasButton("sell")) { + GuiButton sellButton = layout.getButton("sell"); String indicatorKey = getPageIndicatorKey(page, totalPages, spawner); int finalTotalPages = totalPages; - ItemStack shopIndicator = pageIndicatorCache.computeIfAbsent( - indicatorKey, k -> createShopPageIndicator(page, finalTotalPages, spawner, shopButton.getMaterial()) + ItemStack sellIndicator = pageIndicatorCache.computeIfAbsent( + indicatorKey, k -> createSellPageIndicator(page, finalTotalPages, spawner, sellButton.getMaterial()) ); - updates.put(shopButton.getSlot(), shopIndicator); + updates.put(sellButton.getSlot(), sellIndicator); } } @@ -334,7 +344,7 @@ private ItemStack createNavigationButton(String type, int targetPage, Material m return createButton(material, buttonName, Arrays.asList(buttonLore)); } - private ItemStack createShopPageIndicator(int currentPage, int totalPages, SpawnerData spawner, Material material) { + private ItemStack createSellPageIndicator(int currentPage, int totalPages, SpawnerData spawner, Material material) { VirtualInventory virtualInv = spawner.getVirtualInventory(); int maxSlots = spawner.getMaxSpawnerLootSlots(); int usedSlots = virtualInv.getUsedSlots(); @@ -350,8 +360,8 @@ private ItemStack createShopPageIndicator(int currentPage, int totalPages, Spawn placeholders.put("used_slots", formattedUsedSlots); placeholders.put("percent_storage", String.valueOf(percentStorage)); - String name = languageManager.getGuiItemName("shop_page_indicator.name", placeholders); - List lore = languageManager.getGuiItemLoreAsList("shop_page_indicator.lore", placeholders); + String name = languageManager.getGuiItemName("sell_page_indicator.name", placeholders); + List lore = languageManager.getGuiItemLoreAsList("sell_page_indicator.lore", placeholders); return createButton(material, name, lore); } diff --git a/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java b/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java new file mode 100644 index 00000000..ba7f4e1b --- /dev/null +++ b/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java @@ -0,0 +1,167 @@ +package github.nighter.smartspawner.updates; + +import github.nighter.smartspawner.SmartSpawner; +import github.nighter.smartspawner.updates.Version; +import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.logging.Level; + +public class GuiLayoutUpdater { + private static final String GUI_LAYOUT_VERSION_KEY = "gui_layout_version"; + private static final String GUI_LAYOUTS_DIR = "gui_layouts"; + private static final String[] LAYOUT_FILES = {"storage_gui.yml", "main_gui.yml"}; + private static final String[] LAYOUT_NAMES = {"default", "DonutSMP"}; + + private final SmartSpawner plugin; + private final String currentVersion; + + public GuiLayoutUpdater(SmartSpawner plugin) { + this.plugin = plugin; + this.currentVersion = plugin.getDescription().getVersion(); + } + + public void checkAndUpdateLayouts() { + File layoutsDir = new File(plugin.getDataFolder(), GUI_LAYOUTS_DIR); + if (!layoutsDir.exists()) { + return; + } + + for (String layoutName : LAYOUT_NAMES) { + File layoutDir = new File(layoutsDir, layoutName); + if (!layoutDir.exists()) { + continue; + } + + for (String fileName : LAYOUT_FILES) { + File layoutFile = new File(layoutDir, fileName); + if (layoutFile.exists()) { + updateLayoutFile(layoutName, layoutFile, fileName); + } + } + } + } + + private void updateLayoutFile(String layoutName, File layoutFile, String fileName) { + try { + FileConfiguration currentConfig = YamlConfiguration.loadConfiguration(layoutFile); + String configVersionStr = currentConfig.getString(GUI_LAYOUT_VERSION_KEY, "0.0.0"); + + if (configVersionStr.equals("0.0.0")) { + plugin.debug("No version found in " + layoutName + "/" + fileName + ", creating default layout file with header"); + createDefaultLayoutFileWithHeader(layoutName, layoutFile, fileName); + return; + } + + Version configVersion = new Version(configVersionStr); + Version pluginVersion = new Version(currentVersion); + + if (configVersion.compareTo(pluginVersion) >= 0) { + return; + } + + if (!configVersionStr.equals("0.0.0")) { + plugin.debug("Updating " + layoutName + " " + fileName + + " from version " + configVersionStr + " to " + currentVersion); + } + + Map userValues = flattenConfig(currentConfig); + + File tempFile = new File(layoutFile.getParent(), fileName.replace(".yml", "_new.yml")); + createDefaultLayoutFileWithHeader(layoutName, tempFile, fileName); + + FileConfiguration newConfig = YamlConfiguration.loadConfiguration(tempFile); + newConfig.set(GUI_LAYOUT_VERSION_KEY, currentVersion); + + boolean configDiffers = hasConfigDifferences(userValues, newConfig); + + if (configDiffers) { + File backupFile = new File(layoutFile.getParent(), fileName.replace(".yml", "_backup_" + configVersionStr + ".yml")); + Files.copy(layoutFile.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + plugin.getLogger().info("Layout backup created at " + backupFile.getName()); + } else { + plugin.debug("No significant layout changes detected for " + layoutName + "/" + fileName + ", skipping backup creation"); + } + + applyUserValues(newConfig, userValues); + newConfig.save(layoutFile); + tempFile.delete(); + + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Failed to update layout " + layoutName + "/" + fileName + ": " + e.getMessage(), e); + } + } + + private void createDefaultLayoutFileWithHeader(String layoutName, File destinationFile, String fileName) { + try { + String resourcePath = GUI_LAYOUTS_DIR + "/" + layoutName + "/" + fileName; + plugin.saveResource(resourcePath, true); + + FileConfiguration config = YamlConfiguration.loadConfiguration(destinationFile); + config.set(GUI_LAYOUT_VERSION_KEY, currentVersion); + config.save(destinationFile); + + } catch (Exception e) { + plugin.getLogger().log(Level.WARNING, "Failed to create default layout file with header for " + layoutName + "/" + fileName, e); + } + } + + private boolean hasConfigDifferences(Map userValues, FileConfiguration newConfig) { + Map newValues = flattenConfig(newConfig); + + for (Map.Entry entry : userValues.entrySet()) { + String path = entry.getKey(); + Object userValue = entry.getValue(); + + if (path.equals(GUI_LAYOUT_VERSION_KEY)) continue; + + if (!newValues.containsKey(path)) { + return true; + } + + Object newValue = newValues.get(path); + if (!Objects.equals(userValue, newValue)) { + return true; + } + } + + for (String path : newValues.keySet()) { + if (!path.equals(GUI_LAYOUT_VERSION_KEY) && !userValues.containsKey(path)) { + return true; + } + } + + return false; + } + + private Map flattenConfig(ConfigurationSection config) { + Map result = new HashMap<>(); + for (String key : config.getKeys(true)) { + if (!config.isConfigurationSection(key)) { + result.put(key, config.get(key)); + } + } + return result; + } + + private void applyUserValues(FileConfiguration newConfig, Map userValues) { + for (Map.Entry entry : userValues.entrySet()) { + String path = entry.getKey(); + Object value = entry.getValue(); + + if (path.equals(GUI_LAYOUT_VERSION_KEY)) continue; + + if (newConfig.contains(path)) { + newConfig.set(path, value); + } else { + plugin.getLogger().fine("Layout path '" + path + "' from old config no longer exists in new config"); + } + } + } +} \ No newline at end of file diff --git a/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml new file mode 100644 index 00000000..2811a14a --- /dev/null +++ b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml @@ -0,0 +1,48 @@ +# DonutSMP Main GUI Layout Configuration +# This configures the main spawner GUI layout (27 slots, 3 rows of 9) +# Valid materials can be found at: https://jd.papermc.io/paper/1.21.8/org/bukkit/Material.html +# Note: For PLAYER_HEAD material, the texture will be automatically set based on the spawner entity type + +gui_layout_version: "1.0.0" + +buttons: + + # Storage access button + storage: + slot: 11 + material: CHEST + enabled: true + actions: + left_click: "open_storage" + right_click: "open_storage" + + # Spawner information display with shop integration + # With shop integration: Left click for selling/XP, right click for stacker + spawner_info_with_shop: + slot: 13 + material: PLAYER_HEAD + enabled: true + condition: "shop_integration" + actions: + left_click: "sell_inventory" # Collect EXP and sell items in storage + right_click: "open_stacker" # Open stacker GUI + + # Spawner information display without shop integration + # Without shop integration: Both clicks open stacker GUI + spawner_info_no_shop: + slot: 13 + material: PLAYER_HEAD + enabled: true + condition: "no_shop_integration" + actions: + left_click: "open_stacker" # Open stacker GUI + right_click: "open_stacker" # Open stacker GUI + + # Experience collection button + exp: + slot: 15 + material: EXPERIENCE_BOTTLE + enabled: true + actions: + left_click: "collect_exp" + right_click: "collect_exp" \ No newline at end of file diff --git a/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml b/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml index 2fd0fc75..a6d591e4 100644 --- a/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml +++ b/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml @@ -2,6 +2,8 @@ # Slot positions: 1-9 corresponds to inventory slots 46-54 (bottom row) # Valid materials can be found at: https://jd.papermc.io/paper/1.21.6/org/bukkit/Material.html +gui_layout_version: "1.0.0" + buttons: # Return to main menu button return: @@ -15,11 +17,19 @@ buttons: material: ARROW enabled: true - # Take all items button + # Conditional button: Sell button if shop integration available, Take all button otherwise + sell: + slot: 5 + material: GOLD_INGOT + enabled: true + condition: "shop_integration" + + # Take all items button (fallback for slot 5 when no shop integration) take_all: slot: 5 material: SPECTRAL_ARROW enabled: true + condition: "no_shop_integration" # Next page navigation button next_page: @@ -32,9 +42,3 @@ buttons: material: DROPPER enabled: true - # Shop/sell indicator button (requires sell integration) - shop_indicator: - slot: 9 - material: GOLD_INGOT - enabled: true - diff --git a/core/src/main/resources/gui_layouts/default/main_gui.yml b/core/src/main/resources/gui_layouts/default/main_gui.yml new file mode 100644 index 00000000..e5cb296e --- /dev/null +++ b/core/src/main/resources/gui_layouts/default/main_gui.yml @@ -0,0 +1,48 @@ +# Default Main GUI Layout Configuration +# This configures the main spawner GUI layout (27 slots, 3 rows of 9) +# Valid materials can be found at: https://jd.papermc.io/paper/1.21.8/org/bukkit/Material.html +# Note: For PLAYER_HEAD material, the texture will be automatically set based on the spawner entity type + +gui_layout_version: "1.0.0" + +buttons: + + # Storage access button + storage: + slot: 11 + material: CHEST + enabled: true + actions: + left_click: "open_storage" + right_click: "open_storage" + + # Spawner information display with shop integration + # With shop integration: Left click for selling/XP, right click for stacker + spawner_info_with_shop: + slot: 13 + material: PLAYER_HEAD + enabled: true + condition: "shop_integration" + actions: + left_click: "sell_inventory" # Collect EXP and sell items in storage + right_click: "open_stacker" # Open stacker GUI + + # Spawner information display without shop integration + # Without shop integration: Both clicks open stacker GUI + spawner_info_no_shop: + slot: 13 + material: PLAYER_HEAD + enabled: true + condition: "no_shop_integration" + actions: + left_click: "open_stacker" # Open stacker GUI + right_click: "open_stacker" # Open stacker GUI + + # Experience collection button + exp: + slot: 15 + material: EXPERIENCE_BOTTLE + enabled: true + actions: + left_click: "collect_exp" + right_click: "collect_exp" \ No newline at end of file diff --git a/core/src/main/resources/gui_layouts/default/storage_gui.yml b/core/src/main/resources/gui_layouts/default/storage_gui.yml index 0f9c11a0..cc62bb58 100644 --- a/core/src/main/resources/gui_layouts/default/storage_gui.yml +++ b/core/src/main/resources/gui_layouts/default/storage_gui.yml @@ -2,6 +2,8 @@ # Slot positions: 1-9 corresponds to inventory slots 46-54 (bottom row) # Valid materials can be found at: https://jd.papermc.io/paper/1.21.8/org/bukkit/Material.html +gui_layout_version: "1.0.0" + buttons: # Previous page navigation button @@ -22,11 +24,19 @@ buttons: material: HOPPER enabled: true - # Return to main menu button + # Sell or Return button (handled by legacy button type logic) + sell: + slot: 5 + material: GOLD_INGOT + enabled: true + condition: "shop_integration" + + # Return to main menu button (fallback for slot 5 when no shop integration) return: slot: 5 material: RED_STAINED_GLASS_PANE enabled: true + condition: "no_shop_integration" # Take all items button take_all: @@ -39,12 +49,6 @@ buttons: material: DROPPER enabled: true - # Shop/sell indicator button (requires sell integration) - shop_indicator: - slot: 8 - material: GOLD_INGOT - enabled: true - # Next page navigation button next_page: slot: 9