فهرست منبع

Custom Filtering Rules

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel 5 سال پیش
والد
کامیت
dfaff67c77
25فایلهای تغییر یافته به همراه1457 افزوده شده و 28 حذف شده
  1. 1 1
      gradle.properties
  2. 3 0
      src/main/java/me/shedaniel/rei/api/ConfigObject.java
  3. 3 0
      src/main/java/me/shedaniel/rei/api/EntryRegistry.java
  4. 194 0
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringAddRuleScreen.java
  5. 12 4
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringEntry.java
  6. 239 0
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringRuleOptionsScreen.java
  7. 246 0
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringRulesScreen.java
  8. 19 5
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringScreen.java
  9. 1 3
      src/main/java/me/shedaniel/rei/gui/config/entry/NoFilteringEntry.java
  10. 18 1
      src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java
  11. 9 2
      src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java
  12. 12 2
      src/main/java/me/shedaniel/rei/impl/EntryRegistryImpl.java
  13. 9 2
      src/main/java/me/shedaniel/rei/impl/SearchArgument.java
  14. 40 0
      src/main/java/me/shedaniel/rei/impl/filtering/AbstractFilteringRule.java
  15. 52 0
      src/main/java/me/shedaniel/rei/impl/filtering/FilteringContext.java
  16. 63 0
      src/main/java/me/shedaniel/rei/impl/filtering/FilteringContextImpl.java
  17. 34 0
      src/main/java/me/shedaniel/rei/impl/filtering/FilteringContextType.java
  18. 67 0
      src/main/java/me/shedaniel/rei/impl/filtering/FilteringResult.java
  19. 52 0
      src/main/java/me/shedaniel/rei/impl/filtering/FilteringResultImpl.java
  20. 92 0
      src/main/java/me/shedaniel/rei/impl/filtering/FilteringRule.java
  21. 92 0
      src/main/java/me/shedaniel/rei/impl/filtering/rules/ManualFilteringRule.java
  22. 172 0
      src/main/java/me/shedaniel/rei/impl/filtering/rules/SearchFilteringRule.java
  23. 1 1
      src/main/java/me/shedaniel/rei/impl/search/Argument.java
  24. 15 7
      src/main/java/me/shedaniel/rei/impl/search/ArgumentsRegistry.java
  25. 11 0
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json

+ 1 - 1
gradle.properties

@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx3G
-mod_version=4.7.1
+mod_version=4.8.0
 supported_version=1.16.x
 minecraft_version=1.16.1
 yarn_version=1.16.1+build.4+legacy.20w09a+build.8

+ 3 - 0
src/main/java/me/shedaniel/rei/api/ConfigObject.java

@@ -26,6 +26,7 @@ package me.shedaniel.rei.api;
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.rei.gui.config.*;
 import me.shedaniel.rei.impl.ConfigManagerImpl;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import org.jetbrains.annotations.ApiStatus;
