From 99965afbba817e463f2c3c4704d42cf5a2dde429 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 02:12:44 +0000 Subject: [PATCH 1/7] Initial plan From ba8cc2395e4fc5e9847cda0e9b13546af52fb485 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 02:21:16 +0000 Subject: [PATCH 2/7] Add GuiLayoutUpdater and conditional button system for storage GUI Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../spawner/gui/layout/GuiButton.java | 20 +++ .../spawner/gui/layout/GuiLayoutConfig.java | 142 ++++++++++++--- .../spawner/gui/storage/SpawnerStorageUI.java | 29 ++- .../updates/GuiLayoutUpdater.java | 167 ++++++++++++++++++ .../gui_layouts/DonutSMP/main_gui.yml | 37 ++++ .../gui_layouts/DonutSMP/storage_gui.yml | 36 +++- .../gui_layouts/default/main_gui.yml | 37 ++++ .../gui_layouts/default/storage_gui.yml | 46 ++++- 8 files changed, 463 insertions(+), 51 deletions(-) create mode 100644 core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java create mode 100644 core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml create mode 100644 core/src/main/resources/gui_layouts/default/main_gui.yml 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/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/storage/SpawnerStorageUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageUI.java index a590acc4..66f9770e 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,17 @@ 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 +289,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 +345,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 +361,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..9d2f33fe --- /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.utils.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..6672a5a8 --- /dev/null +++ b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml @@ -0,0 +1,37 @@ +# 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 MOB_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 + # Material will be MOB_HEAD with texture based on spawner entity type + # Players can change material if they want different button appearance + spawner_info: + slot: 13 + material: MOB_HEAD + enabled: true + actions: + left_click: "toggle_info" + right_click: "sell_inventory" + + # 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..0e3c47b0 100644 --- a/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml +++ b/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml @@ -2,39 +2,61 @@ # 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: slot: 1 material: BARRIER enabled: true + actions: + left_click: "return" + right_click: "return" # Previous page navigation button previous_page: slot: 4 material: ARROW enabled: true + actions: + left_click: "previous_page" + right_click: "previous_page" + + # Conditional button: Sell button if shop integration available, Take all button otherwise + sell: + slot: 5 + material: GOLD_INGOT + enabled: true + condition: "shop_integration" + actions: + left_click: "sell_all" + right_click: "sell_all" - # Take all items button + # 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" + actions: + left_click: "take_all" + right_click: "take_all" # Next page navigation button next_page: slot: 6 material: ARROW enabled: true + actions: + left_click: "next_page" + right_click: "next_page" drop_page: slot: 8 material: DROPPER enabled: true - - # Shop/sell indicator button (requires sell integration) - shop_indicator: - slot: 9 - material: GOLD_INGOT - enabled: true + actions: + left_click: "drop_page" + right_click: "drop_page" 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..e745bcc4 --- /dev/null +++ b/core/src/main/resources/gui_layouts/default/main_gui.yml @@ -0,0 +1,37 @@ +# 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 MOB_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 + # Material will be MOB_HEAD with texture based on spawner entity type + # Players can change material if they want different button appearance + spawner_info: + slot: 13 + material: MOB_HEAD + enabled: true + actions: + left_click: "toggle_info" + right_click: "sell_inventory" + + # 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..c532337f 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 @@ -9,44 +11,72 @@ buttons: slot: 1 material: SPECTRAL_ARROW enabled: true + actions: + left_click: "previous_page" + right_click: "previous_page" # Discard all items button discard_all: slot: 3 material: CAULDRON enabled: true + actions: + left_click: "discard_all" + right_click: "discard_all" # Item filter configuration button item_filter: slot: 4 material: HOPPER enabled: true + actions: + left_click: "item_filter" + right_click: "item_filter" - # Return to main menu button + # Conditional button: Sell button if shop integration available, Return button otherwise + # This button will display as "sell" if shop integration is available, + # otherwise it will function as an additional return button + sell: + slot: 5 + material: GOLD_INGOT + enabled: true + condition: "shop_integration" + actions: + left_click: "sell_all" + right_click: "sell_all" + + # 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" + actions: + left_click: "return" + right_click: "return" # Take all items button take_all: slot: 6 material: CHEST enabled: true + actions: + left_click: "take_all" + right_click: "take_all" drop_page: slot: 7 material: DROPPER enabled: true - - # Shop/sell indicator button (requires sell integration) - shop_indicator: - slot: 8 - material: GOLD_INGOT - enabled: true + actions: + left_click: "drop_page" + right_click: "drop_page" # Next page navigation button next_page: slot: 9 material: SPECTRAL_ARROW - enabled: true \ No newline at end of file + enabled: true + actions: + left_click: "next_page" + right_click: "next_page" \ No newline at end of file From 5fd6c5a96fb8b81b5785fbd3a1a8e5d5353d013a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 02:24:19 +0000 Subject: [PATCH 3/7] Update SpawnerMenuUI to use layout configuration for main GUI Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../nighter/smartspawner/SmartSpawner.java | 4 ++ .../spawner/gui/main/SpawnerMenuUI.java | 53 +++++++++++++++---- 2 files changed, 47 insertions(+), 10 deletions(-) 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/main/SpawnerMenuUI.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java index aecbefb2..88857a31 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 Date: Sun, 14 Sep 2025 02:43:31 +0000 Subject: [PATCH 4/7] Fix compilation error and implement action configuration system Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../spawner/gui/layout/GuiLayout.java | 5 ++ .../spawner/gui/main/SpawnerMenuAction.java | 72 ++++++++++++++++++- .../gui/storage/SpawnerStorageAction.java | 64 +++++++++++++++++ .../spawner/gui/storage/SpawnerStorageUI.java | 1 - 4 files changed, 140 insertions(+), 2 deletions(-) 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/main/SpawnerMenuAction.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/main/SpawnerMenuAction.java index e6ce4894..0bdbe209 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,68 @@ 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 "toggle_info": + 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; + } + // Collect EXP and sell items if shop integration is available + 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/storage/SpawnerStorageAction.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java index 9271db74..a919a663 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java @@ -139,6 +139,70 @@ private void handleControlSlotClick(Player player, int slot, StoragePageHolder h return; } + Optional 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 66f9770e..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 @@ -129,7 +129,6 @@ private void initializeStaticButtons() { languageManager.getGuiItemLoreAsList("sell_button.lore") )); } - } } public Inventory createInventory(SpawnerData spawner, String title, int page, int totalPages) { From a9fe0f7b858afad85408c6acdf37de74aaebb41e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 03:08:21 +0000 Subject: [PATCH 5/7] Fix import error and update action configurations per feedback Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../spawner/gui/main/SpawnerMenuAction.java | 2 +- .../updates/GuiLayoutUpdater.java | 2 +- .../gui_layouts/DonutSMP/main_gui.yml | 4 ++- .../gui_layouts/DonutSMP/storage_gui.yml | 18 ----------- .../gui_layouts/default/main_gui.yml | 4 ++- .../gui_layouts/default/storage_gui.yml | 30 ++----------------- 6 files changed, 10 insertions(+), 50 deletions(-) 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 0bdbe209..2711ed71 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 @@ -136,7 +136,7 @@ private boolean handleLayoutAction(Player player, SpawnerData spawner, int slot, case "open_storage": handleChestClick(player, spawner); return true; - case "toggle_info": + case "open_stacker": if (isClickTooFrequent(player)) { return true; } diff --git a/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java b/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java index 9d2f33fe..ba7f4e1b 100644 --- a/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java +++ b/core/src/main/java/github/nighter/smartspawner/updates/GuiLayoutUpdater.java @@ -1,7 +1,7 @@ package github.nighter.smartspawner.updates; import github.nighter.smartspawner.SmartSpawner; -import github.nighter.smartspawner.utils.Version; +import github.nighter.smartspawner.updates.Version; import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; diff --git a/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml index 6672a5a8..0eae163c 100644 --- a/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml +++ b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml @@ -19,12 +19,14 @@ buttons: # Spawner information display # Material will be MOB_HEAD with texture based on spawner entity type # Players can change material if they want different button appearance + # Left click: Opens stacker GUI + # Right click: Collects XP + sells items (if shop available) spawner_info: slot: 13 material: MOB_HEAD enabled: true actions: - left_click: "toggle_info" + left_click: "open_stacker" right_click: "sell_inventory" # Experience collection button 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 0e3c47b0..a6d591e4 100644 --- a/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml +++ b/core/src/main/resources/gui_layouts/DonutSMP/storage_gui.yml @@ -10,18 +10,12 @@ buttons: slot: 1 material: BARRIER enabled: true - actions: - left_click: "return" - right_click: "return" # Previous page navigation button previous_page: slot: 4 material: ARROW enabled: true - actions: - left_click: "previous_page" - right_click: "previous_page" # Conditional button: Sell button if shop integration available, Take all button otherwise sell: @@ -29,9 +23,6 @@ buttons: material: GOLD_INGOT enabled: true condition: "shop_integration" - actions: - left_click: "sell_all" - right_click: "sell_all" # Take all items button (fallback for slot 5 when no shop integration) take_all: @@ -39,24 +30,15 @@ buttons: material: SPECTRAL_ARROW enabled: true condition: "no_shop_integration" - actions: - left_click: "take_all" - right_click: "take_all" # Next page navigation button next_page: slot: 6 material: ARROW enabled: true - actions: - left_click: "next_page" - right_click: "next_page" drop_page: slot: 8 material: DROPPER enabled: true - actions: - left_click: "drop_page" - right_click: "drop_page" diff --git a/core/src/main/resources/gui_layouts/default/main_gui.yml b/core/src/main/resources/gui_layouts/default/main_gui.yml index e745bcc4..bb38642c 100644 --- a/core/src/main/resources/gui_layouts/default/main_gui.yml +++ b/core/src/main/resources/gui_layouts/default/main_gui.yml @@ -19,12 +19,14 @@ buttons: # Spawner information display # Material will be MOB_HEAD with texture based on spawner entity type # Players can change material if they want different button appearance + # Left click: Opens stacker GUI + # Right click: Collects XP + sells items (if shop available) spawner_info: slot: 13 material: MOB_HEAD enabled: true actions: - left_click: "toggle_info" + left_click: "open_stacker" right_click: "sell_inventory" # Experience collection button 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 c532337f..cc62bb58 100644 --- a/core/src/main/resources/gui_layouts/default/storage_gui.yml +++ b/core/src/main/resources/gui_layouts/default/storage_gui.yml @@ -11,39 +11,25 @@ buttons: slot: 1 material: SPECTRAL_ARROW enabled: true - actions: - left_click: "previous_page" - right_click: "previous_page" # Discard all items button discard_all: slot: 3 material: CAULDRON enabled: true - actions: - left_click: "discard_all" - right_click: "discard_all" # Item filter configuration button item_filter: slot: 4 material: HOPPER enabled: true - actions: - left_click: "item_filter" - right_click: "item_filter" - # Conditional button: Sell button if shop integration available, Return button otherwise - # This button will display as "sell" if shop integration is available, - # otherwise it will function as an additional return button + # Sell or Return button (handled by legacy button type logic) sell: slot: 5 material: GOLD_INGOT enabled: true condition: "shop_integration" - actions: - left_click: "sell_all" - right_click: "sell_all" # Return to main menu button (fallback for slot 5 when no shop integration) return: @@ -51,32 +37,20 @@ buttons: material: RED_STAINED_GLASS_PANE enabled: true condition: "no_shop_integration" - actions: - left_click: "return" - right_click: "return" # Take all items button take_all: slot: 6 material: CHEST enabled: true - actions: - left_click: "take_all" - right_click: "take_all" drop_page: slot: 7 material: DROPPER enabled: true - actions: - left_click: "drop_page" - right_click: "drop_page" # Next page navigation button next_page: slot: 9 material: SPECTRAL_ARROW - enabled: true - actions: - left_click: "next_page" - right_click: "next_page" \ No newline at end of file + enabled: true \ No newline at end of file From b79dc276ac63956085529a8fe7ce18c2778151a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 03:28:13 +0000 Subject: [PATCH 6/7] Fix Material.MOB_HEAD compilation error by changing to Material.PLAYER_HEAD Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../nighter/smartspawner/spawner/gui/main/SpawnerMenuUI.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 88857a31..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 @@ -281,7 +281,7 @@ public ItemStack createSpawnerInfoItem(Player player, SpawnerData spawner) { // Not in cache, create the ItemStack ItemStack spawnerItem; - if (spawnerInfoButton != null && spawnerInfoButton.getMaterial() == Material.MOB_HEAD) { + if (spawnerInfoButton != null && spawnerInfoButton.getMaterial() == Material.PLAYER_HEAD) { // Use custom head texture for MOB_HEAD material spawnerItem = SpawnerMobHeadTexture.getCustomHead(entityType, player); } else if (spawnerInfoButton != null) { From 1c3273aafb9a81283265b1017acd2e59e5e4a501 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 03:39:38 +0000 Subject: [PATCH 7/7] Fix GuiButton import error and implement conditional GUI configuration system Co-authored-by: ptthanh02 <73684260+ptthanh02@users.noreply.github.com> --- .../spawner/gui/main/SpawnerMenuAction.java | 7 ++++- .../gui/storage/SpawnerStorageAction.java | 1 + .../gui_layouts/DonutSMP/main_gui.yml | 29 ++++++++++++------- .../gui_layouts/default/main_gui.yml | 29 ++++++++++++------- 4 files changed, 45 insertions(+), 21 deletions(-) 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 2711ed71..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 @@ -152,7 +152,12 @@ private boolean handleLayoutAction(Player player, SpawnerData spawner, int slot, if (isClickTooFrequent(player)) { return true; } - // Collect EXP and sell items if shop integration is available + // 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; diff --git a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java index a919a663..1e54a59d 100644 --- a/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java +++ b/core/src/main/java/github/nighter/smartspawner/spawner/gui/storage/SpawnerStorageAction.java @@ -10,6 +10,7 @@ import github.nighter.smartspawner.spawner.gui.main.SpawnerMenuUI; import github.nighter.smartspawner.spawner.gui.synchronization.SpawnerGuiViewManager; import github.nighter.smartspawner.spawner.gui.layout.GuiLayout; +import github.nighter.smartspawner.spawner.gui.layout.GuiButton; import github.nighter.smartspawner.spawner.properties.SpawnerManager; import github.nighter.smartspawner.spawner.properties.VirtualInventory; import github.nighter.smartspawner.language.LanguageManager; diff --git a/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml index 0eae163c..2811a14a 100644 --- a/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml +++ b/core/src/main/resources/gui_layouts/DonutSMP/main_gui.yml @@ -1,7 +1,7 @@ # 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 MOB_HEAD material, the texture will be automatically set based on the spawner entity type +# Note: For PLAYER_HEAD material, the texture will be automatically set based on the spawner entity type gui_layout_version: "1.0.0" @@ -16,18 +16,27 @@ buttons: left_click: "open_storage" right_click: "open_storage" - # Spawner information display - # Material will be MOB_HEAD with texture based on spawner entity type - # Players can change material if they want different button appearance - # Left click: Opens stacker GUI - # Right click: Collects XP + sells items (if shop available) - spawner_info: + # 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: MOB_HEAD + material: PLAYER_HEAD enabled: true + condition: "shop_integration" actions: - left_click: "open_stacker" - right_click: "sell_inventory" + 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: diff --git a/core/src/main/resources/gui_layouts/default/main_gui.yml b/core/src/main/resources/gui_layouts/default/main_gui.yml index bb38642c..e5cb296e 100644 --- a/core/src/main/resources/gui_layouts/default/main_gui.yml +++ b/core/src/main/resources/gui_layouts/default/main_gui.yml @@ -1,7 +1,7 @@ # 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 MOB_HEAD material, the texture will be automatically set based on the spawner entity type +# Note: For PLAYER_HEAD material, the texture will be automatically set based on the spawner entity type gui_layout_version: "1.0.0" @@ -16,18 +16,27 @@ buttons: left_click: "open_storage" right_click: "open_storage" - # Spawner information display - # Material will be MOB_HEAD with texture based on spawner entity type - # Players can change material if they want different button appearance - # Left click: Opens stacker GUI - # Right click: Collects XP + sells items (if shop available) - spawner_info: + # 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: MOB_HEAD + material: PLAYER_HEAD enabled: true + condition: "shop_integration" actions: - left_click: "open_stacker" - right_click: "sell_inventory" + 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: