Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -60,13 +61,34 @@ public void run(Hopper hopper, StorageContainerCache.Cache hopperCache) {
return;
}

Set<Item> itemsToSuck = hopper.getLocation().getWorld().getNearbyEntities(hopper.getLocation().add(0.5, 0.5, 0.5), radius, radius, radius)
Set<Item> 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()) {
Expand All @@ -75,31 +97,35 @@ 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();

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
Expand All @@ -122,27 +148,52 @@ 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()) {
// 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()) {
// 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());
Expand Down Expand Up @@ -175,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()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "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");

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@
<url>https://repo.songoda.com/repository/minecraft-plugins/</url>
</repository>

<repository>
<id>songoda-minecraft-plugins-release</id>
<url>https://repo.songoda.com/repository/minecraft-plugins-release/</url>
</repository>

<repository>
<id>SpigotMC</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
Expand Down