@@ -150,6 +151,8 @@ public interface ConfigObject {
     
     List<EntryStack> getFilteredStacks();
     
+    List<FilteringRule<?>> getFilteringRules();
+    
     @ApiStatus.Experimental
     boolean shouldAsyncSearch();
     

+ 3 - 0
src/main/java/me/shedaniel/rei/api/EntryRegistry.java

@@ -25,6 +25,8 @@ package me.shedaniel.rei.api;
 
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.utils.CollectionUtils;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
 import org.jetbrains.annotations.ApiStatus;
@@ -33,6 +35,7 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
+@Environment(EnvType.CLIENT)
 public interface EntryRegistry {
     
     /**

+ 194 - 0
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringAddRuleScreen.java

@@ -0,0 +1,194 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config.entry;
+
+import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
+import me.shedaniel.rei.impl.filtering.rules.ManualFilteringRule;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.LiteralText;
+import net.minecraft.text.StringRenderable;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+import net.minecraft.util.Identifier;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+public class FilteringAddRuleScreen extends Screen {
+    private final FilteringEntry entry;
+    private RulesList rulesList;
+    Screen parent;
+    
+    public FilteringAddRuleScreen(FilteringEntry entry) {
+        super(new TranslatableText("config.roughlyenoughitems.filteringRulesScreen.new"));
+        this.entry = entry;
+    }
+    
+    @Override
+    protected void init() {
+        super.init();
+        {
+            Text backText = new LiteralText("↩ ").append(new TranslatableText("gui.back"));
+            addButton(new ButtonWidget(4, 4, MinecraftClient.getInstance().textRenderer.getWidth(backText) + 10, 20, backText, button -> {
+                client.openScreen(parent);
+                this.parent = null;
+            }));
+        }
+        rulesList = addChild(new RulesList(client, width, height, 30, height, BACKGROUND_TEXTURE));
+        for (FilteringRule<?> rule : FilteringRule.REGISTRY) {
+            if (!(rule instanceof ManualFilteringRule))
+                rulesList.addItem(new DefaultRuleEntry(parent, entry, rule.createNew(), null));
+        }
+        rulesList.selectItem(rulesList.children().get(0));
+    }
+    
+    @Override
+    public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+        this.rulesList.render(matrices, mouseX, mouseY, delta);
+        super.render(matrices, mouseX, mouseY, delta);
+        this.textRenderer.drawWithShadow(matrices, this.title, this.width / 2.0F - this.textRenderer.getWidth(this.title) / 2.0F, 12.0F, -1);
+    }
+    
+    public static class RulesList extends DynamicElementListWidget<RuleEntry> {
+        private boolean inFocus;
+        
+        public RulesList(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+            super(client, width, height, top, bottom, backgroundLocation);
+        }
+        
+        @Override
+        public boolean changeFocus(boolean lookForwards) {
+            if (!this.inFocus && this.getItemCount() == 0) {
+                return false;
+            } else {
+                this.inFocus = !this.inFocus;
+                if (this.inFocus && this.getSelectedItem() == null && this.getItemCount() > 0) {
+                    this.moveSelection(1);
+                } else if (this.inFocus && this.getSelectedItem() != null) {
+                    this.getSelectedItem();
+                }
+                
+                return this.inFocus;
+            }
+        }
+        
+        @Override
+        protected boolean isSelected(int index) {
+            return Objects.equals(this.getSelectedItem(), this.children().get(index));
+        }
+        
+        @Override
+        protected int addItem(RuleEntry item) {
+            return super.addItem(item);
+        }
+        
+        @Override
+        public int getItemWidth() {
+            return width - 40;
+        }
+        
+        @Override
+        protected int getScrollbarPosition() {
+            return width - 14;
+        }
+    }
+    
+    public static abstract class RuleEntry extends DynamicElementListWidget.ElementEntry<RuleEntry> {
+        private final FilteringRule<?> rule;
+        
+        public RuleEntry(FilteringRule<?> rule) {
+            this.rule = rule;
+        }
+        
+        public FilteringRule<?> getRule() {
+            return rule;
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 26;
+        }
+        
+        @Override
+        public boolean changeFocus(boolean lookForwards) {
+            return false;
+        }
+    }
+    
+    public static class DefaultRuleEntry extends RuleEntry {
+        private final ButtonWidget addButton;
+        private final BiFunction<FilteringEntry, Screen, Screen> screenFunction;
+        
+        public DefaultRuleEntry(Screen parent, FilteringEntry entry, FilteringRule<?> rule, BiFunction<FilteringEntry, Screen, Screen> screenFunction) {
+            super(rule);
+            this.screenFunction = (screenFunction == null ? rule.createEntryScreen().orElse(null) : screenFunction);
+            addButton = new ButtonWidget(0, 0, 20, 20, Text.method_30163(" + "), button -> {
+                entry.edited = true;
+                MinecraftClient.getInstance().openScreen(this.screenFunction.apply(entry, parent));
+                entry.rules.add(0, rule);
+            });
+            addButton.active = this.screenFunction != null;
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+            MinecraftClient client = MinecraftClient.getInstance();
+            {
+                Text title = getRule().getTitle();
+                int i = client.textRenderer.getWidth(title);
+                if (i > entryWidth - 28) {
+                    StringRenderable titleTrimmed = StringRenderable.concat(client.textRenderer.trimToWidth(title, entryWidth - 28 - client.textRenderer.getStringWidth("...")), StringRenderable.plain("..."));
+                    client.textRenderer.drawWithShadow(matrices, titleTrimmed, x + 2, y + 1, 16777215);
+                } else {
+                    client.textRenderer.drawWithShadow(matrices, title, x + 2, y + 1, 16777215);
+                }
+            }
+            {
+                Text subtitle = getRule().getSubtitle();
+                int i = client.textRenderer.getWidth(subtitle);
+                if (i > entryWidth - 28) {
+                    StringRenderable subtitleTrimmed = StringRenderable.concat(client.textRenderer.trimToWidth(subtitle, entryWidth - 28 - client.textRenderer.getStringWidth("...")), StringRenderable.plain("..."));
+                    client.textRenderer.drawWithShadow(matrices, subtitleTrimmed, x + 2, y + 12, 8421504);
+                } else {
+                    client.textRenderer.drawWithShadow(matrices, subtitle, x + 2, y + 12, 8421504);
+                }
+            }
+            addButton.x = x + entryWidth - 25;
+            addButton.y = y + 1;
+            addButton.render(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(addButton);
+        }
+    }
+}

+ 12 - 4
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringEntry.java

@@ -24,8 +24,10 @@
 package me.shedaniel.rei.gui.config.entry;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import me.shedaniel.clothconfig2.api.AbstractConfigListEntry;
 import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.widget.AbstractButtonWidget;
@@ -44,22 +46,27 @@ import java.util.function.Consumer;
 public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
     private int width;
     Consumer<List<EntryStack>> saveConsumer;
+    Consumer<List<FilteringRule<?>>> rulesSaveConsumer;
     List<EntryStack> defaultValue;
     List<EntryStack> configFiltered;
+    List<FilteringRule<?>> rules;
     boolean edited = false;
-    private final FilteringScreen filteringScreen = new FilteringScreen(this);
+    final FilteringScreen filteringScreen = new FilteringScreen(this);
+    final FilteringRulesScreen filteringRulesScreen = new FilteringRulesScreen(this);
     private final AbstractButtonWidget buttonWidget = new ButtonWidget(0, 0, 0, 20, new TranslatableText("config.roughlyenoughitems.filteringScreen"), button -> {
-        filteringScreen.parent = MinecraftClient.getInstance().currentScreen;
-        MinecraftClient.getInstance().openScreen(filteringScreen);
+        filteringRulesScreen.parent = MinecraftClient.getInstance().currentScreen;
+        MinecraftClient.getInstance().openScreen(filteringRulesScreen);
     });
     private final List<Element> children = ImmutableList.of(buttonWidget);
     
-    public FilteringEntry(int width, List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
+    public FilteringEntry(int width, List<EntryStack> configFiltered, List<FilteringRule<?>> rules, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer, Consumer<List<FilteringRule<?>>> rulesSaveConsumer) {
         super(NarratorManager.EMPTY, false);
         this.width = width;
         this.configFiltered = configFiltered;
+        this.rules = Lists.newArrayList(rules);
         this.defaultValue = defaultValue;
         this.saveConsumer = saveConsumer;
+        this.rulesSaveConsumer = rulesSaveConsumer;
     }
     
     @Override
@@ -75,6 +82,7 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
     @Override
     public void save() {
         saveConsumer.accept(getValue());
+        rulesSaveConsumer.accept(rules);
         this.edited = false;
     }
     

+ 239 - 0
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringRuleOptionsScreen.java

@@ -0,0 +1,239 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config.entry;
+
+import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.text.StringRenderable;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+import net.minecraft.util.Identifier;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public abstract class FilteringRuleOptionsScreen<T extends FilteringRule<?>> extends Screen {
+    private final FilteringEntry entry;
+    private RulesList rulesList;
+    Screen parent;
+    public T rule;
+    
+    public FilteringRuleOptionsScreen(FilteringEntry entry, T rule, Screen screen) {
+        super(new TranslatableText("config.roughlyenoughitems.filteringRulesScreen"));
+        this.entry = entry;
+        this.rule = rule;
+        this.parent = screen;
+    }
+    
+    @Override
+    protected void init() {
+        super.init();
+        if (rulesList != null) save();
+        {
+            Text doneText = new TranslatableText("gui.done");
+            int width = MinecraftClient.getInstance().textRenderer.getWidth(doneText);
+            addButton(new ButtonWidget(this.width - 4 - width - 10, 4, width + 10, 20, doneText, button -> {
+                save();
+                client.openScreen(parent);
+            }));
+        }
+        rulesList = addChild(new RulesList(client, width, height, 30, height, BACKGROUND_TEXTURE));
+        addEntries(ruleEntry -> rulesList.addItem(ruleEntry));
+    }
+    
+    public abstract void addEntries(Consumer<RuleEntry> entryConsumer);
+    
+    public abstract void save();
+    
+    public void addText(Consumer<RuleEntry> entryConsumer, StringRenderable text) {
+        for (StringRenderable s : textRenderer.wrapStringToWidthAsList(text, width - 80)) {
+            entryConsumer.accept(new TextRuleEntry(rule, s));
+        }
+    }
+    
+    public void addEmpty(Consumer<RuleEntry> entryConsumer, int height) {
+        entryConsumer.accept(new EmptyRuleEntry(rule, height));
+    }
+    
+    @Override
+    public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+        this.rulesList.render(matrices, mouseX, mouseY, delta);
+        super.render(matrices, mouseX, mouseY, delta);
+        this.textRenderer.drawWithShadow(matrices, this.title, this.width / 2.0F - this.textRenderer.getWidth(this.title) / 2.0F, 12.0F, -1);
+    }
+    
+    public static class RulesList extends DynamicElementListWidget<RuleEntry> {
+        public RulesList(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+            super(client, width, height, top, bottom, backgroundLocation);
+        }
+        
+        @Override
+        protected int addItem(RuleEntry item) {
+            return super.addItem(item);
+        }
+        
+        @Override
+        public int getItemWidth() {
+            return width - 40;
+        }
+        
+        @Override
+        protected int getScrollbarPosition() {
+            return width - 14;
+        }
+    }
+    
+    public static abstract class RuleEntry extends DynamicElementListWidget.ElementEntry<RuleEntry> {
+        private final FilteringRule<?> rule;
+        
+        public RuleEntry(FilteringRule<?> rule) {
+            this.rule = rule;
+        }
+        
+        public FilteringRule<?> getRule() {
+            return rule;
+        }
+    }
+    
+    public static class TextRuleEntry extends RuleEntry {
+        private final StringRenderable text;
+        
+        public TextRuleEntry(FilteringRule<?> rule, StringRenderable text) {
+            super(rule);
+            this.text = text;
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+            MinecraftClient.getInstance().textRenderer.drawWithShadow(matrices, text, x + 5, y, -1);
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 12;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+    }
+    
+    public static class EmptyRuleEntry extends RuleEntry {
+        private final int height;
+        
+        public EmptyRuleEntry(FilteringRule<?> rule, int height) {
+            super(rule);
+            this.height = height;
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return height;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+    }
+    
+    public static class TextFieldRuleEntry extends RuleEntry {
+        private final TextFieldWidget widget;
+        
+        public TextFieldRuleEntry(int width, FilteringRule<?> rule, Consumer<TextFieldWidget> widgetConsumer) {
+            super(rule);
+            this.widget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, width, 18, Text.method_30163(""));
+            widgetConsumer.accept(widget);
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+            widget.x = x + 2;
+            widget.y = y + 2;
+            widget.render(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 20;
+        }
+        
+        public TextFieldWidget getWidget() {
+            return widget;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(widget);
+        }
+    }
+    
+    public static class BooleanRuleEntry extends RuleEntry {
+        private boolean b;
+        private final ButtonWidget widget;
+        
+        public BooleanRuleEntry(int width, boolean b, FilteringRule<?> rule, Function<Boolean, Text> textFunction) {
+            super(rule);
+            this.b = b;
+            this.widget = new ButtonWidget(0, 0, 100, 20, textFunction.apply(b), button -> {
+                this.b = !this.b;
+                button.setMessage(textFunction.apply(this.b));
+            });
+        }
+        
+        public boolean getBoolean() {
+            return b;
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+            widget.x = x + 2;
+            widget.y = y;
+            widget.render(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 20;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(widget);
+        }
+    }
+}

+ 246 - 0
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringRulesScreen.java

@@ -0,0 +1,246 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.gui.config.entry;
+
+import me.shedaniel.clothconfig2.gui.widget.DynamicElementListWidget;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
+import me.shedaniel.rei.impl.filtering.rules.ManualFilteringRule;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.LiteralText;
+import net.minecraft.text.StringRenderable;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+import net.minecraft.util.Identifier;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiFunction;
+
+import static me.shedaniel.rei.gui.RecipeViewingScreen.CHEST_GUI_TEXTURE;
+
+public class FilteringRulesScreen extends Screen {
+    private final FilteringEntry entry;
+    private RulesList rulesList;
+    Screen parent;
+    
+    public FilteringRulesScreen(FilteringEntry entry) {
+        super(new TranslatableText("config.roughlyenoughitems.filteringRulesScreen"));
+        this.entry = entry;
+    }
+    
+    @Override
+    protected void init() {
+        super.init();
+        {
+            Text backText = new LiteralText("↩ ").append(new TranslatableText("gui.back"));
+            addButton(new ButtonWidget(4, 4, MinecraftClient.getInstance().textRenderer.getWidth(backText) + 10, 20, backText, button -> {
+                client.openScreen(parent);
+                this.parent = null;
+            }));
+        }
+        {
+            Text addText = new LiteralText(" + ");
+            addButton(new ButtonWidget(width - 4 - 20, 4, 20, 20, addText, button -> {
+                FilteringAddRuleScreen screen = new FilteringAddRuleScreen(entry);
+                screen.parent = this;
+                client.openScreen(screen);
+            }));
+        }
+        rulesList = addChild(new RulesList(client, width, height, 30, height, BACKGROUND_TEXTURE));
+        for (int i = entry.rules.size() - 1; i >= 0; i--) {
+            FilteringRule<?> rule = entry.rules.get(i);
+            if (rule instanceof ManualFilteringRule)
+                rulesList.addItem(new DefaultRuleEntry(rule, entry, (entry, screen) -> {
+                    entry.filteringScreen.parent = screen;
+                    return entry.filteringScreen;
+                }));
+            else rulesList.addItem(new DefaultRuleEntry(rule, entry, null));
+        }
+        rulesList.selectItem(rulesList.children().get(0));
+    }
+    
+    @Override
+    public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
+        this.rulesList.render(matrices, mouseX, mouseY, delta);
+        super.render(matrices, mouseX, mouseY, delta);
+        this.textRenderer.drawWithShadow(matrices, this.title, this.width / 2.0F - this.textRenderer.getWidth(this.title) / 2.0F, 12.0F, -1);
+    }
+    
+    public static class RulesList extends DynamicElementListWidget<RuleEntry> {
+        private boolean inFocus;
+        
+        public RulesList(MinecraftClient client, int width, int height, int top, int bottom, Identifier backgroundLocation) {
+            super(client, width, height, top, bottom, backgroundLocation);
+        }
+        
+        @Override
+        public boolean changeFocus(boolean lookForwards) {
+            if (!this.inFocus && this.getItemCount() == 0) {
+                return false;
+            } else {
+                this.inFocus = !this.inFocus;
+                if (this.inFocus && this.getSelectedItem() == null && this.getItemCount() > 0) {
+                    this.moveSelection(1);
+                } else if (this.inFocus && this.getSelectedItem() != null) {
+                    this.getSelectedItem();
+                }
+                
+                return this.inFocus;
+            }
+        }
+        
+        @Override
+        protected boolean isSelected(int index) {
+            return Objects.equals(this.getSelectedItem(), this.children().get(index));
+        }
+        
+        @Override
+        protected int addItem(RuleEntry item) {
+            return super.addItem(item);
+        }
+        
+        @Override
+        public boolean mouseClicked(double double_1, double double_2, int int_1) {
+            if (super.mouseClicked(double_1, double_2, int_1))
+                return true;
+            RuleEntry item = getItemAtPosition(double_1, double_2);
+            if (item != null) {
+                client.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                selectItem(item);
+                this.setFocused(item);
+                this.setDragging(true);
+                return true;
+            }
+            return false;
+        }
+        
+        @Override
+        public int getItemWidth() {
+            return width - 40;
+        }
+        
+        @Override
+        protected int getScrollbarPosition() {
+            return width - 14;
+        }
+    }
+    
+    public static abstract class RuleEntry extends DynamicElementListWidget.ElementEntry<RuleEntry> {
+        private final FilteringRule<?> rule;
+        
+        public RuleEntry(FilteringRule<?> rule) {
+            this.rule = rule;
+        }
+        
+        public FilteringRule<?> getRule() {
+            return rule;
+        }
+        
+        @Override
+        public int getItemHeight() {
+            return 26;
+        }
+        
+        @Override
+        public boolean changeFocus(boolean lookForwards) {
+            return false;
+        }
+    }
+    
+    public static class DefaultRuleEntry extends RuleEntry {
+        private final ButtonWidget configureButton;
+        private final ButtonWidget deleteButton;
+        private final BiFunction<FilteringEntry, Screen, Screen> screenFunction;
+        
+        public DefaultRuleEntry(FilteringRule<?> rule, FilteringEntry entry, BiFunction<FilteringEntry, Screen, Screen> screenFunction) {
+            super(rule);
+            this.screenFunction = (screenFunction == null ? rule.createEntryScreen().orElse(null) : screenFunction);
+            configureButton = new ButtonWidget(0, 0, 20, 20, Text.method_30163(null), button -> {
+                entry.edited = true;
+                MinecraftClient.getInstance().openScreen(this.screenFunction.apply(entry, MinecraftClient.getInstance().currentScreen));
+            }) {
+                @Override
+                protected void renderBg(MatrixStack matrices, MinecraftClient client, int mouseX, int mouseY) {
+                    super.renderBg(matrices, client, mouseX, mouseY);
+                    MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+                    drawTexture(matrices, x + 3, y + 3, 0, 0, 14, 14);
+                }
+            };
+            {
+                Text deleteText = new TranslatableText("config.roughlyenoughitems.filteringRulesScreen.delete");
+                deleteButton = new ButtonWidget(0, 0, MinecraftClient.getInstance().textRenderer.getWidth(deleteText) + 10, 20, deleteText, button -> {
+                    final Screen screen = MinecraftClient.getInstance().currentScreen;
+                    entry.edited = true;
+                    entry.rules.remove(rule);
+                    screen.init(MinecraftClient.getInstance(), screen.width, screen.height);
+                });
+            }
+            configureButton.active = this.screenFunction != null;
+            deleteButton.active = !(rule instanceof ManualFilteringRule);
+        }
+        
+        @Override
+        public void render(MatrixStack matrices, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean isHovered, float delta) {
+            MinecraftClient client = MinecraftClient.getInstance();
+            {
+                Text title = getRule().getTitle();
+                int i = client.textRenderer.getWidth(title);
+                if (i > entryWidth - 28) {
+                    StringRenderable titleTrimmed = StringRenderable.concat(client.textRenderer.trimToWidth(title, entryWidth - 28 - client.textRenderer.getStringWidth("...")), StringRenderable.plain("..."));
+                    client.textRenderer.drawWithShadow(matrices, titleTrimmed, x + 2, y + 1, 16777215);
+                } else {
+                    client.textRenderer.drawWithShadow(matrices, title, x + 2, y + 1, 16777215);
+                }
+            }
+            {
+                Text subtitle = getRule().getSubtitle();
+                int i = client.textRenderer.getWidth(subtitle);
+                if (i > entryWidth - 28) {
+                    StringRenderable subtitleTrimmed = StringRenderable.concat(client.textRenderer.trimToWidth(subtitle, entryWidth - 28 - client.textRenderer.getStringWidth("...")), StringRenderable.plain("..."));
+                    client.textRenderer.drawWithShadow(matrices, subtitleTrimmed, x + 2, y + 12, 8421504);
+                } else {
+                    client.textRenderer.drawWithShadow(matrices, subtitle, x + 2, y + 12, 8421504);
+                }
+            }
+            configureButton.x = x + entryWidth - 25;
+            configureButton.y = y + 1;
+            configureButton.render(matrices, mouseX, mouseY, delta);
+            deleteButton.x = x + entryWidth - 27 - deleteButton.getWidth();
+            deleteButton.y = y + 1;
+            deleteButton.render(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Arrays.asList(configureButton, deleteButton);
+        }
+    }
+}

+ 19 - 5
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringScreen.java

@@ -50,7 +50,6 @@ import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.render.BufferBuilder;
 import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexFormats;
-import net.minecraft.client.util.NarratorManager;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.LiteralText;
 import net.minecraft.text.Text;
@@ -103,6 +102,7 @@ public class FilteringScreen extends Screen {
     private ButtonWidget hideButton;
     private ButtonWidget showButton;
     private ButtonWidget backButton;
+    private Rectangle selectionCache;
     
     private List<SearchArgument.SearchArguments> lastSearchArguments = Collections.emptyList();
     
@@ -134,6 +134,7 @@ public class FilteringScreen extends Screen {
                     if (entry.isSelected() && !entry.isFiltered()) {
                         filteringEntry.configFiltered.add(stack);
                         filteringEntry.edited = true;
+                        entry.dirty = true;
                     }
                 }
             });
@@ -147,6 +148,7 @@ public class FilteringScreen extends Screen {
                     entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
                     if (entry.isSelected() && filteringEntry.configFiltered.remove(stack)) {
                         filteringEntry.edited = true;
+                        entry.dirty = true;
                     }
                 }
             });
@@ -207,6 +209,7 @@ public class FilteringScreen extends Screen {
     @Override
     public void render(MatrixStack matrices, int mouseX, int mouseY, float delta) {
         renderHoleBackground(matrices, 0, height, 32, 255, 255);
+        updateSelectionCache();
         Rectangle bounds = getBounds();
         tooltip = null;
         if (bounds.isEmpty())
@@ -269,6 +272,10 @@ public class FilteringScreen extends Screen {
     }
     
     private Rectangle getSelection() {
+        return selectionCache;
+    }
+    
+    private void updateSelectionCache() {
         if (selectionPoint != null) {
             Point p = secondPoint;
             if (p == null) {
@@ -279,9 +286,10 @@ public class FilteringScreen extends Screen {
             int top = Math.min(p.y, selectionPoint.y);
             int right = Math.max(p.x, selectionPoint.x);
             int bottom = Math.max(p.y, selectionPoint.y);
-            return new Rectangle(left, (int) (top - scrolling.scrollAmount), right - left, bottom - top);
+            selectionCache = new Rectangle(left, (int) (top - scrolling.scrollAmount), right - left, bottom - top);
+            return;
         }
-        return new Rectangle(0, 0, 0, 0);
+        selectionCache = new Rectangle(0, 0, 0, 0);
     }
     
     @Override
@@ -416,7 +424,7 @@ public class FilteringScreen extends Screen {
             if (!this.changeFocus(bl)) {
                 this.changeFocus(bl);
             }
-        
+            
             return true;
         }
         return false;
@@ -443,6 +451,8 @@ public class FilteringScreen extends Screen {
     
     private class EntryListEntry extends EntryWidget {
         private int backupY;
+        private boolean filtered = false;
+        private boolean dirty = true;
         
         private EntryListEntry(int x, int y) {
             super(new Point(x, y));
@@ -484,7 +494,11 @@ public class FilteringScreen extends Screen {
         }
         
         public boolean isFiltered() {
-            return CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(filteringEntry.configFiltered, getCurrentEntry()) != null;
+            if (dirty) {
+                filtered = CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(filteringEntry.configFiltered, getCurrentEntry()) != null;
+                dirty = false;
+            }
+            return filtered;
         }
         
         @Override

+ 1 - 3
src/main/java/me/shedaniel/rei/gui/config/entry/NoFilteringEntry.java

@@ -30,14 +30,12 @@ import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.widget.AbstractButtonWidget;
 import net.minecraft.client.gui.widget.ButtonWidget;
-import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.util.NarratorManager;
 import net.minecraft.client.util.Window;
 import net.minecraft.client.util.math.MatrixStack;
 import net.minecraft.text.TranslatableText;
 import org.jetbrains.annotations.ApiStatus;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Consumer;
@@ -51,7 +49,7 @@ public class NoFilteringEntry extends AbstractConfigListEntry<List<EntryStack>>
     private final AbstractButtonWidget buttonWidget = new ButtonWidget(0, 0, 0, 20, new TranslatableText("config.roughlyenoughitems.filteredEntries.loadWorldFirst"), button -> {});
     private final List<Element> children = ImmutableList.of(buttonWidget);
     
-    public NoFilteringEntry(int width,List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
+    public NoFilteringEntry(int width, List<EntryStack> configFiltered, List<EntryStack> defaultValue, Consumer<List<EntryStack>> saveConsumer) {
         super(NarratorManager.EMPTY, false);
         this.width = width;
         this.configFiltered = configFiltered;

+ 18 - 1
src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java

@@ -23,6 +23,7 @@
 
 package me.shedaniel.rei.impl;
 
+import com.google.common.collect.Lists;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonElement;
@@ -49,6 +50,8 @@ import me.shedaniel.rei.gui.config.entry.NoFilteringEntry;
 import me.shedaniel.rei.gui.config.entry.RecipeScreenTypeEntry;
 import me.shedaniel.rei.gui.config.entry.ReloadPluginsEntry;
 import me.shedaniel.rei.gui.credits.CreditsScreen;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
+import me.shedaniel.rei.impl.filtering.rules.ManualFilteringRule;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.fabricmc.loader.api.FabricLoader;
@@ -58,6 +61,8 @@ import net.minecraft.client.gui.screen.ScreenTexts;
 import net.minecraft.client.gui.widget.ButtonWidget;
 import net.minecraft.client.util.InputUtil;
 import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.StringNbtReader;
 import net.minecraft.text.LiteralText;
 import net.minecraft.text.Text;
 import net.minecraft.text.TranslatableText;
@@ -97,6 +102,15 @@ public class ConfigManagerImpl implements ConfigManager {
             return new JsonPrimitive(gson.toJson(stack.toJson()));
         }).registerPrimitiveTypeAdapter(EntryStack.class, it -> {
             return it instanceof String ? EntryStack.readFromJson(gson.fromJson((String) it, JsonElement.class)) : null;
+        }).registerSerializer(FilteringRule.class, (rule, marshaller) -> {
+            return new JsonPrimitive(FilteringRule.toTag(rule, new CompoundTag()).toString());
+        }).registerPrimitiveTypeAdapter(FilteringRule.class, it -> {
+            try {
+                return it instanceof String ? FilteringRule.fromTag(StringNbtReader.parse((String) it)) : null;
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
         }).build()));
         GuiRegistry guiRegistry = AutoConfig.getGuiRegistry(ConfigObjectImpl.class);
         guiRegistry.registerPredicateProvider((i13n, field, config, defaults, guiProvider) -> {
@@ -120,7 +134,7 @@ public class ConfigManagerImpl implements ConfigManager {
                         REIHelper.getInstance().getPreviousContainerScreen() == null || MinecraftClient.getInstance().getNetworkHandler() == null || MinecraftClient.getInstance().getNetworkHandler().getRecipeManager() == null ?
                                 Collections.singletonList(new NoFilteringEntry(220, getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))
                                 :
-                                Collections.singletonList(new FilteringEntry(220, getUnsafely(field, config, new ArrayList<>()), getUnsafely(field, defaults), list -> setUnsafely(field, config, list)))
+                                Collections.singletonList(new FilteringEntry(220, getUnsafely(field, config, new ArrayList<>()), ((ConfigObjectImpl.Advanced.Filtering) config).filteringRules, getUnsafely(field, defaults), list -> setUnsafely(field, config, list), list -> ((ConfigObjectImpl.Advanced.Filtering) config).filteringRules = Lists.newArrayList(list)))
                 , (field) -> field.getType() == List.class, ConfigObjectImpl.UseFilteringScreen.class);
         saveConfig();
         RoughlyEnoughItemsCore.LOGGER.info("Config loaded.");
@@ -136,6 +150,9 @@ public class ConfigManagerImpl implements ConfigManager {
                 stack.setting(EntryStack.Settings.CHECK_AMOUNT, EntryStack.Settings.FALSE).setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE);
             }
         }
+        if (getConfig().getFilteringRules().stream().noneMatch(filteringRule -> filteringRule instanceof ManualFilteringRule)) {
+            getConfig().getFilteringRules().add(new ManualFilteringRule());
+        }
         ((me.sargunvohra.mcmods.autoconfig1u.ConfigManager<ConfigObjectImpl>) AutoConfig.getConfigHolder(ConfigObjectImpl.class)).save();
     }
     

+ 9 - 2
src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java

@@ -32,6 +32,7 @@ import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.gui.config.*;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.util.InputUtil;
@@ -56,7 +57,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     @ConfigEntry.Category("functionality") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
     private Functionality functionality = new Functionality();
     @ConfigEntry.Category("advanced") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
-    private Advanced advanced = new Advanced();
+    public Advanced advanced = new Advanced();
     
     @Override
     public boolean isOverlayVisible() {
@@ -303,6 +304,11 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         return advanced.filtering.filteredStacks;
     }
     
+    @Override
+    public List<FilteringRule<?>> getFilteringRules() {
+        return advanced.filtering.filteringRules;
+    }
+    
     @Override
     @ApiStatus.Experimental
     public boolean shouldAsyncSearch() {
@@ -414,7 +420,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         @ConfigEntry.Gui.CollapsibleObject
         private Miscellaneous miscellaneous = new Miscellaneous();
         @ConfigEntry.Gui.CollapsibleObject(startExpanded = true)
-        private Filtering filtering = new Filtering();
+        public Filtering filtering = new Filtering();
         
         public static class Tooltips {
             @Comment("Declares whether REI should append mod names to entries.") private boolean appendModNames = true;
@@ -465,6 +471,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         
         public static class Filtering {
             @UseFilteringScreen private List<EntryStack> filteredStacks = new ArrayList<>();
+            @ConfigEntry.Gui.Excluded public List<FilteringRule<?>> filteringRules = new ArrayList<>();
         }
     }
 }

+ 12 - 2
src/main/java/me/shedaniel/rei/impl/EntryRegistryImpl.java

@@ -31,6 +31,10 @@ import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.EntryRegistry;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeHelper;
+import me.shedaniel.rei.impl.filtering.FilteringContextImpl;
+import me.shedaniel.rei.impl.filtering.FilteringRule;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.Pair;
@@ -42,6 +46,7 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 @ApiStatus.Internal
+@Environment(EnvType.CLIENT)
 public class EntryRegistryImpl implements EntryRegistry {
     
     private final List<EntryStack> preFilteredList = Lists.newCopyOnWriteArrayList();
@@ -117,14 +122,19 @@ public class EntryRegistryImpl implements EntryRegistry {
     
     public void refilter() {
         long started = System.currentTimeMillis();
-        Collection<EntryStack> filteredStacks = ConfigObject.getInstance().getFilteredStacks();
+        FilteringContextImpl context = new FilteringContextImpl(getStacksList());
+        List<FilteringRule<?>> rules = ConfigObject.getInstance().getFilteringRules();
+        for (int i = rules.size() - 1; i >= 0; i--) {
+            context.handleResult(rules.get(i).processFilteredStacks(context));
+        }
         preFilteredList.clear();
+        Collection<EntryStack> filteredStacks = context.getHiddenStacks();
         for (EntryStack stack : getStacksList()) {
             if (findFirstOrNullEqualsEntryIgnoreAmount(filteredStacks, stack) == null)
                 preFilteredList.add(stack);
         }
         long time = System.currentTimeMillis() - started;
-        RoughlyEnoughItemsCore.LOGGER.info("Refiltered %d entries in %dms.", filteredStacks.size(), time);
+        RoughlyEnoughItemsCore.LOGGER.info("Refiltered %d entries with %d rules in %dms.", filteredStacks.size(), rules.size(), time);
     }
     
     public void reset() {

+ 9 - 2
src/main/java/me/shedaniel/rei/impl/SearchArgument.java

@@ -33,6 +33,8 @@ import me.shedaniel.rei.impl.search.Argument;
 import me.shedaniel.rei.impl.search.ArgumentsRegistry;
 import me.shedaniel.rei.impl.search.MatchStatus;
 import me.shedaniel.rei.utils.CollectionUtils;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.text.Text;
 import org.apache.commons.lang3.StringUtils;
@@ -44,6 +46,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @ApiStatus.Internal
+@Environment(EnvType.CLIENT)
 public class SearchArgument {
     public static final String SPACE = " ", EMPTY = "";
     private static final SearchArgument ALWAYS = new SearchArgument(AlwaysMatchingArgument.INSTANCE, EMPTY, true);
@@ -72,7 +75,7 @@ public class SearchArgument {
             List<SearchArgument> arguments = Lists.newArrayList();
             while (terms.find()) {
                 String term = MoreObjects.firstNonNull(terms.group(1), terms.group(2));
-                for (Argument argument : ArgumentsRegistry.ARGUMENTS) {
+                for (Argument argument : ArgumentsRegistry.ARGUMENT_LIST) {
                     MatchStatus status = argument.matchesArgumentPrefix(term);
                     if (status.isMatched()) {
                         arguments.add(new SearchArgument(argument, status.getText(), !status.isInverted(), !status.shouldPreserveCasing()));
@@ -99,7 +102,7 @@ public class SearchArgument {
         if (searchArguments.isEmpty())
             return true;
         MinecraftClient minecraft = MinecraftClient.getInstance();
-        Object[] data = new Object[ArgumentsRegistry.ARGUMENTS.size()];
+        Object[] data = new Object[ArgumentsRegistry.ARGUMENT_LIST.size()];
         for (SearchArgument.SearchArguments arguments : searchArguments) {
             boolean applicable = true;
             for (SearchArgument argument : arguments.getArguments()) {
@@ -149,6 +152,10 @@ public class SearchArgument {
         public SearchArgument[] getArguments() {
             return arguments;
         }
+        
+        public final boolean isAlways() {
+            return this == ALWAYS;
+        }
     }
     
 }

+ 40 - 0
src/main/java/me/shedaniel/rei/impl/filtering/AbstractFilteringRule.java

@@ -0,0 +1,40 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+
+@Environment(EnvType.CLIENT)
+public abstract class AbstractFilteringRule<T extends AbstractFilteringRule<?>> implements FilteringRule<T> {
+    @Override
+    public boolean equals(Object obj) {
+        return getClass() == obj.getClass();
+    }
+    
+    @Override
+    public int hashCode() {
+        return getClass().hashCode();
+    }
+}

+ 52 - 0
src/main/java/me/shedaniel/rei/impl/filtering/FilteringContext.java

@@ -0,0 +1,52 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+import me.shedaniel.rei.api.EntryStack;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Map;
+import java.util.Set;
+
+@ApiStatus.Internal
+@ApiStatus.Experimental
+public interface FilteringContext {
+    Map<FilteringContextType, Set<EntryStack>> getStacks();
+    
+    default Set<EntryStack> getStacks(FilteringContextType type) {
+        return getStacks().get(type);
+    }
+    
+    default Set<EntryStack> getShownStacks() {
+        return getStacks(FilteringContextType.SHOWN);
+    }
+    
+    default Set<EntryStack> getUnsetStacks() {
+        return getStacks(FilteringContextType.DEFAULT);
+    }
+    
+    default Set<EntryStack> getHiddenStacks() {
+        return getStacks(FilteringContextType.HIDDEN);
+    }
+}

+ 63 - 0
src/main/java/me/shedaniel/rei/impl/filtering/FilteringContextImpl.java

@@ -0,0 +1,63 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+import com.google.common.collect.Maps;
+import me.shedaniel.rei.api.EntryStack;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+
+import java.util.*;
+
+@Environment(EnvType.CLIENT)
+public class FilteringContextImpl implements FilteringContext {
+    private final Map<FilteringContextType, Set<EntryStack>> stacks;
+    
+    public FilteringContextImpl(List<EntryStack> allStacks) {
+        this(Maps.newHashMap());
+        getUnsetStacks().addAll(allStacks);
+    }
+    
+    public FilteringContextImpl(Map<FilteringContextType, Set<EntryStack>> stacks) {
+        this.stacks = stacks;
+        for (FilteringContextType type : FilteringContextType.values()) {
+            this.stacks.computeIfAbsent(type, t -> new TreeSet<>(Comparator.comparing(EntryStack::hashIgnoreAmount)));
+        }
+    }
+    
+    @Override
+    public Map<FilteringContextType, Set<EntryStack>> getStacks() {
+        return stacks;
+    }
+    
+    public void handleResult(FilteringResult result) {
+        getUnsetStacks().removeAll(result.getHiddenStacks());
+        getShownStacks().removeAll(result.getHiddenStacks());
+        getHiddenStacks().addAll(result.getHiddenStacks());
+        
+        getHiddenStacks().removeAll(result.getShownStacks());
+        getUnsetStacks().removeAll(result.getShownStacks());
+        getShownStacks().addAll(result.getShownStacks());
+    }
+}

+ 34 - 0
src/main/java/me/shedaniel/rei/impl/filtering/FilteringContextType.java

@@ -0,0 +1,34 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+public enum FilteringContextType {
+    SHOWN,
+    DEFAULT,
+    HIDDEN;
+    
+    public boolean isHidden() {
+        return this == HIDDEN;
+    }
+}

+ 67 - 0
src/main/java/me/shedaniel/rei/impl/filtering/FilteringResult.java

@@ -0,0 +1,67 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.EntryStack;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+@ApiStatus.Internal
+@ApiStatus.Experimental
+public interface FilteringResult {
+    static FilteringResult create() {
+        return create(Lists.newArrayList(), Lists.newArrayList());
+    }
+    
+    static FilteringResult create(List<EntryStack> hiddenStacks, List<EntryStack> shownStacks) {
+        return new FilteringResultImpl(hiddenStacks, shownStacks);
+    }
+    
+    Set<EntryStack> getHiddenStacks();
+    
+    Set<EntryStack> getShownStacks();
+    
+    default FilteringResult hide(EntryStack stack) {
+        return hide(Collections.singletonList(stack));
+    }
+    
+    default FilteringResult hide(Collection<EntryStack> stacks) {
+        getHiddenStacks().addAll(stacks);
+        return this;
+    }
+    
+    default FilteringResult show(EntryStack stack) {
+        return show(Collections.singletonList(stack));
+    }
+    
+    default FilteringResult show(Collection<EntryStack> stacks) {
+        getShownStacks().addAll(stacks);
+        return this;
+    }
+}

+ 52 - 0
src/main/java/me/shedaniel/rei/impl/filtering/FilteringResultImpl.java

@@ -0,0 +1,52 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+import me.shedaniel.rei.api.EntryStack;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class FilteringResultImpl implements FilteringResult {
+    private final Set<EntryStack> hiddenStacks, shownStacks;
+    
+    public FilteringResultImpl(List<EntryStack> hiddenStacks, List<EntryStack> shownStacks) {
+        this.hiddenStacks = new TreeSet<>(Comparator.comparing(EntryStack::hashIgnoreAmount));
+        this.shownStacks = new TreeSet<>(Comparator.comparing(EntryStack::hashIgnoreAmount));
+        this.hiddenStacks.addAll(hiddenStacks);
+        this.shownStacks.addAll(shownStacks);
+    }
+    
+    @Override
+    public Set<EntryStack> getHiddenStacks() {
+        return hiddenStacks;
+    }
+    
+    @Override
+    public Set<EntryStack> getShownStacks() {
+        return shownStacks;
+    }
+}

+ 92 - 0
src/main/java/me/shedaniel/rei/impl/filtering/FilteringRule.java

@@ -0,0 +1,92 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering;
+
+import com.mojang.serialization.Lifecycle;
+import me.shedaniel.rei.gui.config.entry.FilteringEntry;
+import me.shedaniel.rei.impl.filtering.rules.ManualFilteringRule;
+import me.shedaniel.rei.impl.filtering.rules.SearchFilteringRule;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.Registry;
+import net.minecraft.util.registry.RegistryKey;
+import net.minecraft.util.registry.SimpleRegistry;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+@ApiStatus.Internal
+@ApiStatus.Experimental
+@Environment(EnvType.CLIENT)
+public interface FilteringRule<T extends FilteringRule<?>> {
+    RegistryKey<Registry<FilteringRule<?>>> REGISTRY_KEY = RegistryKey.ofRegistry(new Identifier("roughlyenoughitems", "filtering_rule"));
+    Registry<FilteringRule<?>> REGISTRY = createRegistry();
+    
+    @ApiStatus.Internal
+    static Registry<FilteringRule<?>> createRegistry() {
+        SimpleRegistry<FilteringRule<?>> registry = new SimpleRegistry<>(REGISTRY_KEY, Lifecycle.stable());
+        Registry.register(registry, new Identifier("roughlyenoughitems", "search"), new SearchFilteringRule());
+        Registry.register(registry, new Identifier("roughlyenoughitems", "manual"), new ManualFilteringRule());
+        return registry;
+    }
+    
+    static CompoundTag toTag(FilteringRule<?> rule, CompoundTag tag) {
+        tag.putString("id", REGISTRY.getId(rule).toString());
+        tag.put("rule", rule.toTag(new CompoundTag()));
+        return tag;
+    }
+    
+    static FilteringRule<?> fromTag(CompoundTag tag) {
+        return REGISTRY.get(Identifier.tryParse(tag.getString("id"))).createFromTag(tag.getCompound("rule"));
+    }
+    
+    CompoundTag toTag(CompoundTag tag);
+    
+    T createFromTag(CompoundTag tag);
+    
+    @NotNull
+    FilteringResult processFilteredStacks(@NotNull FilteringContext context);
+    
+    @ApiStatus.Internal
+    default Optional<BiFunction<FilteringEntry, Screen, Screen>> createEntryScreen() {
+        return Optional.empty();
+    }
+    
+    default Text getTitle() {
+        return Text.method_30163(FilteringRule.REGISTRY.getId(this).toString());
+    }
+    
+    default Text getSubtitle() {
+        return Text.method_30163(null);
+    }
+    
+    T createNew();
+}

+ 92 - 0
src/main/java/me/shedaniel/rei/impl/filtering/rules/ManualFilteringRule.java

@@ -0,0 +1,92 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering.rules;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.impl.filtering.AbstractFilteringRule;
+import me.shedaniel.rei.impl.filtering.FilteringContext;
+import me.shedaniel.rei.impl.filtering.FilteringResult;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public class ManualFilteringRule extends AbstractFilteringRule<ManualFilteringRule> {
+    @Override
+    public CompoundTag toTag(CompoundTag tag) {
+        return tag;
+    }
+    
+    @Override
+    public ManualFilteringRule createFromTag(CompoundTag tag) {
+        return new ManualFilteringRule();
+    }
+    
+    @Override
+    public @NotNull FilteringResult processFilteredStacks(@NotNull FilteringContext context) {
+        FilteringResult result = FilteringResult.create();
+        processList(context.getShownStacks(), result);
+        processList(context.getUnsetStacks(), result);
+        return result;
+    }
+    
+    private void processList(Set<EntryStack> stacks, FilteringResult result) {
+        List<EntryStack> filteredStacks = ConfigObject.getInstance().getFilteredStacks();
+        List<EntryStack> filtered = Lists.newArrayList();
+        for (EntryStack stack : stacks) {
+            if (findFirstOrNullEqualsEntryIgnoreAmount(filteredStacks, stack) != null)
+                filtered.add(stack);
+        }
+        result.hide(filtered);
+    }
+    
+    private static EntryStack findFirstOrNullEqualsEntryIgnoreAmount(Collection<EntryStack> list, EntryStack obj) {
+        for (EntryStack t : list) {
+            if (t.equalsIgnoreAmount(obj))
+                return t;
+        }
+        return null;
+    }
+    
+    @Override
+    public Text getTitle() {
+        return new TranslatableText("rule.roughlyenoughitems.filtering.manual");
+    }
+    
+    @Override
+    public Text getSubtitle() {
+        return new TranslatableText("rule.roughlyenoughitems.filtering.manual.subtitle");
+    }
+    
+    @Override
+    public ManualFilteringRule createNew() {
+        throw new UnsupportedOperationException();
+    }
+}

+ 172 - 0
src/main/java/me/shedaniel/rei/impl/filtering/rules/SearchFilteringRule.java

@@ -0,0 +1,172 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.filtering.rules;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.gui.config.entry.FilteringEntry;
+import me.shedaniel.rei.gui.config.entry.FilteringRuleOptionsScreen;
+import me.shedaniel.rei.impl.SearchArgument;
+import me.shedaniel.rei.impl.filtering.AbstractFilteringRule;
+import me.shedaniel.rei.impl.filtering.FilteringContext;
+import me.shedaniel.rei.impl.filtering.FilteringResult;
+import net.fabricmc.api.EnvType;
+import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.text.Text;
+import net.minecraft.text.TranslatableText;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+
+@Environment(EnvType.CLIENT)
+public class SearchFilteringRule extends AbstractFilteringRule<SearchFilteringRule> {
+    private String filter;
+    private List<SearchArgument.SearchArguments> arguments;
+    private boolean show;
+    
+    public SearchFilteringRule() {
+    }
+    
+    public SearchFilteringRule(String filter, List<SearchArgument.SearchArguments> arguments, boolean show) {
+        this.filter = filter;
+        this.arguments = arguments;
+        this.show = show;
+    }
+    
+    @Override
+    public CompoundTag toTag(CompoundTag tag) {
+        tag.putString("filter", filter);
+        tag.putBoolean("show", show);
+        return tag;
+    }
+    
+    @Override
+    public SearchFilteringRule createFromTag(CompoundTag tag) {
+        String filter = tag.getString("filter");
+        boolean show = tag.getBoolean("show");
+        return new SearchFilteringRule(filter, SearchArgument.processSearchTerm(filter), show);
+    }
+    
+    @NotNull
+    @Override
+    public FilteringResult processFilteredStacks(@NotNull FilteringContext context) {
+        List<CompletableFuture<List<EntryStack>>> completableFutures = Lists.newArrayList();
+        processList(context.getUnsetStacks(), completableFutures);
+        if (show) processList(context.getHiddenStacks(), completableFutures);
+        else processList(context.getShownStacks(), completableFutures);
+        try {
+            CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture[0])).get(10, TimeUnit.SECONDS);
+        } catch (InterruptedException | ExecutionException | TimeoutException e) {
+            e.printStackTrace();
+        }
+        FilteringResult result = FilteringResult.create();
+        for (CompletableFuture<List<EntryStack>> future : completableFutures) {
+            List<EntryStack> now = future.getNow(null);
+            if (now != null) {
+                if (show) {
+                    result.show(now);
+                } else {
+                    result.hide(now);
+                }
+            }
+        }
+        return result;
+    }
+    
+    @Override
+    public SearchFilteringRule createNew() {
+        return new SearchFilteringRule("", Collections.singletonList(SearchArgument.SearchArguments.ALWAYS), true);
+    }
+    
+    private void processList(Set<EntryStack> stacks, List<CompletableFuture<List<EntryStack>>> completableFutures) {
+        int size = 100;
+        List<EntryStack> stacks1 = Lists.newArrayList(stacks);
+        Iterator<EntryStack> iterator = stacks1.iterator();
+        for (int i = 0; i < stacks1.size(); i += size) {
+            int[] start = {i};
+            completableFutures.add(CompletableFuture.supplyAsync(() -> {
+                int end = Math.min(stacks1.size(), start[0] + size);
+                List<EntryStack> output = Lists.newArrayList();
+                for (; start[0] < end; start[0]++) {
+                    EntryStack stack = stacks1.get(start[0]);
+                    boolean shown = SearchArgument.canSearchTermsBeAppliedTo(stack, arguments);
+                    if (shown) {
+                        output.add(stack);
+                    }
+                }
+                return output;
+            }));
+        }
+    }
+    
+    @Override
+    public Text getTitle() {
+        return new TranslatableText("rule.roughlyenoughitems.filtering.search");
+    }
+    
+    @Override
+    public Text getSubtitle() {
+        return new TranslatableText("rule.roughlyenoughitems.filtering.search.subtitle");
+    }
+    
+    @Override
+    public Optional<BiFunction<FilteringEntry, Screen, Screen>> createEntryScreen() {
+        return Optional.of((entry, screen) -> new FilteringRuleOptionsScreen<SearchFilteringRule>(entry, this, screen) {
+            TextFieldRuleEntry entry = null;
+            BooleanRuleEntry show = null;
+            
+            @Override
+            public void addEntries(Consumer<RuleEntry> entryConsumer) {
+                addEmpty(entryConsumer, 10);
+                addText(entryConsumer, new TranslatableText("rule.roughlyenoughitems.filtering.search.filter").formatted(Formatting.GRAY));
+                entryConsumer.accept(entry = new TextFieldRuleEntry(width - 36, rule, widget -> {
+                    widget.setMaxLength(9999);
+                    if (entry != null) widget.setText(entry.getWidget().getText());
+                    else widget.setText(rule.filter);
+                }));
+                addEmpty(entryConsumer, 10);
+                addText(entryConsumer, new TranslatableText("rule.roughlyenoughitems.filtering.search.show").formatted(Formatting.GRAY));
+                entryConsumer.accept(show = new BooleanRuleEntry(width - 36, show == null ? rule.show : show.getBoolean(), rule, bool -> {
+                    return new TranslatableText("rule.roughlyenoughitems.filtering.search.show." + bool);
+                }));
+            }
+            
+            @Override
+            public void save() {
+                rule.filter = entry.getWidget().getText();
+                rule.arguments = SearchArgument.processSearchTerm(rule.filter);
+                rule.show = show.getBoolean();
+            }
+        });
+    }
+}

+ 1 - 1
src/main/java/me/shedaniel/rei/impl/search/Argument.java

@@ -51,7 +51,7 @@ public abstract class Argument {
     
     public final int getDataOrdinal() {
         if (dataOrdinal == -1) {
-            dataOrdinal = ArgumentsRegistry.ARGUMENTS.indexOf(this);
+            dataOrdinal = ArgumentsRegistry.ARGUMENT_LIST.indexOf(this);
         }
         return dataOrdinal;
     }

+ 15 - 7
src/main/java/me/shedaniel/rei/impl/search/ArgumentsRegistry.java

@@ -24,20 +24,28 @@
 package me.shedaniel.rei.impl.search;
 
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import org.jetbrains.annotations.ApiStatus;
 
 import java.util.List;
+import java.util.Map;
 
 @ApiStatus.Internal
 public final class ArgumentsRegistry {
-    public static final List<Argument> ARGUMENTS = Lists.newArrayList();
+    public static final Map<String, Argument> ARGUMENTS = Maps.newHashMap();
+    public static final List<Argument> ARGUMENT_LIST = Lists.newArrayList();
     
     static {
-        ARGUMENTS.add(AlwaysMatchingArgument.INSTANCE);
-        ARGUMENTS.add(ModArgument.INSTANCE);
-        ARGUMENTS.add(TooltipArgument.INSTANCE);
-        ARGUMENTS.add(TagArgument.INSTANCE);
-        ARGUMENTS.add(RegexArgument.INSTANCE);
-        ARGUMENTS.add(TextArgument.INSTANCE);
+        register(AlwaysMatchingArgument.INSTANCE);
+        register(ModArgument.INSTANCE);
+        register(TooltipArgument.INSTANCE);
+        register(TagArgument.INSTANCE);
+        register(RegexArgument.INSTANCE);
+        register(TextArgument.INSTANCE);
+    }
+    
+    private static void register(Argument argument) {
+        ARGUMENTS.put(argument.getName(), argument);
+        ARGUMENT_LIST.add(argument);
     }
 }

+ 11 - 0
src/main/resources/assets/roughlyenoughitems/lang/en_us.json

@@ -202,6 +202,17 @@
   "config.roughlyenoughitems.scrollingEntryListWidget.boolean.true": "Scrolled",
   "config.roughlyenoughitems.scrollingEntryListWidget.boolean.false": "Paginated",
   "config.roughlyenoughitems.filteringScreen": "Customized Filtering",
+  "config.roughlyenoughitems.filteringRulesScreen": "Customized Filtering Rules",
+  "config.roughlyenoughitems.filteringRulesScreen.new": "Create Filtering Rule",
+  "config.roughlyenoughitems.filteringRulesScreen.delete": "Delete",
+  "rule.roughlyenoughitems.filtering.manual": "Manual Filtering",
+  "rule.roughlyenoughitems.filtering.manual.subtitle": "Manually select what to hide and show.",
+  "rule.roughlyenoughitems.filtering.search": "Search Filtering",
+  "rule.roughlyenoughitems.filtering.search.subtitle": "Filter using search filters.",
+  "rule.roughlyenoughitems.filtering.search.filter": "Search Filter:",
+  "rule.roughlyenoughitems.filtering.search.show": "Show / Hide:",
+  "rule.roughlyenoughitems.filtering.search.show.true": "Show",
+  "rule.roughlyenoughitems.filtering.search.show.false": "Hide",
   "language.roughlyenoughitems.english": "English",
   "language.roughlyenoughitems.japanese": "Japanese",
   "language.roughlyenoughitems.chinese_simplified": "Chinese Simplified",