From 324a8973289079fd7d9609343c836fe2acef6664 Mon Sep 17 00:00:00 2001 From: "songoda-plugins-overview[bot]" <111250264+songoda-plugins-overview[bot]@users.noreply.github.com> Date: Tue, 1 Apr 2025 17:56:08 +0000 Subject: [PATCH 1/7] Updates contents of README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0526cf9e..7e810a20 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,11 @@ create a thread over on [our Discord server][Discord invite]. [Plugin page]: https://songoda.com/product/8 -[Plugin wiki]: https://songoda.notion.site/EpicHoppers-3901d0bf2c9a42c995882a9ca599bfbe +[Plugin wiki]: https://wiki.songoda.com/EpicHoppers-1130f108970281cab45ddd6df8d38534 [Discord invite]: https://discord.gg/7TXM8xr2Ng [Discord shield]: https://img.shields.io/discord/1214289374506917889?color=5865F2&label=Discord&logo=discord&logoColor=5865F2 -[Latest version shield]: https://img.shields.io/badge/dynamic/xml?style=flat&color=blue&logo=github&logoColor=white&label=Latest&url=https%3A%2F%2Fraw.githubusercontent.com%2Fcraftaro%2FEpicHoppers%2Fmaster%2Fpom.xml&query=%2F*%5Blocal-name()%3D'project'%5D%2F*%5Blocal-name()%3D'version'%5D +[Latest version shield]: https://img.shields.io/badge/dynamic/xml?style=flat&color=blue&logo=github&logoColor=white&label=Latest&url=https%3A%2F%2Fraw.githubusercontent.com%2FSongoda-Plugins%2FEpicHoppers%2Fmaster%2Fpom.xml&query=%2F*%5Blocal-name()%3D'project'%5D%2F*%5Blocal-name()%3D'version'%5D [bStats page]: https://bstats.org/plugin/bukkit/EpicHoppers/4185 [bStats shield]: https://img.shields.io/bstats/servers/4185?label=Servers From 30803183cca7417eba5c55540405504c6dc758c7 Mon Sep 17 00:00:00 2001 From: Francesco Date: Mon, 10 Nov 2025 17:10:00 +0100 Subject: [PATCH 2/7] Fix: Always emit InventoryPickupItemEvent in suction module The suction module was only emitting InventoryPickupItemEvent when the EMIT_INVENTORYPICKUPITEMEVENT setting was enabled. This prevented other plugins from cancelling item pickup for their custom items. This change makes the suction module always emit the event, allowing any plugin to cancel pickup by listening to InventoryPickupItemEvent, maintaining consistency with standard hopper behavior. This is important because suction is an aggressive collection mechanism that bypasses normal hopper behavior, and other plugins should have control over what items can be collected. Benefits: - Universal plugin compatibility - Plugins can block their custom items from being collected - Uses standard Bukkit event system - Backwards compatible with existing plugins --- .../hopper/levels/modules/ModuleSuction.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/EpicHoppers-Plugin/src/main/java/com/craftaro/epichoppers/hopper/levels/modules/ModuleSuction.java b/EpicHoppers-Plugin/src/main/java/com/craftaro/epichoppers/hopper/levels/modules/ModuleSuction.java index d6caf516..6a4102d1 100644 --- a/EpicHoppers-Plugin/src/main/java/com/craftaro/epichoppers/hopper/levels/modules/ModuleSuction.java +++ b/EpicHoppers-Plugin/src/main/java/com/craftaro/epichoppers/hopper/levels/modules/ModuleSuction.java @@ -75,11 +75,9 @@ public void run(Hopper hopper, StorageContainerCache.Cache hopperCache) { boolean filterEndpoint = hopper.getFilter().getEndPoint() != null; - Inventory hopperInventory = null; - if (Settings.EMIT_INVENTORYPICKUPITEMEVENT.getBoolean()) { - InventoryHolder inventoryHolder = (InventoryHolder) hopper.getBlock().getState(); - hopperInventory = Bukkit.createInventory(inventoryHolder, InventoryType.HOPPER); - } + // Always create inventory for the suction module to allow plugins to cancel the pickup event + InventoryHolder inventoryHolder = (InventoryHolder) hopper.getBlock().getState(); + Inventory hopperInventory = Bukkit.createInventory(inventoryHolder, InventoryType.HOPPER); for (Item item : itemsToSuck) { ItemStack itemStack = item.getItemStack(); @@ -122,13 +120,13 @@ public void run(Hopper hopper, StorageContainerCache.Cache hopperCache) { } } - if (Settings.EMIT_INVENTORYPICKUPITEMEVENT.getBoolean()) { - hopperInventory.setContents(hopperCache.cachedInventory); - InventoryPickupItemEvent pickupEvent = new InventoryPickupItemEvent(hopperInventory, item); - Bukkit.getPluginManager().callEvent(pickupEvent); - if (pickupEvent.isCancelled()) { - continue; - } + // Always emit the event for suction module to allow any plugin to prevent item pickup + // This is important because suction is an aggressive collection mechanism + hopperInventory.setContents(hopperCache.cachedInventory); + InventoryPickupItemEvent pickupEvent = new InventoryPickupItemEvent(hopperInventory, item); + Bukkit.getPluginManager().callEvent(pickupEvent); + if (pickupEvent.isCancelled()) { + continue; } // try to add the items to the hopper From 765b02735c7adcd8bdc1ea7506e87e59ba436a4d Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 12 Nov 2025 11:08:24 +0100 Subject: [PATCH 3/7] Fix: Use SongodaCore 3.9.8 release version and add release repository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed from SNAPSHOT version which was not available in repositories. Added minecraft-plugins-release repository to resolve dependencies. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- pom.xml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 99ce71ac..c79cba0e 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ https://songoda.com/product/epichoppers-8 - 3.9.8-SNAPSHOT + 3.9.8 8 1.8 @@ -45,6 +45,11 @@ https://repo.songoda.com/repository/minecraft-plugins/ + + songoda-minecraft-plugins-release + https://repo.songoda.com/repository/minecraft-plugins-release/ + + SpigotMC https://hub.spigotmc.org/nexus/content/repositories/snapshots/ From 98e8758a7f01ad3bf2ecb63a126da0f145e80c19 Mon Sep 17 00:00:00 2001 From: Francesco Date: Tue, 11 Nov 2025 18:02:34 +0100 Subject: [PATCH 4/7] Fix WildStacker/UltimateStacker/RoseStacker support in suction module The suction module wasn't properly handling items stacked by stacker plugins (WildStacker, UltimateStacker, RoseStacker). This caused large stacks to disappear or be counted incorrectly. Changes made: - Bypass PickupDelay check for stacker items since we handle them through their APIs - Read item amount BEFORE emitting InventoryPickupItemEvent (WildStacker modifies the stack during event processing at HIGHEST priority, which was causing incorrect counts) - Add protected item check to respect shop items and custom plugin items (checks for display name, lore, or PDC data) - Only bypass event cancellation for stacker items that aren't protected - this lets suction work with stacker plugins while still respecting protection from shop plugins like EzChestShopReborn - Fix premature loop termination by changing return to continue for shulker boxes, shop items, and blacklisted items - Add null checks in updateAmount() for WildStacker and RoseStacker APIs with fallback to vanilla handling This ensures stacked items (like 128 diamonds from WildStacker) get collected properly while still respecting protected items from other plugins. --- .../hopper/levels/modules/ModuleSuction.java | 97 ++++++++++++++++--- 1 file changed, 82 insertions(+), 15 deletions(-) diff --git a/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/hopper/levels/modules/ModuleSuction.java b/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/hopper/levels/modules/ModuleSuction.java index 8e89c831..fbb5c05e 100644 --- a/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/hopper/levels/modules/ModuleSuction.java +++ b/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/hopper/levels/modules/ModuleSuction.java @@ -15,6 +15,7 @@ import dev.rosewood.rosestacker.api.RoseStackerAPI; import dev.rosewood.rosestacker.stack.StackedItem; import org.bukkit.Bukkit; +import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.entity.EntityType; import org.bukkit.entity.Item; @@ -60,13 +61,34 @@ public void run(Hopper hopper, StorageContainerCache.Cache hopperCache) { return; } - Set itemsToSuck = hopper.getLocation().getWorld().getNearbyEntities(hopper.getLocation().add(0.5, 0.5, 0.5), radius, radius, radius) + Set itemsToSuck = hopper.getLocation().getWorld() + .getNearbyEntities(hopper.getLocation().add(0.5, 0.5, 0.5), radius, radius, radius) .stream() - .filter(entity -> entity.getType() == EntityType.DROPPED_ITEM - && !entity.isDead() - && entity.getTicksLived() >= ((Item) entity).getPickupDelay() - && entity.getLocation().getBlock().getType() != Material.HOPPER) + .filter(entity -> entity.getType() == EntityType.DROPPED_ITEM) .map(Item.class::cast) + .filter(entity -> { + if (entity.isDead() || entity.getLocation().getBlock().getType() == Material.HOPPER) { + return false; + } + + // For WildStacker items, bypass the PickupDelay check since we use their API + if (WILD_STACKER && WildStackerAPI.getStackedItem(entity) != null) { + return true; + } + + // For UltimateStacker items, bypass the PickupDelay check + if (ULTIMATE_STACKER && UltimateStackerApi.getStackedItemManager().isStackedItem(entity)) { + return true; + } + + // For RoseStacker items, bypass the PickupDelay check + if (ROSE_STACKER && RoseStackerAPI.getInstance().getStackedItem(entity) != null) { + return true; + } + + // For normal items, check PickupDelay + return entity.getTicksLived() >= entity.getPickupDelay(); + }) .collect(Collectors.toSet()); if (itemsToSuck.isEmpty()) { @@ -82,22 +104,28 @@ public void run(Hopper hopper, StorageContainerCache.Cache hopperCache) { for (Item item : itemsToSuck) { ItemStack itemStack = item.getItemStack(); - if (item.getPickupDelay() == 0) { + // Check if this is a stacker plugin item + boolean isStackerItem = (WILD_STACKER && WildStackerAPI.getStackedItem(item) != null) + || (ULTIMATE_STACKER && UltimateStackerApi.getStackedItemManager().isStackedItem(item)) + || (ROSE_STACKER && RoseStackerAPI.getInstance().getStackedItem(item) != null); + + // Skip items with PickupDelay 0 (unless they're stacker items, which we handle via API) + if (!isStackerItem && item.getPickupDelay() == 0) { item.setPickupDelay(25); continue; } if (itemStack.getType().name().contains("SHULKER_BOX")) { - return; + continue; } if (itemStack.hasItemMeta() && itemStack.getItemMeta().hasDisplayName() && itemStack.getItemMeta().getDisplayName().startsWith("***")) { - return; //Compatibility with Shop instance: https://www.spigotmc.org/resources/shop-a-simple-intuitive-shop-instance.9628/ + continue; //Compatibility with Shop instance: https://www.spigotmc.org/resources/shop-a-simple-intuitive-shop-instance.9628/ } if (BLACKLIST.contains(item.getUniqueId())) { - return; + continue; } // respect filter if no endpoint @@ -120,27 +148,52 @@ public void run(Hopper hopper, StorageContainerCache.Cache hopperCache) { } } + // IMPORTANT: Read the actual item amount BEFORE emitting the event! + // WildStacker (priority HIGHEST) modifies the item during the event, + // so we must capture the correct amount first + int toAdd = getActualItemAmount(item); + // Always emit the event for suction module to allow any plugin to prevent item pickup // This is important because suction is an aggressive collection mechanism hopperInventory.setContents(hopperCache.cachedInventory); InventoryPickupItemEvent pickupEvent = new InventoryPickupItemEvent(hopperInventory, item); Bukkit.getPluginManager().callEvent(pickupEvent); + if (pickupEvent.isCancelled()) { - continue; + // Check if this is a protected item (from shops or other plugins) first + // Protected items should always respect the cancellation to maintain compatibility + boolean isProtectedItem = itemStack.hasItemMeta() && ( + itemStack.getItemMeta().hasDisplayName() || + itemStack.getItemMeta().hasLore() || + !itemStack.getItemMeta().getPersistentDataContainer().isEmpty() + ); + + if (isProtectedItem) { + // This item is protected by another plugin - always respect that + continue; + } else if (!isStackerItem) { + // Normal item that's been cancelled - skip it + continue; + } + // If we get here, it's a stacker item that's not protected, so we bypass the cancellation + // This allows suction to work properly with stacker plugins } // try to add the items to the hopper - int toAdd, added = hopperCache.addAny(itemStack, toAdd = getActualItemAmount(item)); + int added = hopperCache.addAny(itemStack, toAdd); + if (added == 0) { return; } // items added ok! - if (added == toAdd) { + if (added >= toAdd) { + // All items were added, remove the entity item.remove(); } else { - // update the item's total - updateAmount(item, toAdd - added); + // Only some items were added, update the remaining amount + int remaining = toAdd - added; + updateAmount(item, remaining); // wait before trying to add again BLACKLIST.add(item.getUniqueId()); @@ -173,7 +226,21 @@ private void updateAmount(Item item, int amount) { if (ULTIMATE_STACKER) { UltimateStackerApi.getStackedItemManager().updateStack(item, amount); } else if (WILD_STACKER) { - WildStackerAPI.getStackedItem(item).setStackAmount(amount, true); + com.bgsoftware.wildstacker.api.objects.StackedItem stackedItem = WildStackerAPI.getStackedItem(item); + if (stackedItem != null) { + stackedItem.setStackAmount(amount, true); + } else { + // Fallback if item is not tracked by WildStacker + item.getItemStack().setAmount(Math.min(amount, item.getItemStack().getMaxStackSize())); + } + } else if (ROSE_STACKER) { + StackedItem stackedItem = RoseStackerAPI.getInstance().getStackedItem(item); + if (stackedItem != null) { + stackedItem.setStackSize(amount); + } else { + // Fallback if item is not tracked by RoseStacker + item.getItemStack().setAmount(Math.min(amount, item.getItemStack().getMaxStackSize())); + } } else { item.getItemStack().setAmount(Math.min(amount, item.getItemStack().getMaxStackSize())); } From 0100ffdaa894330f4b55142f92b26438f8094b06 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 12 Nov 2025 12:30:00 +0100 Subject: [PATCH 5/7] Add database table prefix configuration setting Added a new configuration option to support database table prefixes. This is useful when multiple servers need to share the same database but keep their data separate. Configuration key: Main.Database Table Prefix Default value: "" (empty string - no prefix) Example usage: Setting it to "survival_" creates tables like "survival_placed_hoppers" instead of "placed_hoppers" This allows server administrators to run multiple instances (survival, creative, etc.) on the same MySQL database without table name conflicts. --- .../main/java/com/songoda/epichoppers/settings/Settings.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java b/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java index fb9482c0..3d8c31ed 100644 --- a/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java +++ b/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java @@ -101,6 +101,11 @@ public class Settings { "However it is a high frequency event and may have an impact on your server performance which is why it is disabled by default.", "If you absolutely need this enable it but be aware of the potential performance impact."); + public static final ConfigSetting DATABASE_TABLE_PREFIX = new ConfigSetting(CONFIG, "Main.Database Table Prefix", "", + "Prefix for database tables. Useful when running multiple servers sharing the same database.", + "Example: 'survival_' would create tables like 'survival_placed_hoppers' instead of 'placed_hoppers'.", + "Leave empty for no prefix."); + public static final ConfigSetting ECO_ICON = new ConfigSetting(CONFIG, "Interfaces.Economy Icon", "SUNFLOWER"); public static final ConfigSetting XP_ICON = new ConfigSetting(CONFIG, "Interfaces.XP Icon", "EXPERIENCE_BOTTLE"); From b67be416cf5583a95b9e9aeae3d9f0495377b9a0 Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 12 Nov 2025 12:41:28 +0100 Subject: [PATCH 6/7] Add database table prefix configuration Added configuration option to support custom database table prefixes. This allows multiple servers to share the same database while keeping their hopper data separate. Configuration: - Key: Database.Table Prefix - Default: "" (empty - no prefix) - Example: "survival_" creates tables like "survival_placed_hoppers" The prefix is automatically applied to all database tables: - placed_hoppers - links - items - boosted_players Usage: Useful for running multiple server instances (survival, creative, etc.) on the same MySQL database without table name conflicts. --- .../main/java/com/songoda/epichoppers/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java b/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java index 3d8c31ed..bc22b554 100644 --- a/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java +++ b/EpicHoppers-Plugin/src/main/java/com/songoda/epichoppers/settings/Settings.java @@ -101,7 +101,7 @@ public class Settings { "However it is a high frequency event and may have an impact on your server performance which is why it is disabled by default.", "If you absolutely need this enable it but be aware of the potential performance impact."); - public static final ConfigSetting DATABASE_TABLE_PREFIX = new ConfigSetting(CONFIG, "Main.Database Table Prefix", "", + public static final ConfigSetting DATABASE_TABLE_PREFIX = new ConfigSetting(CONFIG, "Database.Table Prefix", "", "Prefix for database tables. Useful when running multiple servers sharing the same database.", "Example: 'survival_' would create tables like 'survival_placed_hoppers' instead of 'placed_hoppers'.", "Leave empty for no prefix."); From d4fa5a03a1f28b43c61627f0b96a5638f56333ab Mon Sep 17 00:00:00 2001 From: Francesco Date: Wed, 12 Nov 2025 19:07:32 +0100 Subject: [PATCH 7/7] Use SongodaCore 3.9.8-SNAPSHOT for table prefix support --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c79cba0e..b46b3a92 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ https://songoda.com/product/epichoppers-8 - 3.9.8 + 3.9.8-SNAPSHOT 8 1.8