diff --git a/README.md b/README.md
index 36cc92d..68aa47e 100644
--- a/README.md
+++ b/README.md
@@ -3,5 +3,15 @@ Eliminate complicated hopper setups with this little plugin. Filter items throug
HopperFilter has been designed from the ground up to be as performant as possible. Any pull requests improving performance are welcome.
+# Extra Feature
+- Regex Syntax support with "r:{regex}"
+
e.g. r:^(diamond|emerald|iron_ingot|gold_ingot|netherite_ingot|netherite_scrap|lapis_lazuli|quartz|amethyst_shard|copper_ingot)$
+
This filters for diamond, emeralds etc.
+
To write regex you can use [Regex101](https://regex101.com)
+- Config file /plugins/HopperFilter/config.yml
+- Edit move rate for all hopper types (via config)
+- Now move item from every slot in the inventory if it matches
+
## Credits
Thanks to LiveOverflow for the inspiration for this plugin in [this](https://youtu.be/Gi2PPBCEHuM?t=224) video.
+
Contributor: [Kiko](https://github.com/Kiko-Dev-Tech) (Extra Features)
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index af57489..c7f377a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,18 +6,35 @@
net.earthmc
HopperFilter
- 0.3.7
+ 0.4.3
jar
HopperFilter
- Name hoppers. Filter items.
+ Name hoppers. Filter items. Now with Regex Filter!
- 17
- UTF-8
-
+ 17
+ UTF-8
+
https://earthmc.net
-
+
+
+ jwkerr
+ Fruitloopins
+ https://github.com/jwkerr
+
+ creator
+
+
+
+ kiko
+ Kiko
+ https://github.com/Kiko-Dev-Tech
+
+ contributor(Regex Filer, rate config and optimization)
+
+
+
diff --git a/src/main/java/net/earthmc/hopperfilter/HopperFilter.java b/src/main/java/net/earthmc/hopperfilter/HopperFilter.java
index bd8c7fd..abb89c2 100644
--- a/src/main/java/net/earthmc/hopperfilter/HopperFilter.java
+++ b/src/main/java/net/earthmc/hopperfilter/HopperFilter.java
@@ -1,5 +1,6 @@
package net.earthmc.hopperfilter;
+import net.earthmc.hopperfilter.command.HopperFilterCommand;
import net.earthmc.hopperfilter.listener.HopperRenameListener;
import net.earthmc.hopperfilter.listener.InventoryActionListener;
import org.bukkit.event.Listener;
@@ -8,17 +9,34 @@
public final class HopperFilter extends JavaPlugin {
private static HopperFilter instance;
-
+ public int itemsPerTransfer = 1;
@Override
public void onEnable() {
instance = this;
registerListeners(
new HopperRenameListener(),
- new InventoryActionListener()
+ new InventoryActionListener(this)
);
+ saveDefaultConfig(); // Copies config.yml to plugin folder if not present
+ reloadConfig(); // Loads or reloads config from disk
+ loadSettings();
+
+ //add Reload Command for config
+ this.getCommand("hopperfilter").setExecutor(new HopperFilterCommand(this));
+ int itemsPerTransfer = getConfig().getInt("transfer.items-per-transfer", 1);
+ getLogger().info("Transfer rate set to: " + itemsPerTransfer + " items per 8 ticks.");
+ }
+
+ public void loadSettings() {
+ itemsPerTransfer = getConfig().getInt("transfer.items-per-transfer", 1);
}
+ public int getItemsPerTransfer() {
+ return itemsPerTransfer;
+ }
+
+
private void registerListeners(Listener... listeners) {
for (Listener listener : listeners) {
getServer().getPluginManager().registerEvents(listener, this);
diff --git a/src/main/java/net/earthmc/hopperfilter/command/HopperFilterCommand.java b/src/main/java/net/earthmc/hopperfilter/command/HopperFilterCommand.java
new file mode 100644
index 0000000..717cb54
--- /dev/null
+++ b/src/main/java/net/earthmc/hopperfilter/command/HopperFilterCommand.java
@@ -0,0 +1,34 @@
+package net.earthmc.hopperfilter.command;
+
+import net.earthmc.hopperfilter.HopperFilter;
+import org.bukkit.ChatColor;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandExecutor;
+import org.bukkit.command.CommandSender;
+
+public class HopperFilterCommand implements CommandExecutor {
+
+ private final HopperFilter plugin;
+
+ public HopperFilterCommand(HopperFilter plugin) {
+ this.plugin = plugin;
+ }
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
+ if (!sender.hasPermission("hopperfilter.reload")) {
+ sender.sendMessage(ChatColor.RED + "You don't have permission to do that.");
+ return true;
+ }
+
+ plugin.reloadConfig(); // Reload config.yml
+ plugin.loadSettings();
+ sender.sendMessage(ChatColor.GREEN + "HopperFilter config reloaded.");
+ return true;
+ }
+
+ sender.sendMessage(ChatColor.YELLOW + "Usage: /" + label + " reload");
+ return true;
+ }
+}
diff --git a/src/main/java/net/earthmc/hopperfilter/listener/InventoryActionListener.java b/src/main/java/net/earthmc/hopperfilter/listener/InventoryActionListener.java
index 3f19a5b..1c368e3 100644
--- a/src/main/java/net/earthmc/hopperfilter/listener/InventoryActionListener.java
+++ b/src/main/java/net/earthmc/hopperfilter/listener/InventoryActionListener.java
@@ -1,69 +1,181 @@
package net.earthmc.hopperfilter.listener;
+import net.earthmc.hopperfilter.HopperFilter;
import net.earthmc.hopperfilter.util.ContainerUtil;
import net.earthmc.hopperfilter.util.PatternUtil;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import org.apache.commons.lang3.tuple.Pair;
import org.bukkit.*;
-import org.bukkit.block.Block;
-import org.bukkit.block.BlockFace;
-import org.bukkit.block.Hopper;
+import org.bukkit.block.*;
+import org.bukkit.block.data.BlockData;
import org.bukkit.enchantments.Enchantment;
+import org.bukkit.entity.Player;
import org.bukkit.entity.minecart.HopperMinecart;
import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryMoveItemEvent;
import org.bukkit.event.inventory.InventoryPickupItemEvent;
import org.bukkit.event.inventory.InventoryType;
-import org.bukkit.inventory.Inventory;
-import org.bukkit.inventory.InventoryHolder;
-import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.*;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionType;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.PatternSyntaxException;
public class InventoryActionListener implements Listener {
- @EventHandler
+ private final HopperFilter plugin;
+
+ public InventoryActionListener(HopperFilter plugin) {
+ super();
+ this.plugin = plugin;
+ }
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryMoveItem(final InventoryMoveItemEvent event) {
+ InventoryHolder sourceHolder = event.getSource().getHolder(false);
+ InventoryHolder destHolder = event.getDestination().getHolder(false);
+
+ if (!isHopperOrMinecart(sourceHolder) && !isHopperOrMinecart(destHolder)) return;
+ //excluded crafter
+ if (sourceHolder instanceof Crafter || destHolder instanceof Crafter) return;
+
+ if (isFurnace(sourceHolder)) {
+ String hopperName = getFilterName(event.getDestination().getHolder(false));
+ if (hopperName != null && !canItemPassHopper(hopperName, event.getItem())) {
+ event.setCancelled(true); // deny pulling this item
+ }
+ return; // let vanilla handle transfer rate
+ }
+ event.setCancelled(true); // cancel default behavior
+ Bukkit.getScheduler().runTaskLater(plugin, () -> moveItem(event), 1L);
+ }
+
+ private String getFilterName(InventoryHolder holder) {
+ if (holder instanceof Hopper h && h.customName() != null)
+ return PatternUtil.serialiseComponent(h.customName());
+ if (holder instanceof HopperMinecart h && h.customName() != null)
+ return PatternUtil.serialiseComponent(h.customName());
+ return null;
+ }
+
+ private boolean isFurnace(InventoryHolder holder) {
+ return holder instanceof Furnace
+ || holder instanceof BlastFurnace
+ || holder instanceof Smoker;
+ }
+
+ private void moveItem(InventoryMoveItemEvent event) {
+ final Inventory source = event.getSource();
final Inventory destination = event.getDestination();
- if (!destination.getType().equals(InventoryType.HOPPER)) return;
- final ItemStack item = event.getItem();
- final InventoryHolder holder = destination.getHolder(false);
+ final InventoryHolder sourceHolder = source.getHolder(false);
+ final InventoryHolder destHolder = destination.getHolder(false);
- String hopperName;
- if (holder instanceof final Hopper hopper) {
- hopperName = PatternUtil.serialiseComponent(hopper.customName());
- } else if (holder instanceof final HopperMinecart hopperMinecart) {
- hopperName = PatternUtil.serialiseComponent(hopperMinecart.customName());
+ boolean isDefaultHopper = false;
+ String filterName = null;
+
+ if (isHopperWithFilter(sourceHolder)) {
+ filterName = getCustomName(sourceHolder);
+ } else if (isHopperWithFilter(destHolder)) {
+ filterName = getCustomName(destHolder);
} else {
- return;
+ isDefaultHopper = true;
}
- if (!canItemPassHopper(hopperName, item)) {
- event.setCancelled(true);
- return;
+ for (int i = 0; i < source.getSize(); i++) {
+ ItemStack stack = source.getItem(i);
+ if (stack == null || stack.getType().isAir()) continue;
+
+ if (!canItemPassHopper(filterName, stack) && !isDefaultHopper) continue;
+
+ int maxToMove = Math.min(plugin.getItemsPerTransfer(), stack.getAmount());
+ ItemStack toMove = stack.clone();
+ toMove.setAmount(maxToMove);
+
+ int moved = 0;
+
+ if (destination instanceof FurnaceInventory furnaceInv) {
+ Block sourceBlock = getBlockFromHolder(sourceHolder);
+ Block destBlock = getBlockFromHolder(destHolder);
+ if (sourceBlock == null || destBlock == null) return;
+
+ BlockFace direction = destBlock.getFace(sourceBlock);
+ if (direction == null) direction = BlockFace.UP; // fallback: assume from below
+
+ int slot = switch (direction) {
+ case UP -> 0; // from above = input
+ case DOWN -> 2; // from below = output (disallowed)
+ default -> 1; // from side = fuel
+ };
+
+ if (slot == 2) return;
+ if (slot == 0 && !hasFurnaceRecipe(toMove)) return;
+ if (slot == 1 && !toMove.getType().isFuel()) return;
+
+ ItemStack current = furnaceInv.getItem(slot);
+ if (current == null || current.getType().isAir()) {
+ furnaceInv.setItem(slot, toMove);
+ moved = toMove.getAmount();
+ } else if (current.isSimilar(toMove) && current.getAmount() < current.getMaxStackSize()) {
+ int insertable = Math.min(toMove.getAmount(), current.getMaxStackSize() - current.getAmount());
+ current.setAmount(current.getAmount() + insertable);
+ furnaceInv.setItem(slot, current);
+ moved = insertable;
+ }
+
+ } else {
+ Map leftovers = destination.addItem(toMove);
+ moved = toMove.getAmount() - leftovers.values().stream().mapToInt(ItemStack::getAmount).sum();
+ }
+
+ if (moved > 0) {
+ stack.setAmount(stack.getAmount() - moved);
+ source.setItem(i, stack.getAmount() > 0 ? stack : null);
+ }
+
+ break;
}
+ }
- // Checks below only matter for hopper blocks
- if (!(holder instanceof Hopper hopper)) return;
- // If there was a filter on this hopper we don't need to check for a more suitable hopper since this one is specifically filtering for this item
- if (hopperName != null) return;
- final Inventory source = event.getSource();
- // If the item can pass, but there is a more suitable hopper with a filter we do this
- if (shouldCancelDueToMoreSuitableHopper(source, hopper, item)) event.setCancelled(true);
+ private boolean isHopperOrMinecart(InventoryHolder holder) {
+ return holder instanceof Hopper
+ || holder instanceof HopperMinecart
+ || holder instanceof org.bukkit.entity.minecart.StorageMinecart;
+ }
+
+ private Block getBlockFromHolder(InventoryHolder holder) {
+ if (holder instanceof BlockState state) return state.getBlock();
+ return null;
+ }
+
+ private boolean hasFurnaceRecipe(ItemStack item) {
+ return Bukkit.getRecipesFor(item).stream()
+ .anyMatch(r -> r instanceof FurnaceRecipe);
+ }
+
+ private boolean isHopperWithFilter(InventoryHolder holder) {
+ if (holder instanceof Hopper h) return h.customName() != null;
+ if (holder instanceof HopperMinecart h) return h.customName() != null;
+ return false;
+ }
+
+ private String getCustomName(InventoryHolder holder) {
+ if (holder instanceof Hopper h) return PatternUtil.serialiseComponent(h.customName());
+ if (holder instanceof HopperMinecart h) return PatternUtil.serialiseComponent(h.customName());
+ return null;
}
- @EventHandler
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onInventoryPickupItem(final InventoryPickupItemEvent event) {
final Inventory inventory = event.getInventory();
if (!inventory.getType().equals(InventoryType.HOPPER)) return;
@@ -125,6 +237,9 @@ private boolean shouldCancelDueToMoreSuitableHopper(final Inventory source, fina
private boolean canItemPassHopper(final String hopperName, final ItemStack item) {
if (hopperName == null) return true;
+ if (hopperName.startsWith("r:")) {
+ return filterByRegex(hopperName, item);
+ }
nextCondition: for (final String condition : hopperName.split(",")) {
nextAnd: for (final String andString : condition.split("&")) {
for (final String orString : andString.split("\\|")) {
@@ -139,36 +254,49 @@ private boolean canItemPassHopper(final String hopperName, final ItemStack item)
return false;
}
- private boolean canItemPassPattern(final String pattern, final ItemStack item) {
+ /** Add Regex Support by Kiko (https://github.com/Kiko-Dev-Tech)
+ *
+ * @param pattern
+ * @param item
+ * @return
+ */
+ private boolean filterByRegex(final String pattern, final ItemStack item) {
final String itemName = item.getType().getKey().getKey();
+ if (pattern.isEmpty() || pattern.equals(itemName)) return true;
+ final String regex = pattern.substring(2);
+ try {
+ return itemName.matches(regex);
+ } catch (PatternSyntaxException e) {
+ Bukkit.getLogger().warning("Invalid regex pattern in hopper filter: " + regex);
+ return false;
+ }
+ }
+ private boolean canItemPassPattern(final String pattern, final ItemStack item) {
+ final String itemName = item.getType().getKey().getKey();
if (pattern.isEmpty() || pattern.equals(itemName)) return true;
- final char prefix = pattern.charAt(0); // The character at the start of the pattern
- final String string = pattern.substring(1); // Anything after the prefix
+ final char prefix = pattern.charAt(0);
+ final String string = pattern.substring(1);
return switch (prefix) {
- case '!' -> !canItemPassPattern(string, item); // NOT operator
- case '*' -> itemName.contains(string); // Contains specified pattern
- case '^' -> itemName.startsWith(string); // Starts with specified pattern
- case '$' -> itemName.endsWith(string); // Ends with specified pattern
- case '#' -> { // Item has specified tag
+ case '!' -> !canItemPassPattern(string, item);
+ case '*' -> itemName.contains(string);
+ case '^' -> itemName.startsWith(string);
+ case '$' -> itemName.endsWith(string);
+ case '#' -> {
final NamespacedKey key = NamespacedKey.fromString(string);
if (key == null) yield false;
-
Tag tag = Bukkit.getTag(Tag.REGISTRY_BLOCKS, key, Material.class);
if (tag == null) tag = Bukkit.getTag(Tag.REGISTRY_ITEMS, key, Material.class);
-
yield tag != null && tag.isTagged(item.getType());
}
- case '~' -> doesItemHaveSpecifiedPotionEffect(item, string); // Item has specified potion effect
- case '+' -> doesItemHaveSpecifiedEnchantment(item, string); // Item has specified enchantment
- case '=' -> { // Item has specified name (including renames)
+ case '~' -> doesItemHaveSpecifiedPotionEffect(item, string);
+ case '+' -> doesItemHaveSpecifiedEnchantment(item, string);
+ case '=' -> {
String displayName = PlainTextComponentSerializer.plainText().serialize(item.displayName())
.toLowerCase()
.replaceAll(" ", "_");
-
displayName = displayName.substring(1, displayName.length() - 1);
-
yield displayName.equals(string);
}
default -> false;
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
new file mode 100644
index 0000000..d0745ec
--- /dev/null
+++ b/src/main/resources/config.yml
@@ -0,0 +1,3 @@
+
+transfer:
+ items-per-transfer: 1 #how many items are moved every 8 ticks
\ No newline at end of file
diff --git a/src/main/resources/paper.yml b/src/main/resources/paper.yml
new file mode 100644
index 0000000..ca08ee2
--- /dev/null
+++ b/src/main/resources/paper.yml
@@ -0,0 +1,4 @@
+hopper:
+ ignore_hopper_move_events: false # must be false
+ cooldown: 8
+ tick-inactive: true
\ No newline at end of file
diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml
index eef23c8..5f19891 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -2,7 +2,16 @@ name: HopperFilter
version: "${project.version}"
main: net.earthmc.hopperfilter.HopperFilter
api-version: "1.20"
-authors: [Fruitloopins]
-description: Name hoppers. Filter items.
+authors: [Fruitloopins, Kiko]
+description: Name hoppers. Filter items. Now with Regex.
website: https://earthmc.net
folia-supported: true
+commands:
+ hopperfilter:
+ description: Reload the HopperFilter plugin
+ usage: / reload
+ permission: hopperfilter.reload
+permissions:
+ hopperfilter.reload:
+ description: Allows reloading the plugin
+ default: op
\ No newline at end of file