Danielshe 5 жил өмнө
parent
commit
f7bf538084
75 өөрчлөгдсөн 2435 нэмэгдсэн , 1126 устгасан
  1. 11 3
      build.gradle
  2. 1 1
      gradle.properties
  3. 0 52
      src/main/java/com/zeitheron/hammercore/client/utils/Scissors.java
  4. 4 3
      src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java
  5. 18 6
      src/main/java/me/shedaniel/rei/api/ClientHelper.java
  6. 16 1
      src/main/java/me/shedaniel/rei/api/Entry.java
  7. 63 13
      src/main/java/me/shedaniel/rei/api/EntryRegistry.java
  8. 153 0
      src/main/java/me/shedaniel/rei/api/EntryStack.java
  9. 16 2
      src/main/java/me/shedaniel/rei/api/LiveRecipeGenerator.java
  10. 22 0
      src/main/java/me/shedaniel/rei/api/ObjectHolder.java
  11. 14 2
      src/main/java/me/shedaniel/rei/api/RecipeCategory.java
  12. 64 4
      src/main/java/me/shedaniel/rei/api/RecipeDisplay.java
  13. 32 3
      src/main/java/me/shedaniel/rei/api/RecipeHelper.java
  14. 18 0
      src/main/java/me/shedaniel/rei/api/Renderer.java
  15. 25 1
      src/main/java/me/shedaniel/rei/api/TransferRecipeDisplay.java
  16. 17 0
      src/main/java/me/shedaniel/rei/api/annotations/Experimental.java
  17. 17 0
      src/main/java/me/shedaniel/rei/api/annotations/Internal.java
  18. 17 0
      src/main/java/me/shedaniel/rei/api/annotations/ToBeRemoved.java
  19. 11 8
      src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  20. 8 9
      src/main/java/me/shedaniel/rei/gui/OverlaySearchField.java
  21. 12 4
      src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  22. 9 10
      src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  23. 1 1
      src/main/java/me/shedaniel/rei/gui/credits/CreditsScreen.java
  24. 68 26
      src/main/java/me/shedaniel/rei/gui/renderers/SimpleRecipeRenderer.java
  25. 1 1
      src/main/java/me/shedaniel/rei/gui/widget/CategoryBaseWidget.java
  26. 7 7
      src/main/java/me/shedaniel/rei/gui/widget/ClickableLabelWidget.java
  27. 3 0
      src/main/java/me/shedaniel/rei/gui/widget/DetailedButtonWidget.java
  28. 150 173
      src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  29. 195 0
      src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java
  30. 24 1
      src/main/java/me/shedaniel/rei/gui/widget/LabelWidget.java
  31. 111 0
      src/main/java/me/shedaniel/rei/gui/widget/PanelWidget.java
  32. 7 64
      src/main/java/me/shedaniel/rei/gui/widget/RecipeBaseWidget.java
  33. 1 1
      src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java
  34. 58 20
      src/main/java/me/shedaniel/rei/gui/widget/SlotWidget.java
  35. 8 12
      src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java
  36. 8 0
      src/main/java/me/shedaniel/rei/gui/widget/Widget.java
  37. 78 0
      src/main/java/me/shedaniel/rei/impl/AbstractEntryStack.java
  38. 10 20
      src/main/java/me/shedaniel/rei/impl/BaseBoundsHandlerImpl.java
  39. 11 9
      src/main/java/me/shedaniel/rei/impl/ClientHelperImpl.java
  40. 14 8
      src/main/java/me/shedaniel/rei/impl/DisplayHelperImpl.java
  41. 96 0
      src/main/java/me/shedaniel/rei/impl/EmptyEntryStack.java
  42. 8 22
      src/main/java/me/shedaniel/rei/impl/EntryRegistryImpl.java
  43. 15 0
      src/main/java/me/shedaniel/rei/impl/FluidEntry.java
  44. 186 0
      src/main/java/me/shedaniel/rei/impl/FluidEntryStack.java
  45. 158 0
      src/main/java/me/shedaniel/rei/impl/ItemEntryStack.java
  46. 15 0
      src/main/java/me/shedaniel/rei/impl/ItemStackEntry.java
  47. 51 0
      src/main/java/me/shedaniel/rei/impl/ObjectHolderImpl.java
  48. 28 21
      src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java
  49. 15 2
      src/main/java/me/shedaniel/rei/impl/ScreenHelper.java
  50. 8 6
      src/main/java/me/shedaniel/rei/mixin/MixinBrewingRecipeRegistry.java
  51. 43 37
      src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java
  52. 13 9
      src/main/java/me/shedaniel/rei/plugin/autocrafting/DefaultCategoryHandler.java
  53. 0 92
      src/main/java/me/shedaniel/rei/plugin/blasting/DefaultBlastingCategory.java
  54. 3 65
      src/main/java/me/shedaniel/rei/plugin/blasting/DefaultBlastingDisplay.java
  55. 10 13
      src/main/java/me/shedaniel/rei/plugin/brewing/DefaultBrewingCategory.java
  56. 29 16
      src/main/java/me/shedaniel/rei/plugin/brewing/DefaultBrewingDisplay.java
  57. 6 7
      src/main/java/me/shedaniel/rei/plugin/campfire/DefaultCampfireCategory.java
  58. 19 12
      src/main/java/me/shedaniel/rei/plugin/campfire/DefaultCampfireDisplay.java
  59. 16 18
      src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingCategory.java
  60. 16 16
      src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingDisplay.java
  61. 41 36
      src/main/java/me/shedaniel/rei/plugin/cooking/DefaultCookingCategory.java
  62. 84 0
      src/main/java/me/shedaniel/rei/plugin/cooking/DefaultCookingDisplay.java
  63. 8 6
      src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCraftingCategory.java
  64. 5 5
      src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCraftingDisplay.java
  65. 13 7
      src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCustomDisplay.java
  66. 20 8
      src/main/java/me/shedaniel/rei/plugin/crafting/DefaultShapedDisplay.java
  67. 20 8
      src/main/java/me/shedaniel/rei/plugin/crafting/DefaultShapelessDisplay.java
  68. 0 91
      src/main/java/me/shedaniel/rei/plugin/smelting/DefaultSmeltingCategory.java
  69. 3 65
      src/main/java/me/shedaniel/rei/plugin/smelting/DefaultSmeltingDisplay.java
  70. 3 66
      src/main/java/me/shedaniel/rei/plugin/smoking/DefaultSmokingDisplay.java
  71. 6 5
      src/main/java/me/shedaniel/rei/plugin/stonecutting/DefaultStoneCuttingCategory.java
  72. 17 11
      src/main/java/me/shedaniel/rei/plugin/stonecutting/DefaultStoneCuttingDisplay.java
  73. 6 7
      src/main/java/me/shedaniel/rei/plugin/stripping/DefaultStrippingCategory.java
  74. 10 10
      src/main/java/me/shedaniel/rei/plugin/stripping/DefaultStrippingDisplay.java
  75. 161 0
      src/main/java/me/shedaniel/rei/utils/CollectionUtils.java

+ 11 - 3
build.gradle

@@ -29,7 +29,6 @@ static def buildTime() {
 license {
     header rootProject.file('HEADER')
     include '**/*.java'
-    exclude '**/Scissors.java'
 }
 
 repositories {
@@ -56,7 +55,7 @@ dependencies {
     modImplementation("me.shedaniel.cloth:config-2:${cloth_config_version}") {
         transitive = false
     }
-    modApi("me.shedaniel.cloth:fiber2cloth:1.2.0") {
+    modApi("me.shedaniel.cloth:fiber2cloth:1.2.1") {
         transitive = false
     }
     modApi "me.zeroeightsix:fiber:0.6.0-7"
@@ -68,7 +67,7 @@ dependencies {
         include("me.shedaniel.cloth:config-2:${cloth_config_version}") {
             transitive = false
         }
-        include("me.shedaniel.cloth:fiber2cloth:1.2.0") {
+        include("me.shedaniel.cloth:fiber2cloth:1.2.1") {
             transitive = false
         }
         include "me.zeroeightsix:fiber:0.6.0-7"
@@ -84,6 +83,15 @@ task sourcesJar(type: Jar, dependsOn: classes) {
     from sourceSets.main.allSource
 }
 
+task remapMavenJar(type: net.fabricmc.loom.task.RemapJarTask, dependsOn: jar) {
+    classifier = "maven"
+    afterEvaluate {
+        input = file("${project.buildDir}/libs/${archivesBaseName}-${version}-dev.jar")
+//        archiveName = "${archivesBaseName}-${version}-maven.jar"
+        addNestedDependencies = false
+    }
+}
+
 publishing {
     publications {
         mavenJava(MavenPublication) {

+ 1 - 1
gradle.properties

@@ -1,4 +1,4 @@
-mod_version=3.1.8-unstable
+mod_version=3.2.1-unstable
 minecraft_version=19w44a
 yarn_version=19w44a+build.3
 fabricloader_version=0.6.3+build.168

+ 0 - 52
src/main/java/com/zeitheron/hammercore/client/utils/Scissors.java

@@ -1,52 +0,0 @@
-package com.zeitheron.hammercore.client.utils;
-
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.util.Window;
-import org.lwjgl.opengl.GL11;
-
-/**
- * This is originally the part of Hammer Lib, repacked in REI with permission.
- * Adapted GL scissor for minecraft pixel resolution and adjusts (0;0) as left-top corner.
- *
- * @author Zeitheron
- */
-public class Scissors {
-    /**
-     * Starts the scissor test
-     */
-    public static void begin() {
-        GL11.glEnable(GL11.GL_SCISSOR_TEST);
-    }
-    
-    /**
-     * Setup the scissor bounds
-     *
-     * @param x      the top left x coordinates
-     * @param y      the top left y coordinates
-     * @param width  the width of the bounds
-     * @param height the height of the bounds
-     */
-    public static void scissor(int x, int y, int width, int height) {
-        Window window = MinecraftClient.getInstance().getWindow();
-        
-        int sw = window.getWidth();
-        int sh = window.getHeight();
-        float dw = window.getScaledWidth();
-        float dh = window.getScaledHeight();
-        
-        x = Math.round(sw * (x / dw));
-        y = Math.round(sh * (y / dh));
-        
-        width = Math.round(sw * (width / dw));
-        height = Math.round(sh * (height / dh));
-        
-        GL11.glScissor(x, sh - height - y, width, height);
-    }
-    
-    /**
-     * Stops the scissor test
-     */
-    public static void end() {
-        GL11.glDisable(GL11.GL_SCISSOR_TEST);
-    }
-}

+ 4 - 3
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -136,9 +136,10 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
         
         registerClothEvents();
         discoverPluginEntries();
-        FabricLoader.getInstance().getAllMods().stream().map(ModContainer::getMetadata).filter(metadata -> metadata.containsCustomValue("roughlyenoughitems:plugins")).forEach(modMetadata -> {
-            RoughlyEnoughItemsCore.LOGGER.error("[REI] REI plugin from " + modMetadata.getId() + " is not loaded because it is too old!");
-        });
+        for (ModContainer modContainer : FabricLoader.getInstance().getAllMods()) {
+            if (modContainer.getMetadata().containsCustomValue("roughlyenoughitems:plugins"))
+                RoughlyEnoughItemsCore.LOGGER.error("[REI] REI plugin from " + modContainer.getMetadata().getId() + " is not loaded because it is too old!");
+        }
         
         ClientSidePacketRegistry.INSTANCE.register(RoughlyEnoughItemsNetwork.CREATE_ITEMS_MESSAGE_PACKET, (packetContext, packetByteBuf) -> {
             ItemStack stack = packetByteBuf.readItemStack();

+ 18 - 6
src/main/java/me/shedaniel/rei/api/ClientHelper.java

@@ -55,28 +55,40 @@ public interface ClientHelper {
     void registerFabricKeyBinds();
     
     /**
-     * Tries to cheat items using either packets or commands.
+     * Tries to cheat stack using either packets or commands.
      *
      * @param stack the stack to cheat in
      * @return whether it failed
      */
-    boolean tryCheatingStack(ItemStack stack);
+    boolean tryCheatingEntry(EntryStack stack);
+    
+    default boolean tryCheatingStack(ItemStack stack) {
+        return tryCheatingEntry(EntryStack.create(stack));
+    }
     
     /**
-     * Finds recipe for the item and opens the recipe screen.
+     * Finds recipe for the stack and opens the recipe screen.
      *
      * @param stack the stack to find recipe for
      * @return whether the stack has any recipes to show
      */
-    boolean executeRecipeKeyBind(ItemStack stack);
+    boolean executeRecipeKeyBind(EntryStack stack);
+    
+    default boolean executeRecipeKeyBind(ItemStack stack) {
+        return executeRecipeKeyBind(EntryStack.create(stack));
+    }
     
     /**
-     * Finds usage for the item and opens the recipe screen.
+     * Finds usage for the stack and opens the recipe screen.
      *
      * @param stack the stack to find usage for
      * @return whether the stack has any usages to show
      */
-    boolean executeUsageKeyBind(ItemStack stack);
+    boolean executeUsageKeyBind(EntryStack stack);
+    
+    default boolean executeUsageKeyBind(ItemStack stack) {
+        return executeUsageKeyBind(EntryStack.create(stack));
+    }
     
     FabricKeyBinding getFocusSearchFieldKeyBinding();
     

+ 16 - 1
src/main/java/me/shedaniel/rei/api/Entry.java

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.api;
 
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import me.shedaniel.rei.impl.FluidEntry;
 import me.shedaniel.rei.impl.ItemStackEntry;
 import net.minecraft.fluid.Fluid;
@@ -12,7 +13,9 @@ import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nullable;
 
-public interface Entry {
+@Deprecated
+@ToBeRemoved
+public interface Entry extends Cloneable {
     @SuppressWarnings("deprecation")
     static Entry create(ItemStack itemStack) {
         return new ItemStackEntry(itemStack);
@@ -31,6 +34,18 @@ public interface Entry {
     @Nullable
     Fluid getFluid();
     
+    Entry clone();
+    
+    default EntryStack toEntryStack() {
+        if (getEntryType() == Type.ITEM)
+            return EntryStack.create(getItemStack());
+        if (getEntryType() == Type.FLUID)
+            return EntryStack.create(getFluid());
+        return EntryStack.empty();
+    }
+    
+    boolean equalsEntry(Entry other, boolean checkTags);
+    
     public static enum Type {
         ITEM, FLUID
     }

+ 63 - 13
src/main/java/me/shedaniel/rei/api/EntryRegistry.java

@@ -5,10 +5,13 @@
 
 package me.shedaniel.rei.api;
 
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
 
+import java.util.Collections;
 import java.util.List;
 
 public interface EntryRegistry {
@@ -18,7 +21,17 @@ public interface EntryRegistry {
      *
      * @return an unmodifiable item list
      */
-    List<Entry> getEntryList();
+    @Deprecated
+    default List<Entry> getEntryList() {
+        return Collections.unmodifiableList(getModifiableEntryList());
+    }
+    
+    /**
+     * Gets the current modifiable stacks list
+     *
+     * @return a stacks list
+     */
+    List<EntryStack> getStacksList();
     
     /**
      * Gets the current modifiable item list
@@ -26,7 +39,9 @@ public interface EntryRegistry {
      * @return an modifiable item list
      */
     @Deprecated
-    List<Entry> getModifiableEntryList();
+    default List<Entry> getModifiableEntryList() {
+        return CollectionUtils.map(getStacksList(), EntryStack::toEntry);
+    }
     
     /**
      * Gets all possible stacks from an item
@@ -42,31 +57,66 @@ public interface EntryRegistry {
      * @param afterItem the stack to put after
      * @param stack     the stack to register
      */
-    void registerItemStack(Item afterItem, ItemStack stack);
+    @Deprecated
+    default void registerItemStack(Item afterItem, ItemStack stack) {
+        registerEntryAfter(EntryStack.create(afterItem), EntryStack.create(stack));
+    }
     
-    void registerFluid(Fluid fluid);
+    @Deprecated
+    default void registerFluid(Fluid fluid) {
+        registerEntry(EntryStack.create(fluid));
+    }
+    
+    default void registerEntry(EntryStack stack) {
+        registerEntryAfter(null, stack);
+    }
+    
+    void registerEntryAfter(EntryStack afterEntry, EntryStack stack);
+    
+    @ToBeRemoved
+    @Deprecated
+    default void registerItemStack(Item afterItem, ItemStack... stacks) {
+        EntryStack afterStack = EntryStack.create(afterItem);
+        for (int i = stacks.length - 1; i >= 0; i--) {
+            ItemStack stack = stacks[i];
+            if (stack != null && !stack.isEmpty())
+                registerEntryAfter(afterStack, EntryStack.create(stack));
+        }
+    }
     
     /**
      * Registers multiple stacks to the item list
      *
-     * @param afterItem the stack to put after
-     * @param stacks    the stacks to register
+     * @param afterStack the stack to put after
+     * @param stacks     the stacks to register
      */
-    default void registerItemStack(Item afterItem, ItemStack... stacks) {
+    default void registerEntriesAfter(EntryStack afterStack, EntryStack... stacks) {
         for (int i = stacks.length - 1; i >= 0; i--) {
-            ItemStack stack = stacks[i];
+            EntryStack stack = stacks[i];
             if (stack != null && !stack.isEmpty())
-                registerItemStack(afterItem, stack);
+                registerEntryAfter(afterStack, stack);
         }
     }
     
+    @ToBeRemoved
+    @Deprecated
+    default void registerItemStack(ItemStack... stacks) {
+        registerItemStack(null, stacks);
+    }
+    
     /**
      * Registers multiple stacks to the item list
      *
      * @param stacks the stacks to register
      */
-    default void registerItemStack(ItemStack... stacks) {
-        registerItemStack(null, stacks);
+    default void registerEntries(EntryStack... stacks) {
+        registerEntriesAfter(null, stacks);
+    }
+    
+    @ToBeRemoved
+    @Deprecated
+    default boolean alreadyContain(ItemStack stack) {
+        return alreadyContain(EntryStack.create(stack));
     }
     
     /**
@@ -75,8 +125,8 @@ public interface EntryRegistry {
      * @param stack the stack to check
      * @return whether the stack has been registered
      */
-    default boolean alreadyContain(ItemStack stack) {
-        return getEntryList().stream().filter(entry -> entry.getEntryType() == Entry.Type.ITEM).anyMatch(entry -> ItemStack.areEqualIgnoreDamage(stack, entry.getItemStack()));
+    default boolean alreadyContain(EntryStack stack) {
+        return CollectionUtils.anyMatchEqualsAll(getStacksList(), stack);
     }
     
 }

+ 153 - 0
src/main/java/me/shedaniel/rei/api/EntryStack.java

@@ -0,0 +1,153 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.api;
+
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import me.shedaniel.rei.impl.EmptyEntryStack;
+import me.shedaniel.rei.impl.FluidEntryStack;
+import me.shedaniel.rei.impl.ItemEntryStack;
+import net.minecraft.block.Block;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemConvertible;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public interface EntryStack {
+    
+    static EntryStack empty() {
+        return EmptyEntryStack.EMPTY;
+    }
+    
+    static EntryStack create(Fluid fluid) {
+        return create(fluid, 1000);
+    }
+    
+    static EntryStack create(Fluid fluid, int amount) {
+        return new FluidEntryStack(fluid, amount);
+    }
+    
+    static EntryStack create(ItemStack stack) {
+        return new ItemEntryStack(stack);
+    }
+    
+    static EntryStack create(ItemConvertible item) {
+        return new ItemEntryStack(new ItemStack(item));
+    }
+    
+    static EntryStack create(Block block) {
+        return new ItemEntryStack(new ItemStack(block));
+    }
+    
+    Optional<Identifier> getIdentifier();
+    
+    EntryStack.Type getType();
+    
+    int getAmount();
+    
+    void setAmount(int amount);
+    
+    boolean isEmpty();
+    
+    @ToBeRemoved
+    @Deprecated
+    Entry toEntry();
+    
+    EntryStack copy();
+    
+    Object getObject();
+    
+    boolean equals(EntryStack stack, boolean ignoreTags, boolean ignoreAmount);
+    
+    boolean equalsIgnoreTagsAndAmount(EntryStack stack);
+    
+    boolean equalsIgnoreTags(EntryStack stack);
+    
+    boolean equalsIgnoreAmount(EntryStack stack);
+    
+    boolean equalsAll(EntryStack stack);
+    
+    int getZ();
+    
+    void setZ(int z);
+    
+    default ItemStack getItemStack() {
+        if (getType() == Type.ITEM)
+            return (ItemStack) getObject();
+        return null;
+    }
+    
+    default Item getItem() {
+        if (getType() == Type.ITEM)
+            return ((ItemStack) getObject()).getItem();
+        return null;
+    }
+    
+    default Fluid getFluid() {
+        if (getType() == Type.FLUID)
+            return (Fluid) getObject();
+        return null;
+    }
+    
+    <T> EntryStack setting(Settings<T> settings, T value);
+    
+    <T> EntryStack removeSetting(Settings<T> settings);
+    
+    EntryStack clearSettings();
+    
+    default <T> EntryStack addSetting(Settings<T> settings, T value) {
+        return setting(settings, value);
+    }
+    
+    <T> ObjectHolder<T> getSetting(Settings<T> settings);
+    
+    @Nullable
+    QueuedTooltip getTooltip(int mouseX, int mouseY);
+    
+    void render(Rectangle bounds, int mouseX, int mouseY, float delta);
+    
+    public static enum Type {
+        ITEM, FLUID, EMPTY
+    }
+    
+    public static class Settings<T> {
+        public static final Supplier<Boolean> TRUE = () -> true;
+        public static final Supplier<Boolean> FALSE = () -> false;
+        public static final Settings<Supplier<Boolean>> RENDER = new Settings(TRUE);
+        public static final Settings<Supplier<Boolean>> CHECK_TAGS = new Settings(FALSE);
+        public static final Settings<Supplier<Boolean>> TOOLTIP_ENABLED = new Settings(TRUE);
+        public static final Settings<Supplier<Boolean>> TOOLTIP_APPEND_MOD = new Settings(TRUE);
+        public static final Settings<Supplier<Boolean>> RENDER_COUNTS = new Settings(TRUE);
+        public static final Settings<Function<EntryStack, List<String>>> TOOLTIP_APPEND_EXTRA = new Settings<Function<EntryStack, List<String>>>(stack -> Collections.emptyList());
+        public static final Settings<Function<EntryStack, String>> COUNTS = new Settings<Function<EntryStack, String>>(stack -> null);
+        
+        private T defaultValue;
+        
+        public Settings(T defaultValue) {
+            this.defaultValue = defaultValue;
+        }
+        
+        public T getDefaultValue() {
+            return defaultValue;
+        }
+        
+        public static class Item {
+            public static final Settings<Supplier<Boolean>> RENDER_OVERLAY = new Settings(TRUE);
+            
+            private Item() {
+            }
+        }
+    }
+}

+ 16 - 2
src/main/java/me/shedaniel/rei/api/LiveRecipeGenerator.java

@@ -15,8 +15,22 @@ public interface LiveRecipeGenerator<T extends RecipeDisplay> {
     
     Identifier getCategoryIdentifier();
     
-    Optional<List<T>> getRecipeFor(ItemStack stack);
+    @Deprecated
+    default Optional<List<T>> getRecipeFor(ItemStack stack) {
+        return Optional.empty();
+    }
     
-    Optional<List<T>> getUsageFor(ItemStack stack);
+    default Optional<List<T>> getRecipeFor(EntryStack entry) {
+        return Optional.empty();
+    }
+    
+    @Deprecated
+    default Optional<List<T>> getUsageFor(ItemStack stack) {
+        return Optional.empty();
+    }
+    
+    default Optional<List<T>> getUsageFor(EntryStack entry) {
+        return Optional.empty();
+    }
     
 }

+ 22 - 0
src/main/java/me/shedaniel/rei/api/ObjectHolder.java

@@ -0,0 +1,22 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.api;
+
+public interface ObjectHolder<T> {
+    int intValue();
+    
+    long longValue();
+    
+    boolean booleanValue();
+    
+    float floatValue();
+    
+    double doubleValue();
+    
+    String stringValue();
+    
+    T value();
+}

+ 14 - 2
src/main/java/me/shedaniel/rei/api/RecipeCategory.java

@@ -6,6 +6,7 @@
 package me.shedaniel.rei.api;
 
 import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import me.shedaniel.rei.gui.renderers.RecipeRenderer;
 import me.shedaniel.rei.gui.widget.CategoryBaseWidget;
@@ -32,9 +33,18 @@ public interface RecipeCategory<T extends RecipeDisplay> {
     /**
      * Gets the renderer of the icon, allowing developers to render things other than items
      *
+     * @see RecipeCategory#getLogo()
      * @return the renderer of the icon
      */
-    Renderer getIcon();
+    @ToBeRemoved
+    @Deprecated
+    default Renderer getIcon() {
+        return Renderer.empty();
+    }
+    
+    default EntryStack getLogo() {
+        return getIcon().getEntry();
+    }
     
     /**
      * Gets the category name
@@ -51,7 +61,7 @@ public interface RecipeCategory<T extends RecipeDisplay> {
      */
     @SuppressWarnings("unchecked")
     default RecipeRenderer getSimpleRenderer(T recipe) {
-        return Renderer.fromRecipe(recipe::getInput, recipe::getOutput);
+        return Renderer.fromRecipeEntries(recipe::getInputEntries, recipe::getOutputEntries);
     }
     
     /**
@@ -126,7 +136,9 @@ public interface RecipeCategory<T extends RecipeDisplay> {
      * Gets whether the category will check tags, useful for potions
      *
      * @return whether the category will check tags
+     * @deprecated no longer used
      */
+    @Deprecated
     default boolean checkTags() {
         return false;
     }

+ 64 - 4
src/main/java/me/shedaniel/rei/api/RecipeDisplay.java

@@ -5,10 +5,12 @@
 
 package me.shedaniel.rei.api;
 
-import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.Identifier;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 
@@ -16,21 +18,79 @@ public interface RecipeDisplay {
     
     /**
      * @return a list of items
+     * @see RecipeDisplay#getInputStacks()
      */
-    List<List<ItemStack>> getInput();
+    @ToBeRemoved
+    @Deprecated
+    default List<List<ItemStack>> getInput() {
+        return Collections.emptyList();
+    }
+    
+    /**
+     * @return a list of inputs
+     */
+    default List<List<EntryStack>> getInputEntries() {
+        List<List<ItemStack>> input = getInput();
+        if (input.isEmpty())
+            return Collections.emptyList();
+        List<List<EntryStack>> list = new ArrayList<>();
+        for (List<ItemStack> stacks : input) {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : stacks) {
+                entries.add(EntryStack.create(stack));
+            }
+            list.add(entries);
+        }
+        return list;
+    }
     
     /**
      * @return a list of outputs
      */
-    List<ItemStack> getOutput();
+    @ToBeRemoved
+    @Deprecated
+    default List<ItemStack> getOutput() {
+        return Collections.emptyList();
+    }
+    
+    /**
+     * @return a list of outputs
+     */
+    default List<EntryStack> getOutputEntries() {
+        List<ItemStack> input = getOutput();
+        if (input.isEmpty())
+            return Collections.emptyList();
+        List<EntryStack> entries = new ArrayList<>();
+        for (ItemStack stack : input) {
+            entries.add(EntryStack.create(stack));
+        }
+        return entries;
+    }
     
     /**
      * Gets the required items used in craftable filters
      *
      * @return the list of required items
      */
+    default List<List<EntryStack>> getRequiredEntries() {
+        List<List<ItemStack>> input = getRequiredItems();
+        if (input.isEmpty())
+            return Collections.emptyList();
+        List<List<EntryStack>> list = new ArrayList<>();
+        for (List<ItemStack> stacks : input) {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : stacks) {
+                entries.add(EntryStack.create(stack));
+            }
+            list.add(entries);
+        }
+        return list;
+    }
+    
+    @ToBeRemoved
+    @Deprecated
     default List<List<ItemStack>> getRequiredItems() {
-        return Lists.newArrayList();
+        return Collections.emptyList();
     }
     
     /**

+ 32 - 3
src/main/java/me/shedaniel/rei/api/RecipeHelper.java

@@ -7,12 +7,14 @@ package me.shedaniel.rei.api;
 
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import net.minecraft.client.gui.screen.ingame.AbstractContainerScreen;
 import net.minecraft.item.ItemStack;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.recipe.RecipeManager;
 import net.minecraft.util.Identifier;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -50,7 +52,22 @@ public interface RecipeHelper {
      * @param inventoryItems the materials
      * @return the list of craftable items
      */
-    List<ItemStack> findCraftableByItems(List<ItemStack> inventoryItems);
+    default List<ItemStack> findCraftableByItems(List<ItemStack> inventoryItems) {
+        List<ItemStack> itemStacks = new ArrayList<>();
+        for (EntryStack item : findCraftableEntriesByItems(inventoryItems)) {
+            if (item.getItemStack() != null)
+                itemStacks.add(item.getItemStack());
+        }
+        return itemStacks;
+    }
+    
+    /**
+     * Gets all craftable items from materials.
+     *
+     * @param inventoryItems the materials
+     * @return the list of craftable entries
+     */
+    List<EntryStack> findCraftableEntriesByItems(List<ItemStack> inventoryItems);
     
     /**
      * Registers a category
@@ -91,7 +108,13 @@ public interface RecipeHelper {
      * @param stack the stack to be crafted
      * @return the map of recipes
      */
-    Map<RecipeCategory<?>, List<RecipeDisplay>> getRecipesFor(ItemStack stack);
+    Map<RecipeCategory<?>, List<RecipeDisplay>> getRecipesFor(EntryStack stack);
+    
+    @ToBeRemoved
+    @Deprecated
+    default Map<RecipeCategory<?>, List<RecipeDisplay>> getRecipesFor(ItemStack stack) {
+        return getRecipesFor(EntryStack.create(stack));
+    }
     
     RecipeCategory getCategory(Identifier identifier);
     
@@ -115,7 +138,13 @@ public interface RecipeHelper {
      * @param stack the stack to be used
      * @return the map of recipes
      */
-    Map<RecipeCategory<?>, List<RecipeDisplay>> getUsagesFor(ItemStack stack);
+    Map<RecipeCategory<?>, List<RecipeDisplay>> getUsagesFor(EntryStack stack);
+    
+    @ToBeRemoved
+    @Deprecated
+    default Map<RecipeCategory<?>, List<RecipeDisplay>> getUsagesFor(ItemStack stack) {
+        return getUsagesFor(EntryStack.create(stack));
+    }
     
     /**
      * Gets the optional of the speed crafting button area from a category

+ 18 - 0
src/main/java/me/shedaniel/rei/api/Renderer.java

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.api;
 
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import me.shedaniel.rei.gui.renderers.EmptyRenderer;
 import me.shedaniel.rei.gui.renderers.FluidRenderer;
 import me.shedaniel.rei.gui.renderers.ItemStackRenderer;
@@ -14,14 +15,17 @@ import net.minecraft.client.gui.DrawableHelper;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.NotNull;
 
 import javax.annotation.Nullable;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
 
+@Deprecated
 public abstract class Renderer extends DrawableHelper {
     /**
      * Gets an item stack renderer by an item stack supplier
@@ -102,10 +106,16 @@ public abstract class Renderer extends DrawableHelper {
      * @param output the list of output items
      * @return the recipe renderer
      */
+    @ToBeRemoved
+    @Deprecated
     public static SimpleRecipeRenderer fromRecipe(Supplier<List<List<ItemStack>>> input, Supplier<List<ItemStack>> output) {
         return new SimpleRecipeRenderer(input, output);
     }
     
+    public static SimpleRecipeRenderer fromRecipeEntries(Supplier<List<List<EntryStack>>> input, Supplier<List<EntryStack>> output) {
+        return new SimpleRecipeRenderer(input, output, 0);
+    }
+    
     public static ItemStackRenderer fromItemStacks(List<ItemStack> stacks) {
         return fromItemStacks(() -> stacks, true, null);
     }
@@ -185,6 +195,14 @@ public abstract class Renderer extends DrawableHelper {
      */
     public abstract void render(int x, int y, double mouseX, double mouseY, float delta);
     
+    public EntryStack getEntry() {
+        if (this instanceof ItemStackRenderer)
+            return EntryStack.create(((ItemStackRenderer) this).getItemStack());
+        if (this instanceof FluidRenderer)
+            return EntryStack.create(((FluidRenderer) this).getFluid());
+        return EntryStack.empty();
+    }
+    
     @Nullable
     public QueuedTooltip getQueuedTooltip(float delta) {
         return null;

+ 25 - 1
src/main/java/me/shedaniel/rei/api/TransferRecipeDisplay.java

@@ -5,10 +5,13 @@
 
 package me.shedaniel.rei.api;
 
+import com.google.common.collect.Lists;
 import me.shedaniel.rei.server.ContainerInfo;
 import net.minecraft.container.Container;
 import net.minecraft.item.ItemStack;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 public interface TransferRecipeDisplay extends RecipeDisplay {
@@ -17,6 +20,27 @@ public interface TransferRecipeDisplay extends RecipeDisplay {
     
     int getHeight();
     
-    List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container);
+    default List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container) {
+        List<List<ItemStack>> list = Lists.newArrayListWithCapacity(containerInfo.getCraftingWidth(container) * containerInfo.getCraftingHeight(container));
+        for (int i = 0; i < containerInfo.getCraftingWidth(container) * containerInfo.getCraftingHeight(container); i++) {
+            list.add(Lists.newArrayList());
+        }
+        return list;
+    }
+    
+    default List<List<EntryStack>> getOrganisedInputEntries(ContainerInfo<Container> containerInfo, Container container) {
+        List<List<ItemStack>> input = getOrganisedInput(containerInfo, container);
+        if (input.isEmpty())
+            return Collections.emptyList();
+        List<List<EntryStack>> list = new ArrayList<>();
+        for (List<ItemStack> stacks : input) {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : stacks) {
+                entries.add(EntryStack.create(stack));
+            }
+            list.add(entries);
+        }
+        return list;
+    }
     
 }

+ 17 - 0
src/main/java/me/shedaniel/rei/api/annotations/Experimental.java

@@ -0,0 +1,17 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.api.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
+public @interface Experimental {
+}

+ 17 - 0
src/main/java/me/shedaniel/rei/api/annotations/Internal.java

@@ -0,0 +1,17 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.api.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
+public @interface Internal {
+}

+ 17 - 0
src/main/java/me/shedaniel/rei/api/annotations/ToBeRemoved.java

@@ -0,0 +1,17 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.api.annotations;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(value = {CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
+public @interface ToBeRemoved {
+}

+ 11 - 8
src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -13,7 +13,7 @@ import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.ClientHelper;
 import me.shedaniel.rei.api.DisplayHelper;
-import me.shedaniel.rei.api.Entry;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeHelper;
 import me.shedaniel.rei.gui.config.SearchFieldLocation;
 import me.shedaniel.rei.gui.widget.*;
@@ -21,6 +21,7 @@ import me.shedaniel.rei.impl.RecipeHelperImpl;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.Weather;
 import me.shedaniel.rei.listeners.ContainerScreenHooks;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
@@ -46,7 +47,6 @@ import org.apache.logging.log4j.util.TriConsumer;
 
 import javax.annotation.Nullable;
 import java.util.*;
-import java.util.stream.Collectors;
 
 public class ContainerScreenOverlay extends Widget {
     
@@ -310,7 +310,7 @@ public class ContainerScreenOverlay extends Widget {
             buttonLeft.enabled = buttonRight.enabled = getTotalPage() > 0;
         }
         if (ScreenHelper.searchField == null)
-            ScreenHelper.searchField = new SearchFieldWidget(0, 0, 0, 0);
+            ScreenHelper.setSearchField(new OverlaySearchField(0, 0, 0, 0));
         ScreenHelper.searchField.getBounds().setBounds(getTextFieldArea());
         this.widgets.add(ScreenHelper.searchField);
         ScreenHelper.searchField.setText(searchTerm);
@@ -436,14 +436,14 @@ public class ContainerScreenOverlay extends Widget {
             DisplayHelper.DisplayBoundsHandler<?> boundsHandler = RoughlyEnoughItemsCore.getDisplayHelper().getResponsibleBoundsHandler(MinecraftClient.getInstance().currentScreen.getClass());
             entryListWidget.updateList(boundsHandler, boundsHandler.getItemListArea(rectangle), page, searchTerm, true);
         }
-        if (SearchFieldWidget.isSearching) {
+        if (OverlaySearchField.isSearching) {
             GuiLighting.disable();
             setBlitOffset(200);
             if (MinecraftClient.getInstance().currentScreen instanceof AbstractContainerScreen) {
                 ContainerScreenHooks hooks = (ContainerScreenHooks) MinecraftClient.getInstance().currentScreen;
                 int left = hooks.rei_getContainerLeft(), top = hooks.rei_getContainerTop();
                 for (Slot slot : ((AbstractContainerScreen<?>) MinecraftClient.getInstance().currentScreen).getContainer().slotList)
-                    if (!slot.hasStack() || !entryListWidget.filterEntry(Entry.create(slot.getStack()), entryListWidget.getLastSearchArgument()))
+                    if (!slot.hasStack() || !entryListWidget.filterEntry(EntryStack.create(slot.getStack()), entryListWidget.getLastSearchArgument()))
                         fillGradient(left + slot.xPosition, top + slot.yPosition, left + slot.xPosition + 16, top + slot.yPosition + 16, -601874400, -601874400);
             }
             setBlitOffset(0);
@@ -456,7 +456,7 @@ public class ContainerScreenOverlay extends Widget {
             for (RecipeHelperImpl.ScreenClickArea area : RecipeHelper.getInstance().getScreenClickAreas())
                 if (area.getScreenClass().equals(MinecraftClient.getInstance().currentScreen.getClass()))
                     if (area.getRectangle().contains(mouseX - hooks.rei_getContainerLeft(), mouseY - hooks.rei_getContainerTop())) {
-                        String collect = Arrays.asList(area.getCategories()).stream().map(identifier -> RecipeHelper.getInstance().getCategory(identifier).getCategoryName()).collect(Collectors.joining(", "));
+                        String collect = CollectionUtils.mapAndJoinToString(area.getCategories(), identifier -> RecipeHelper.getInstance().getCategory(identifier).getCategoryName(), ", ");
                         QUEUED_TOOLTIPS.add(QueuedTooltip.create(I18n.translate("text.rei.view_recipes_for", collect)));
                         break;
                     }
@@ -470,7 +470,10 @@ public class ContainerScreenOverlay extends Widget {
                 toggleButtonWidget.lateRender(mouseX, mouseY, delta);
             Screen currentScreen = MinecraftClient.getInstance().currentScreen;
             if (!(currentScreen instanceof RecipeViewingScreen) || !((RecipeViewingScreen) currentScreen).choosePageActivated)
-                QUEUED_TOOLTIPS.stream().filter(Objects::nonNull).forEach(this::renderTooltip);
+                for (QueuedTooltip queuedTooltip : QUEUED_TOOLTIPS) {
+                    if (queuedTooltip != null)
+                        renderTooltip(queuedTooltip);
+                }
         }
         QUEUED_TOOLTIPS.clear();
     }
@@ -495,7 +498,7 @@ public class ContainerScreenOverlay extends Widget {
     private boolean hasSameListContent(List<ItemStack> list1, List<ItemStack> list2) {
         list1.sort(Comparator.comparing(Object::toString));
         list2.sort(Comparator.comparing(Object::toString));
-        return list1.stream().map(Objects::toString).collect(Collectors.joining("")).equals(list2.stream().map(Objects::toString).collect(Collectors.joining("")));
+        return CollectionUtils.mapAndJoinToString(list1, Object::toString, "").equals(CollectionUtils.mapAndJoinToString(list2, Object::toString, ""));
     }
     
     public void addTooltip(@Nullable QueuedTooltip queuedTooltip) {

+ 8 - 9
src/main/java/me/shedaniel/rei/gui/widget/SearchFieldWidget.java → src/main/java/me/shedaniel/rei/gui/OverlaySearchField.java

@@ -3,11 +3,10 @@
  * Licensed under the MIT License.
  */
 
-package me.shedaniel.rei.gui.widget;
+package me.shedaniel.rei.gui;
 
-import com.mojang.blaze3d.systems.RenderSystem;
-import me.shedaniel.rei.RoughlyEnoughItemsCore;
-import me.shedaniel.rei.gui.ContainerScreenOverlay;
+import me.shedaniel.math.compat.RenderHelper;
+import me.shedaniel.rei.gui.widget.TextFieldWidget;
 import me.shedaniel.rei.impl.ScreenHelper;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.render.GuiLighting;
@@ -16,25 +15,25 @@ import net.minecraft.client.sound.PositionedSoundInstance;
 import net.minecraft.client.util.InputUtil;
 import net.minecraft.sound.SoundEvents;
 
-public class SearchFieldWidget extends TextFieldWidget {
+public class OverlaySearchField extends TextFieldWidget {
     
     public static boolean isSearching = false;
     public long keybindFocusTime = -1;
     public int keybindFocusKey = -1;
     protected long lastClickedTime = -1;
     
-    public SearchFieldWidget(int x, int y, int width, int height) {
+    OverlaySearchField(int x, int y, int width, int height) {
         super(x, y, width, height);
         setMaxLength(10000);
     }
     
     public void laterRender(int int_1, int int_2, float float_1) {
         GuiLighting.disable();
-        RenderSystem.disableDepthTest();
+        RenderHelper.disableDepthTest();
         setEditableColor(ContainerScreenOverlay.getEntryListWidget().children().isEmpty() && !getText().isEmpty() ? 16733525 : isSearching ? -1313241 : 14737632);
         setSuggestion(!isFocused() && getText().isEmpty() ? I18n.translate("text.rei.search.field.suggestion") : null);
         super.render(int_1, int_2, float_1);
-        RenderSystem.enableDepthTest();
+        RenderHelper.enableDepthTest();
     }
     
     @Override
@@ -77,7 +76,7 @@ public class SearchFieldWidget extends TextFieldWidget {
     
     @Override
     public boolean charTyped(char char_1, int int_1) {
-        if (System.currentTimeMillis() - keybindFocusTime < 1000 && InputUtil.isKeyPressed(MinecraftClient.getInstance().getWindow().getHandle(), keybindFocusKey)) {
+        if (System.currentTimeMillis() - keybindFocusTime < 1000 && InputUtil.isKeyPressed(MinecraftClient.getInstance().window.getHandle(), keybindFocusKey)) {
             keybindFocusTime = -1;
             keybindFocusKey = -1;
             return true;

+ 12 - 4
src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -13,6 +13,7 @@ import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.gui.widget.*;
 import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
@@ -123,7 +124,8 @@ public class RecipeViewingScreen extends Screen {
         this.widgets.clear();
         this.largestWidth = width - 100;
         this.largestHeight = height - 40;
-        this.guiWidth = MathHelper.clamp(getCurrentDisplayed().stream().map(display -> selectedCategory.getDisplayWidth(display)).max(Integer::compareTo).orElse(150) + 30, 0, largestWidth);
+        int maxWidthDisplay = CollectionUtils.mapAndMax(getCurrentDisplayed(), display -> selectedCategory.getDisplayWidth((RecipeDisplay) display), (Comparator<Integer>) Comparator.naturalOrder()).orElse(150);
+        this.guiWidth = MathHelper.clamp(maxWidthDisplay, 0, largestWidth);
         this.guiHeight = MathHelper.floor(MathHelper.clamp((selectedCategory.getDisplayHeight() + 7d) * (getRecipesPerPage() + 1d) + 40d, 186d, (double) largestHeight));
         this.bounds = new Rectangle(width / 2 - guiWidth / 2, height / 2 - guiHeight / 2, guiWidth, guiHeight);
         this.page = MathHelper.clamp(page, 0, getTotalPages(selectedCategory) - 1);
@@ -272,7 +274,7 @@ public class RecipeViewingScreen extends Screen {
                         return false;
                     }
                 });
-                tab.setRenderer(categories.get(j), categories.get(j).getIcon(), categories.get(j).getCategoryName(), tab.getId() + categoryPages * TABS_PER_PAGE == categories.indexOf(selectedCategory));
+                tab.setRenderer(categories.get(j), categories.get(j).getLogo(), categories.get(j).getCategoryName(), tab.getId() + categoryPages * TABS_PER_PAGE == categories.indexOf(selectedCategory));
             }
         }
         Optional<ButtonAreaSupplier> supplier = RecipeHelper.getInstance().getAutoCraftButtonArea(selectedCategory);
@@ -379,7 +381,10 @@ public class RecipeViewingScreen extends Screen {
                 fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, 0xFF9E9E9E);
             }
         }
-        tabs.stream().filter(tabWidget -> !tabWidget.isSelected()).forEach(tabWidget -> tabWidget.render(mouseX, mouseY, delta));
+        for (TabWidget tab : tabs) {
+            if (!tab.isSelected())
+                tab.render(mouseX, mouseY, delta);
+        }
         GuiLighting.disable();
         super.render(mouseX, mouseY, delta);
         widgets.forEach(widget -> {
@@ -388,7 +393,10 @@ public class RecipeViewingScreen extends Screen {
         });
         RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
         GuiLighting.disable();
-        tabs.stream().filter(TabWidget::isSelected).forEach(tabWidget -> tabWidget.render(mouseX, mouseY, delta));
+        for (TabWidget tab : tabs) {
+            if (tab.isSelected())
+                tab.render(mouseX, mouseY, delta);
+        }
         GuiLighting.disable();
         ScreenHelper.getLastOverlay().render(mouseX, mouseY, delta);
         ScreenHelper.getLastOverlay().lateRender(mouseX, mouseY, delta);

+ 9 - 10
src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -7,8 +7,8 @@ package me.shedaniel.rei.gui;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
 import com.mojang.blaze3d.systems.RenderSystem;
-import com.zeitheron.hammercore.client.utils.Scissors;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
@@ -17,6 +17,7 @@ import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.gui.renderers.RecipeRenderer;
 import me.shedaniel.rei.gui.widget.*;
 import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
@@ -37,7 +38,6 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.stream.Collectors;
 
 public class VillagerRecipeViewingScreen extends Screen {
     
@@ -185,7 +185,7 @@ public class VillagerRecipeViewingScreen extends Screen {
                         return false;
                     }
                 });
-                tab.setRenderer(categories.get(j), categories.get(j).getIcon(), categories.get(j).getCategoryName(), tab.getId() + tabsPage * TABS_PER_PAGE == selectedCategoryIndex);
+                tab.setRenderer(categories.get(j), categories.get(j).getLogo(), categories.get(j).getCategoryName(), tab.getId() + tabsPage * TABS_PER_PAGE == selectedCategoryIndex);
             }
         }
         ButtonWidget w, w2;
@@ -247,7 +247,7 @@ public class VillagerRecipeViewingScreen extends Screen {
     
     @Override
     public boolean mouseClicked(double mouseX, double mouseY, int int_1) {
-        double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+        double height = CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
         int actualHeight = scrollListBounds.height - 2;
         if (height > actualHeight && scrollBarAlpha > 0 && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
             double scrollbarPositionMinX = scrollListBounds.getMaxX() - 6;
@@ -271,7 +271,7 @@ public class VillagerRecipeViewingScreen extends Screen {
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+        double height = CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
         if (scrollListBounds.contains(double_1, double_2) && height > scrollListBounds.height - 2) {
             if (double_3 > 0)
                 scroll -= 16;
@@ -337,8 +337,7 @@ public class VillagerRecipeViewingScreen extends Screen {
         GuiLighting.disable();
         ScreenHelper.getLastOverlay().render(mouseX, mouseY, delta);
         RenderSystem.pushMatrix();
-        Scissors.begin();
-        Scissors.scissor(0, scrollListBounds.y + 1, width, scrollListBounds.height - 2);
+        ScissorsHandler.INSTANCE.scissor(new Rectangle(0, scrollListBounds.y + 1, width, scrollListBounds.height - 2));
         for (int i = 0; i < buttonWidgets.size(); i++) {
             ButtonWidget buttonWidget = buttonWidgets.get(i);
             buttonWidget.getBounds().y = scrollListBounds.y + 1 + yOffset - (int) scroll;
@@ -356,7 +355,7 @@ public class VillagerRecipeViewingScreen extends Screen {
                 ScreenHelper.getLastOverlay().addTooltip(recipeRenderers.get(i).getQueuedTooltip(delta));
             }
         }
-        double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+        double height = CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
         if (height > scrollListBounds.height - 2) {
             Tessellator tessellator = Tessellator.getInstance();
             BufferBuilder buffer = tessellator.getBufferBuilder();
@@ -385,7 +384,7 @@ public class VillagerRecipeViewingScreen extends Screen {
             RenderSystem.enableAlphaTest();
             RenderSystem.enableTexture();
         }
-        Scissors.end();
+        ScissorsHandler.INSTANCE.removeLastScissor();
         RenderSystem.popMatrix();
         ScreenHelper.getLastOverlay().lateRender(mouseX, mouseY, delta);
     }
@@ -393,7 +392,7 @@ public class VillagerRecipeViewingScreen extends Screen {
     @Override
     public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
         if (int_1 == 0 && scrollBarAlpha > 0 && draggingScrollBar) {
-            double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+            double height = CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
             int actualHeight = scrollListBounds.height - 2;
             if (height > actualHeight && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
                 int int_3 = MathHelper.clamp((int) ((actualHeight * actualHeight) / height), 32, actualHeight - 8);

+ 1 - 1
src/main/java/me/shedaniel/rei/gui/credits/CreditsScreen.java

@@ -83,7 +83,7 @@ public class CreditsScreen extends Screen {
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        if (entryListWidget.mouseScrolled(double_1, double_2, double_3 * 3))
+        if (entryListWidget.mouseScrolled(double_1, double_2, double_3))
             return true;
         return super.mouseScrolled(double_1, double_2, double_3);
     }

+ 68 - 26
src/main/java/me/shedaniel/rei/gui/renderers/SimpleRecipeRenderer.java

@@ -6,11 +6,13 @@
 package me.shedaniel.rei.gui.renderers;
 
 import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.Renderer;
 import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
 import me.shedaniel.rei.gui.widget.QueuedTooltip;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.render.GuiLighting;
+import net.minecraft.fluid.Fluid;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.Pair;
@@ -26,54 +28,94 @@ import java.util.stream.Collectors;
 
 public class SimpleRecipeRenderer extends RecipeRenderer {
     
-    public static final Comparator<ItemStack> ITEM_STACK_COMPARATOR = (o1, o2) -> {
-        if (o1.getItem() == o2.getItem()) {
-            if (o1.getCount() != o2.getCount())
-                return o1.getCount() - o2.getCount();
-            int compare = Boolean.compare(o1.hasTag(), o2.hasTag());
+    private static final Comparator<EntryStack> ENTRY_COMPARATOR = (o1, o2) -> {
+        if (o1.getType() == EntryStack.Type.FLUID) {
+            if (o2.getType() == EntryStack.Type.ITEM)
+                return -1;
+            return o1.getFluid().hashCode() - o2.getFluid().hashCode();
+        } else if (o2.getType() == EntryStack.Type.FLUID) {
+            if (o1.getType() == EntryStack.Type.ITEM)
+                return 1;
+            return o1.getFluid().hashCode() - o2.getFluid().hashCode();
+        }
+        ItemStack i1 = o1.getItemStack();
+        ItemStack i2 = o2.getItemStack();
+        if (i1.getItem() == i2.getItem()) {
+            if (i1.getCount() != i2.getCount())
+                return i1.getCount() - i2.getCount();
+            int compare = Boolean.compare(i1.hasTag(), i2.hasTag());
             if (compare != 0)
                 return compare;
-            if (o1.getTag().getSize() != o2.getTag().getSize())
-                return o1.getTag().getSize() - o2.getTag().getSize();
-            return o1.getTag().hashCode() - o2.getTag().hashCode();
+            if (i1.getTag().getSize() != i2.getTag().getSize())
+                return i1.getTag().getSize() - i2.getTag().getSize();
+            return i1.getTag().hashCode() - i2.getTag().hashCode();
         }
-        return o1.getItem().hashCode() - o2.getItem().hashCode();
+        return i1.getItem().hashCode() - i2.getItem().hashCode();
     };
     private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
-    private List<ItemStackRenderer> inputRenderer;
-    private ItemStackRenderer outputRenderer;
+    private List<Renderer> inputRenderer;
+    private Renderer outputRenderer;
     private QueuedTooltip lastTooltip;
     
+    @Deprecated
     public SimpleRecipeRenderer(Supplier<List<List<ItemStack>>> input, Supplier<List<ItemStack>> output) {
-        List<Pair<List<ItemStack>, AtomicInteger>> newList = Lists.newArrayList();
-        List<Pair<List<ItemStack>, Integer>> a = input.get().stream().map(stacks -> new Pair<>(stacks, stacks.stream().map(ItemStack::getCount).max(Integer::compareTo).orElse(1))).collect(Collectors.toList());
-        for (Pair<List<ItemStack>, Integer> pair : a) {
-            Optional<Pair<List<ItemStack>, AtomicInteger>> any = newList.stream().filter(pairr -> equalsList(pair.getLeft(), pairr.getLeft())).findAny();
+        this(() -> (List<List<EntryStack>>) input.get().stream().map(s -> s.stream().map(EntryStack::create).collect(Collectors.toList())).collect(Collectors.toList()),
+                () -> output.get().stream().map(EntryStack::create).collect(Collectors.toList()), 0);
+    }
+    
+    public SimpleRecipeRenderer(Supplier<List<List<EntryStack>>> input, Supplier<List<EntryStack>> output, int forDifferentConstructor) {
+        List<Pair<List<EntryStack>, AtomicInteger>> newList = Lists.newArrayList();
+        List<Pair<List<EntryStack>, Integer>> a = input.get().stream().map(stacks -> new Pair<>(stacks, stacks.stream().map(EntryStack::getAmount).max(Integer::compareTo).orElse(1))).collect(Collectors.toList());
+        for (Pair<List<EntryStack>, Integer> pair : a) {
+            Optional<Pair<List<EntryStack>, AtomicInteger>> any = newList.stream().filter(pairr -> equalsList(pair.getLeft(), pairr.getLeft())).findAny();
             if (any.isPresent()) {
                 any.get().getRight().addAndGet(pair.getRight());
             } else
                 newList.add(new Pair<>(pair.getLeft(), new AtomicInteger(pair.getRight())));
         }
-        List<List<ItemStack>> b = Lists.newArrayList();
-        for (Pair<List<ItemStack>, AtomicInteger> pair : newList)
+        List<List<EntryStack>> b = Lists.newArrayList();
+        for (Pair<List<EntryStack>, AtomicInteger> pair : newList)
             b.add(pair.getLeft().stream().map(stack -> {
-                ItemStack s = stack.copy();
-                s.setCount(pair.getRight().get());
+                EntryStack s = stack.copy();
+                s.setAmount(pair.getRight().get());
                 return s;
             }).collect(Collectors.toList()));
-        this.inputRenderer = b.stream().filter(stacks -> !stacks.isEmpty()).map(stacks -> Renderer.fromItemStacks(stacks)).collect(Collectors.toList());
-        this.outputRenderer = Renderer.fromItemStacks(output.get().stream().filter(stack -> !stack.isEmpty()).collect(Collectors.toList()));
+        this.inputRenderer = b.stream().filter(stacks -> !stacks.isEmpty()).map(stacks -> fromEntries(stacks)).collect(Collectors.toList());
+        this.outputRenderer = fromEntries(output.get().stream().filter(stack -> !stack.isEmpty()).collect(Collectors.toList()));
+    }
+    
+    @Deprecated
+    private static Renderer fromEntries(List<EntryStack> entries) {
+        boolean isItem = true;
+        for (EntryStack entry : entries) {
+            if (entry.getType() != EntryStack.Type.ITEM)
+                isItem = false;
+        }
+        if (isItem)
+            return Renderer.fromItemStacks(entries.stream().map(EntryStack::getItemStack).collect(Collectors.toList()));
+        boolean isFluid = true;
+        for (EntryStack entry : entries) {
+            if (entry.getType() != EntryStack.Type.FLUID)
+                isFluid = false;
+        }
+        
+        if (isFluid) {
+            List<Fluid> fluids = entries.stream().map(EntryStack::getFluid).collect(Collectors.toList());
+            if (!fluids.isEmpty())
+                return Renderer.fromFluid(fluids.get(0));
+        }
+        return Renderer.empty();
     }
     
-    public static boolean equalsList(List<ItemStack> list_1, List<ItemStack> list_2) {
-        List<ItemStack> stacks_1 = list_1.stream().distinct().sorted(ITEM_STACK_COMPARATOR).collect(Collectors.toList());
-        List<ItemStack> stacks_2 = list_2.stream().distinct().sorted(ITEM_STACK_COMPARATOR).collect(Collectors.toList());
+    public static boolean equalsList(List<EntryStack> list_1, List<EntryStack> list_2) {
+        List<EntryStack> stacks_1 = list_1.stream().distinct().sorted(ENTRY_COMPARATOR).collect(Collectors.toList());
+        List<EntryStack> stacks_2 = list_2.stream().distinct().sorted(ENTRY_COMPARATOR).collect(Collectors.toList());
         if (stacks_1.equals(stacks_2))
             return true;
         if (stacks_1.size() != stacks_2.size())
             return false;
         for (int i = 0; i < stacks_1.size(); i++)
-            if (!stacks_1.get(i).isItemEqualIgnoreDamage(stacks_2.get(i)))
+            if (!stacks_1.get(i).equalsIgnoreTagsAndAmount(stacks_2.get(i)))
                 return false;
         return true;
     }
@@ -84,7 +126,7 @@ public class SimpleRecipeRenderer extends RecipeRenderer {
         int xx = x + 4, yy = y + 2;
         int j = 0;
         int itemsPerLine = getItemsPerLine();
-        for (ItemStackRenderer itemStackRenderer : inputRenderer) {
+        for (Renderer itemStackRenderer : inputRenderer) {
             itemStackRenderer.setBlitOffset(getBlitOffset() + 50);
             if (lastTooltip == null && MinecraftClient.getInstance().currentScreen instanceof VillagerRecipeViewingScreen && mouseX >= xx && mouseX <= xx + 16 && mouseY >= yy && mouseY <= yy + 16) {
                 lastTooltip = itemStackRenderer.getQueuedTooltip(delta);

+ 1 - 1
src/main/java/me/shedaniel/rei/gui/widget/CategoryBaseWidget.java

@@ -14,7 +14,7 @@ public class CategoryBaseWidget extends RecipeBaseWidget {
     }
     
     @Override
-    protected int getTextureOffset() {
+    protected int getYTextureOffset() {
         return 66;
     }
     

+ 7 - 7
src/main/java/me/shedaniel/rei/gui/widget/ClickableLabelWidget.java

@@ -27,10 +27,14 @@ public abstract class ClickableLabelWidget extends LabelWidget {
     
     @Override
     public void render(int mouseX, int mouseY, float delta) {
-        int colour = getDefaultColor();
+        int color = getDefaultColor();
         if (clickable && isHovered(mouseX, mouseY))
-            colour = getHoveredColor();
-        drawCenteredString(font, (isHovered(mouseX, mouseY) ? Formatting.UNDERLINE.toString() : "") + text, x, y, colour);
+            color = getHoveredColor();
+        int width = font.getStringWidth(text);
+        if (isHasShadows())
+            font.drawWithShadow((isHovered(mouseX, mouseY) ? Formatting.UNDERLINE.toString() : "") + text, x - width / 2, y, color);
+        else
+            font.draw((isHovered(mouseX, mouseY) ? Formatting.UNDERLINE.toString() : "") + text, x - width / 2, y, color);
         if (clickable && getTooltips().isPresent())
             if (!focused && containsMouse(mouseX, mouseY))
                 ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltips().get().split("\n")));
@@ -38,10 +42,6 @@ public abstract class ClickableLabelWidget extends LabelWidget {
                 ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(new Point(x, y), getTooltips().get().split("\n")));
     }
     
-    public int getDefaultColor() {
-        return ScreenHelper.isDarkModeEnabled() ? 0xFFBBBBBB : -1;
-    }
-    
     public int getHoveredColor() {
         return ScreenHelper.isDarkModeEnabled() ? -1 : 0xFF66FFCC;
     }

+ 3 - 0
src/main/java/me/shedaniel/rei/gui/widget/DetailedButtonWidget.java

@@ -6,8 +6,11 @@
 package me.shedaniel.rei.gui.widget;
 
 import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import net.minecraft.text.Text;
 
+@ToBeRemoved
+@Deprecated
 public class DetailedButtonWidget extends ButtonWidget {
     
     private DetailedButtonWidget.PressAction pressAction;

+ 150 - 173
src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -6,19 +6,21 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.google.common.collect.Lists;
-import com.zeitheron.hammercore.client.utils.Scissors;
-import me.shedaniel.clothconfig2.api.RunSixtyTimesEverySec;
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget.Interpolation;
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget.Precision;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import me.shedaniel.rei.gui.config.ItemCheatingMode;
 import me.shedaniel.rei.gui.config.ItemListOrdering;
-import me.shedaniel.rei.gui.renderers.FluidRenderer;
-import me.shedaniel.rei.gui.renderers.ItemStackRenderer;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.SearchArgument;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.item.TooltipContext;
 import net.minecraft.client.network.ClientPlayerEntity;
@@ -33,55 +35,27 @@ import net.minecraft.item.ItemGroup;
 import net.minecraft.item.ItemStack;
 import net.minecraft.text.Text;
 import net.minecraft.util.ActionResult;
-import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 import net.minecraft.util.registry.Registry;
 import org.apache.commons.lang3.StringUtils;
 
-import javax.annotation.Nullable;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicReference;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.function.Supplier;
 
+@SuppressWarnings({"deprecation", "rawtypes"})
 public class EntryListWidget extends Widget {
     
+    private static final Supplier<Boolean> RENDER_EXTRA_CONFIG = () -> RoughlyEnoughItemsCore.getConfigManager().getConfig().doesRenderEntryExtraOverlay();
     private static final String SPACE = " ", EMPTY = "";
-    private static final Comparator<Entry> ASCENDING_COMPARATOR;
+    private static final Comparator<EntryStack> ASCENDING_COMPARATOR;
     private static List<Item> searchBlacklisted = Lists.newArrayList();
     private static float scroll;
-    private static float scrollVelocity;
+    private static float target;
+    private static long start;
+    private static long duration;
     private static float maxScroll;
-    protected static RunSixtyTimesEverySec scroller = () -> {
-        try {
-            if (scrollVelocity == 0.0F && scroll >= 0.0F && scroll <= getMaxScroll()) {
-                scrollerUnregisterTick();
-            } else {
-                float change = scrollVelocity * 0.3F;
-                if (scrollVelocity != 0) {
-                    scroll += change;
-                    scrollVelocity -= scrollVelocity * (scroll >= 0.0F && scroll <= getMaxScroll() ? 0.2D : 0.4D);
-                    if (Math.abs(scrollVelocity) < 0.1F) {
-                        scrollVelocity = 0.0F;
-                    }
-                }
-                
-                if (scroll < 0.0F && scrollVelocity == 0.0F) {
-                    scroll = Math.min(scroll + (0.0F - scroll) * 0.2F, 0.0F);
-                    if (Math.abs(scroll) < 0.1F)
-                        scroll = 0.0F;
-                } else if (scroll > getMaxScroll() && scrollVelocity == 0.0F) {
-                    scroll = Math.max(scroll - (scroll - getMaxScroll()) * 0.2F, getMaxScroll());
-                    if (scroll > getMaxScroll() && scroll < getMaxScroll() + 0.1F) {
-                        scroll = getMaxScroll();
-                    }
-                }
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-        }
-    };
     private static float scrollBarAlpha = 0;
     private static float scrollBarAlphaFuture = 0;
     private static long scrollBarAlphaFutureTime = -1;
@@ -90,9 +64,9 @@ public class EntryListWidget extends Widget {
     static {
         ASCENDING_COMPARATOR = (entry, entry1) -> {
             if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemListOrdering().equals(ItemListOrdering.name))
-                return tryGetEntryName(entry).compareToIgnoreCase(tryGetEntryName(entry1));
+                return tryGetEntryStackName(entry).compareToIgnoreCase(tryGetEntryStackName(entry1));
             if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemListOrdering().equals(ItemListOrdering.item_groups)) {
-                if (entry.getEntryType() == Entry.Type.ITEM && entry1.getEntryType() == Entry.Type.ITEM) {
+                if (entry.getType() == EntryStack.Type.ITEM && entry1.getType() == EntryStack.Type.ITEM) {
                     ItemStack stack0 = entry.getItemStack();
                     ItemStack stack1 = entry1.getItemStack();
                     List<ItemGroup> itemGroups = Arrays.asList(ItemGroup.GROUPS);
@@ -104,7 +78,7 @@ public class EntryListWidget extends Widget {
     }
     
     private final List<SearchArgument[]> lastSearchArgument;
-    private List<Entry> currentDisplayed;
+    private List<EntryStack> currentDisplayed;
     private List<Slot> widgets;
     private int width, height, page;
     private Rectangle rectangle, listArea;
@@ -115,14 +89,12 @@ public class EntryListWidget extends Widget {
         this.height = 0;
         this.page = page;
         this.lastSearchArgument = Lists.newArrayList();
-        scroller.unregisterTick();
-        this.scrollVelocity = 0;
     }
     
     public static List<String> tryGetItemStackToolTip(ItemStack itemStack, boolean careAboutAdvanced) {
         if (!searchBlacklisted.contains(itemStack.getItem()))
             try {
-                return itemStack.getTooltip(MinecraftClient.getInstance().player, MinecraftClient.getInstance().options.advancedItemTooltips && careAboutAdvanced ? TooltipContext.Default.ADVANCED : TooltipContext.Default.NORMAL).stream().map(Text::asFormattedString).collect(Collectors.toList());
+                return CollectionUtils.map(itemStack.getTooltip(MinecraftClient.getInstance().player, MinecraftClient.getInstance().options.advancedItemTooltips && careAboutAdvanced ? TooltipContext.Default.ADVANCED : TooltipContext.Default.NORMAL), Text::asFormattedString);
             } catch (Throwable e) {
                 e.printStackTrace();
                 searchBlacklisted.add(itemStack.getItem());
@@ -130,6 +102,8 @@ public class EntryListWidget extends Widget {
         return Collections.singletonList(tryGetItemStackName(itemStack));
     }
     
+    @ToBeRemoved
+    @Deprecated
     public static String tryGetEntryName(Entry stack) {
         if (stack.getEntryType() == Entry.Type.ITEM)
             return tryGetItemStackName(stack.getItemStack());
@@ -138,11 +112,19 @@ public class EntryListWidget extends Widget {
         return "";
     }
     
+    public static String tryGetEntryStackName(EntryStack stack) {
+        if (stack.getType() == EntryStack.Type.ITEM)
+            return tryGetItemStackName(stack.getItemStack());
+        else if (stack.getType() == EntryStack.Type.FLUID)
+            return tryGetFluidName(stack.getFluid());
+        return "";
+    }
+    
     public static String tryGetFluidName(Fluid fluid) {
         Identifier id = Registry.FLUID.getId(fluid);
         if (I18n.hasTranslation("block." + id.toString().replaceFirst(":", ".")))
             return I18n.translate("block." + id.toString().replaceFirst(":", "."));
-        return Stream.of(id.getPath().split("_")).map(StringUtils::capitalize).collect(Collectors.joining(" "));
+        return CollectionUtils.mapAndJoinToString(id.getPath().split("_"), StringUtils::capitalize, " ");
     }
     
     public static String tryGetItemStackName(ItemStack stack) {
@@ -161,7 +143,7 @@ public class EntryListWidget extends Widget {
         return "ERROR";
     }
     
-    public static boolean filterEntry(Entry entry, List<SearchArgument[]> arguments) {
+    public static boolean filterEntry(EntryStack entry, List<SearchArgument[]> arguments) {
         if (arguments.isEmpty())
             return true;
         AtomicReference<String> mod = new AtomicReference<>(), tooltips = new AtomicReference<>(), name = new AtomicReference<>();
@@ -170,21 +152,27 @@ public class EntryListWidget extends Widget {
             for (SearchArgument argument : arguments1) {
                 if (argument.getArgumentType() == (SearchArgument.ArgumentType.ALWAYS))
                     return true;
-                if (argument.getArgumentType() == SearchArgument.ArgumentType.MOD)
-                    if (argument.getFunction(!argument.isInclude()).apply(fillMod(entry, mod).get())) {
+                if (argument.getArgumentType() == SearchArgument.ArgumentType.MOD) {
+                    fillMod(entry, mod);
+                    if (mod.get() != null && !mod.get().isEmpty() && argument.getFunction(!argument.isInclude()).apply(mod.get())) {
                         b = false;
                         break;
                     }
-                if (argument.getArgumentType() == SearchArgument.ArgumentType.TOOLTIP)
-                    if (argument.getFunction(!argument.isInclude()).apply(fillTooltip(entry, tooltips).get())) {
+                }
+                if (argument.getArgumentType() == SearchArgument.ArgumentType.TOOLTIP) {
+                    fillTooltip(entry, tooltips);
+                    if (tooltips.get() != null && !tooltips.get().isEmpty() && argument.getFunction(!argument.isInclude()).apply(tooltips.get())) {
                         b = false;
                         break;
                     }
-                if (argument.getArgumentType() == SearchArgument.ArgumentType.TEXT)
-                    if (argument.getFunction(!argument.isInclude()).apply(fillName(entry, name).get())) {
+                }
+                if (argument.getArgumentType() == SearchArgument.ArgumentType.TEXT) {
+                    fillName(entry, name);
+                    if (name.get() != null && !name.get().isEmpty() && argument.getFunction(!argument.isInclude()).apply(name.get())) {
                         b = false;
                         break;
                     }
+                }
             }
             if (b)
                 return true;
@@ -192,37 +180,31 @@ public class EntryListWidget extends Widget {
         return false;
     }
     
-    private static AtomicReference<String> fillMod(Entry entry, AtomicReference<String> mod) {
-        if (mod.get() == null)
-            if (entry.getEntryType() == Entry.Type.ITEM)
-                mod.set(ClientHelper.getInstance().getModFromItem(entry.getItemStack().getItem()).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
-            else if (entry.getEntryType() == Entry.Type.FLUID)
-                mod.set(ClientHelper.getInstance().getModFromIdentifier(Registry.FLUID.getId(entry.getFluid())).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
+    private static AtomicReference<String> fillMod(EntryStack entry, AtomicReference<String> mod) {
+        if (mod.get() == null) {
+            Optional<Identifier> identifier = entry.getIdentifier();
+            if (identifier.isPresent())
+                mod.set(ClientHelper.getInstance().getModFromIdentifier(identifier.get()).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
+            else mod.set("");
+        }
         return mod;
     }
     
-    private static AtomicReference<String> fillTooltip(Entry entry, AtomicReference<String> mod) {
+    private static AtomicReference<String> fillTooltip(EntryStack entry, AtomicReference<String> mod) {
         if (mod.get() == null)
-            if (entry.getEntryType() == Entry.Type.ITEM)
-                mod.set(tryGetItemStackToolTip(entry.getItemStack(), false).stream().collect(Collectors.joining("")).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
+            if (entry.getType() == EntryStack.Type.ITEM)
+                mod.set(CollectionUtils.joinToString(tryGetItemStackToolTip(entry.getItemStack(), false), "").replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
             else
-                mod.set(tryGetFluidName(entry.getFluid()).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
+                mod.set(tryGetEntryStackName(entry).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
         return mod;
     }
     
-    private static AtomicReference<String> fillName(Entry entry, AtomicReference<String> mod) {
+    private static AtomicReference<String> fillName(EntryStack entry, AtomicReference<String> mod) {
         if (mod.get() == null)
-            if (entry.getEntryType() == Entry.Type.ITEM)
-                mod.set(tryGetItemStackName(entry.getItemStack()).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
-            else
-                mod.set(tryGetFluidName(entry.getFluid()).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
+            mod.set(tryGetEntryStackName(entry).replace(SPACE, EMPTY).toLowerCase(Locale.ROOT));
         return mod;
     }
     
-    private static void scrollerUnregisterTick() {
-        scroller.unregisterTick();
-    }
-    
     public static float getMaxScroll() {
         return Math.max(maxScroll - ScreenHelper.getLastOverlay().getEntryListWidget().rectangle.height, 0);
     }
@@ -231,8 +213,43 @@ public class EntryListWidget extends Widget {
         return scroll;
     }
     
-    public static float getScrollVelocity() {
-        return scrollVelocity;
+    public static final float clamp(float v) {
+        return clamp(v, 300f);
+    }
+    
+    public static final float clamp(float v, float clampExtension) {
+        return MathHelper.clamp(v, -clampExtension, getMaxScroll() + clampExtension);
+    }
+    
+    public static void offset(float value, boolean animated) {
+        scrollTo(target + value, animated);
+    }
+    
+    public static void scrollTo(float value, boolean animated) {
+        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
+    }
+    
+    public static void scrollTo(float value, boolean animated, long duration) {
+        target = clamp(value);
+        
+        if (animated) {
+            start = System.currentTimeMillis();
+            EntryListWidget.duration = duration;
+        } else
+            scroll = target;
+    }
+    
+    private static void updatePosition(float delta) {
+        target = clamp(target);
+        if (target < 0) {
+            target -= target * (1 - ClothConfigInitializer.getBounceBackMultiplier()) * delta / 3;
+        } else if (target > getMaxScroll()) {
+            target = (float) ((target - getMaxScroll()) * (1 - (1 - ClothConfigInitializer.getBounceBackMultiplier()) * delta / 3) + getMaxScroll());
+        }
+        if (!Precision.almostEquals(scroll, target, Precision.FLOAT_EPSILON))
+            scroll = (float) Interpolation.expoEase(scroll, target, Math.min((System.currentTimeMillis() - start) / ((double) duration), 1));
+        else
+            scroll = target;
     }
     
     public int getFullTotalSlotsPerPage() {
@@ -241,17 +258,12 @@ public class EntryListWidget extends Widget {
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        if (!this.scroller.isRegistered())
-            this.scroller.registerTick();
         if (RoughlyEnoughItemsCore.getConfigManager().getConfig().isEntryListWidgetScrolled() && rectangle.contains(double_1, double_2)) {
-            if (this.scroll >= 0F && double_3 > 0)
-                scrollVelocity -= 24;
-            else if (this.scroll <= this.getMaxScroll() && double_3 < 0)
-                scrollVelocity += 24;
             if (scrollBarAlphaFuture == 0)
                 scrollBarAlphaFuture = 1f;
             if (System.currentTimeMillis() - scrollBarAlphaFutureTime > 300f)
                 scrollBarAlphaFutureTime = System.currentTimeMillis();
+            offset((float) (ClothConfigInitializer.getScrollStep() * -double_3), true);
             return true;
         }
         return super.mouseScrolled(double_1, double_2, double_3);
@@ -288,15 +300,15 @@ public class EntryListWidget extends Widget {
         if (!widgetScrolled)
             scroll = 0;
         else {
+            updatePosition(float_1);
             page = 0;
             ScreenHelper.getLastOverlay().setPage(0);
-            Scissors.begin();
-            Scissors.scissor(rectangle.x, rectangle.y, rectangle.width, rectangle.height);
+            ScissorsHandler.INSTANCE.scissor(rectangle);
         }
         widgets.forEach(widget -> {
             if (widgetScrolled) {
-                widget.y = (int) (widget.backupY - scroll);
-                if (widget.y <= rectangle.y + rectangle.height && widget.y + widget.getBounds().height >= rectangle.y)
+                widget.getBounds().y = (int) (widget.backupY - scroll);
+                if (widget.getBounds().y <= rectangle.y + rectangle.height && widget.getBounds().getMaxY() >= rectangle.y)
                     widget.render(int_1, int_2, float_1);
             } else {
                 widget.render(int_1, int_2, float_1);
@@ -331,7 +343,7 @@ public class EntryListWidget extends Widget {
                 RenderSystem.enableAlphaTest();
                 RenderSystem.enableTexture();
             }
-            Scissors.end();
+            ScissorsHandler.INSTANCE.removeLastScissor();
         }
         RenderSystem.popMatrix();
         ClientPlayerEntity player = minecraft.player;
@@ -345,7 +357,7 @@ public class EntryListWidget extends Widget {
         this.widgets = Lists.newCopyOnWriteArrayList();
         calculateListSize(rectangle);
         if (currentDisplayed.isEmpty() || processSearchTerm)
-            currentDisplayed = processSearchTerm(searchTerm, RoughlyEnoughItemsCore.getEntryRegistry().getEntryList(), new ArrayList<>(ScreenHelper.inventoryStacks));
+            currentDisplayed = processSearchTerm(searchTerm, RoughlyEnoughItemsCore.getEntryRegistry().getStacksList(), new ArrayList<>(ScreenHelper.inventoryStacks));
         int startX = rectangle.getCenterX() - width * 9;
         int startY = rectangle.getCenterY() - height * 9;
         this.listArea = new Rectangle(startX, startY, width * 18, height * 18);
@@ -364,74 +376,16 @@ public class EntryListWidget extends Widget {
                 j++;
                 if (j > currentDisplayed.size())
                     break;
-                final Entry entry = currentDisplayed.get(j - 1);
+                final EntryStack stack = currentDisplayed.get(j - 1).copy()
+                        .setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE)
+                        .setting(EntryStack.Settings.Item.RENDER_OVERLAY, RENDER_EXTRA_CONFIG);
                 maxScroll = y + 18;
-                widgets.add(new Slot(entry, xx, yy, x, y, entry.getEntryType() == Entry.Type.ITEM ? new ItemStackRenderer() {
-                    @Override
-                    public ItemStack getItemStack() {
-                        return entry.getItemStack();
-                    }
-                    
-                    @Override
-                    protected String getCounts() {
-                        return "";
-                    }
-    
-                    @Override
-                    protected boolean renderOverlay() {
-                        return RoughlyEnoughItemsCore.getConfigManager().getConfig().doesRenderEntryExtraOverlay();
-                    }
-    
-                    @Nullable
-                    @Override
-                    public QueuedTooltip getQueuedTooltip(float delta) {
-                        ClientPlayerEntity player = minecraft.player;
-                        if (!ClientHelper.getInstance().isCheating() || player.inventory.getCursorStack().isEmpty())
-                            return super.getQueuedTooltip(delta);
-                        return null;
-                    }
-                } : new FluidRenderer() {
-                    @Override
-                    public Fluid getFluid() {
-                        return entry.getFluid();
-                    }
-                    
-                    @Override
-                    protected List<String> getExtraToolTips(Fluid fluid) {
-                        if (MinecraftClient.getInstance().options.advancedItemTooltips)
-                            return Collections.singletonList(Formatting.DARK_GRAY.toString() + Registry.FLUID.getId(fluid).toString());
-                        return super.getExtraToolTips(fluid);
-                    }
-                }, false, true, true) {
-                    @Override
-                    public boolean mouseClicked(double mouseX, double mouseY, int button) {
-                        if (isCurrentRendererItem() && containsMouse(mouseX, mouseY)) {
-                            if (ClientHelper.getInstance().isCheating()) {
-                                if (getCurrentItemStack() != null && !getCurrentItemStack().isEmpty()) {
-                                    ItemStack cheatedStack = getCurrentItemStack().copy();
-                                    if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemCheatingMode() == ItemCheatingMode.REI_LIKE)
-                                        cheatedStack.setCount(button != 1 ? 1 : cheatedStack.getMaxCount());
-                                    else if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemCheatingMode() == ItemCheatingMode.JEI_LIKE)
-                                        cheatedStack.setCount(button != 0 ? 1 : cheatedStack.getMaxCount());
-                                    else
-                                        cheatedStack.setCount(1);
-                                    return ClientHelper.getInstance().tryCheatingStack(cheatedStack);
-                                }
-                            } else if (button == 0) {
-                                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentItemStack().copy());
-                            } else if (button == 1)
-                                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentItemStack().copy());
-                        }
-                        return false;
-                    }
-                });
+                widgets.add((Slot) new Slot(xx, yy, x, y).entry(stack).noBackground());
             }
             if (j > currentDisplayed.size())
                 break;
         }
-        EntryListWidget.maxScroll = maxScroll;
-        if (!scroller.isRegistered())
-            scroller.registerTick();
+        EntryListWidget.maxScroll = Math.max(maxScroll - 18, 0);
     }
     
     public int getTotalPage() {
@@ -471,7 +425,9 @@ public class EntryListWidget extends Widget {
                 int int_2 = rectangle.height;
                 int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) maxScroll), 32, int_2 - 8);
                 double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
-                scroll = MathHelper.clamp((float) (scroll + double_4 * double_6), 0, height - rectangle.height);
+                scrollBarAlphaFutureTime = System.currentTimeMillis();
+                scrollBarAlphaFuture = 1f;
+                scrollTo(MathHelper.clamp((float) (scroll + double_4 * double_6), 0, height - rectangle.height), false);
             }
         }
         return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
@@ -486,19 +442,19 @@ public class EntryListWidget extends Widget {
         return false;
     }
     
-    public List<Entry> getCurrentDisplayed() {
+    public List getCurrentDisplayed() {
         return currentDisplayed;
     }
     
-    private List<Entry> processSearchTerm(String searchTerm, List<Entry> ol, List<ItemStack> inventoryItems) {
+    private List<EntryStack> processSearchTerm(String searchTerm, List<EntryStack> ol, List<ItemStack> inventoryItems) {
         lastSearchArgument.clear();
-        List<Entry> os = new LinkedList<>(ol);
+        List<EntryStack> os = new LinkedList<>(ol);
         if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemListOrdering() != ItemListOrdering.registry)
-            os = ol.stream().sorted(ASCENDING_COMPARATOR).collect(Collectors.toList());
+            os.sort(ASCENDING_COMPARATOR);
         if (!RoughlyEnoughItemsCore.getConfigManager().getConfig().isItemListAscending())
             Collections.reverse(os);
         String[] splitSearchTerm = StringUtils.splitByWholeSeparatorPreserveAllTokens(searchTerm, "|");
-        Arrays.stream(splitSearchTerm).forEachOrdered(s -> {
+        for (String s : splitSearchTerm) {
             String[] split = StringUtils.split(s);
             SearchArgument[] arguments = new SearchArgument[split.length];
             for (int i = 0; i < split.length; i++) {
@@ -520,20 +476,20 @@ public class EntryListWidget extends Widget {
                 lastSearchArgument.add(arguments);
             else
                 lastSearchArgument.add(new SearchArgument[]{SearchArgument.ALWAYS});
-        });
-        List<Entry> stacks = Collections.emptyList();
+        }
+        List<EntryStack> stacks = Collections.emptyList();
         if (lastSearchArgument.isEmpty())
             stacks = os;
         else
-            stacks = os.stream().filter(entry -> filterEntry(entry, lastSearchArgument)).collect(Collectors.toList());
+            stacks = CollectionUtils.filter(os, entry -> filterEntry(entry, lastSearchArgument));
         if (!RoughlyEnoughItemsCore.getConfigManager().isCraftableOnlyEnabled() || stacks.isEmpty() || inventoryItems.isEmpty())
             return Collections.unmodifiableList(stacks);
-        List<ItemStack> workingItems = RecipeHelper.getInstance().findCraftableByItems(inventoryItems);
-        List<Entry> newList = Lists.newLinkedList();
-        for (ItemStack workingItem : workingItems) {
-            Optional<Entry> any = stacks.stream().filter(i -> i.getItemStack() != null && i.getItemStack().isItemEqualIgnoreDamage(workingItem)).findAny();
-            if (any.isPresent())
-                newList.add(any.get());
+        List<EntryStack> workingItems = RecipeHelper.getInstance().findCraftableEntriesByItems(inventoryItems);
+        List<EntryStack> newList = Lists.newLinkedList();
+        for (EntryStack workingItem : workingItems) {
+            EntryStack any = CollectionUtils.findFirstOrNullEquals(stacks, workingItem);
+            if (any != null)
+                newList.add(any);
         }
         if (newList.isEmpty())
             return Collections.unmodifiableList(stacks);
@@ -596,27 +552,21 @@ public class EntryListWidget extends Widget {
         return widgets;
     }
     
-    public class Slot extends SlotWidget {
+    private class Slot extends EntryWidget {
         private final int backupY;
         private int xx, yy;
-        private Entry entry;
         
-        public Slot(Entry entry, int xx, int yy, int x, int y, Renderer renderer, boolean drawBackground, boolean showToolTips, boolean clickToMoreRecipes) {
-            super(x, y, renderer, drawBackground, showToolTips, clickToMoreRecipes);
+        public Slot(int xx, int yy, int x, int y) {
+            super(x, y);
             this.xx = xx;
             this.yy = yy;
             this.backupY = y;
-            this.entry = entry;
         }
         
         public int getBackupY() {
             return backupY;
         }
         
-        public Entry getEntry() {
-            return entry;
-        }
-        
         public int getXx() {
             return xx;
         }
@@ -629,6 +579,33 @@ public class EntryListWidget extends Widget {
         public boolean containsMouse(double mouseX, double mouseY) {
             return super.containsMouse(mouseX, mouseY) && rectangle.contains(mouseX, mouseY);
         }
+        
+        @Override
+        protected void queueTooltip(int mouseX, int mouseY, float delta) {
+            ClientPlayerEntity player = minecraft.player;
+            if (!ClientHelper.getInstance().isCheating() || player.inventory.getCursorStack().isEmpty())
+                super.queueTooltip(mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int button) {
+            if (!interactable)
+                return super.mouseClicked(mouseX, mouseY, button);
+            if (containsMouse(mouseX, mouseY) && ClientHelper.getInstance().isCheating()) {
+                EntryStack entry = getCurrentEntry().copy();
+                if (entry.getType() == EntryStack.Type.ITEM) {
+                    if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemCheatingMode() == ItemCheatingMode.REI_LIKE)
+                        entry.setAmount(button != 1 ? 1 : entry.getItemStack().getMaxCount());
+                    else if (RoughlyEnoughItemsCore.getConfigManager().getConfig().getItemCheatingMode() == ItemCheatingMode.JEI_LIKE)
+                        entry.setAmount(button != 0 ? 1 : entry.getItemStack().getMaxCount());
+                    else
+                        entry.setAmount(1);
+                }
+                ClientHelper.getInstance().tryCheatingEntry(entry);
+                return true;
+            }
+            return super.mouseClicked(mouseX, mouseY, button);
+        }
     }
     
 }

+ 195 - 0
src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java

@@ -0,0 +1,195 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.gui.widget;
+
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.compat.RenderHelper;
+import me.shedaniel.math.impl.PointHelper;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.annotations.Experimental;
+import me.shedaniel.rei.impl.ScreenHelper;
+import net.minecraft.client.gui.Element;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+@Experimental
+public class EntryWidget extends WidgetWithBounds {
+    
+    protected static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    protected static final Identifier RECIPE_GUI_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
+    
+    protected boolean highlight = true;
+    protected boolean tooltips = true;
+    protected boolean background = true;
+    protected boolean interactable = true;
+    private Rectangle bounds;
+    private List<EntryStack> entryStacks;
+    
+    protected EntryWidget(int x, int y) {
+        this.bounds = new Rectangle(x - 1, y - 1, 18, 18);
+        this.entryStacks = new ArrayList<>();
+    }
+    
+    public static EntryWidget create(int x, int y) {
+        return new EntryWidget(x, y);
+    }
+    
+    public EntryWidget disableInteractions() {
+        return interactable(false);
+    }
+    
+    public EntryWidget interactable(boolean b) {
+        interactable = b;
+        return this;
+    }
+    
+    public EntryWidget noHighlight() {
+        return highlight(false);
+    }
+    
+    public EntryWidget highlight(boolean b) {
+        highlight = b;
+        return this;
+    }
+    
+    public EntryWidget noTooltips() {
+        return tooltips(false);
+    }
+    
+    public EntryWidget tooltips(boolean b) {
+        tooltips = b;
+        return this;
+    }
+    
+    public EntryWidget noBackground() {
+        return background(false);
+    }
+    
+    public EntryWidget background(boolean b) {
+        background = b;
+        return this;
+    }
+    
+    public EntryWidget clearStacks() {
+        entryStacks.clear();
+        return this;
+    }
+    
+    public EntryWidget entry(EntryStack stack) {
+        entryStacks.add(stack);
+        return this;
+    }
+    
+    public EntryWidget entries(Collection<EntryStack> stacks) {
+        entryStacks.addAll(stacks);
+        return this;
+    }
+    
+    protected EntryStack getCurrentEntry() {
+        if (entryStacks.isEmpty())
+            return EntryStack.empty();
+        if (entryStacks.size() == 1)
+            return entryStacks.get(0);
+        return entryStacks.get(MathHelper.floor((System.currentTimeMillis() / 500 % (double) entryStacks.size()) / 1f));
+    }
+    
+    public List<EntryStack> entries() {
+        return entryStacks;
+    }
+    
+    @Override
+    public Rectangle getBounds() {
+        return bounds;
+    }
+    
+    protected Rectangle getInnerBounds() {
+        return new Rectangle(bounds.x + 1, bounds.y + 1, bounds.width - 2, bounds.height - 2);
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        if (background) {
+            drawBackground(mouseX, mouseY, delta);
+        }
+        drawCurrentEntry(mouseX, mouseY, delta);
+        
+        boolean highlighted = containsMouse(mouseX, mouseY);
+        if (tooltips && highlighted) {
+            queueTooltip(mouseX, mouseY, delta);
+        }
+        if (highlight && highlighted) {
+            drawHighlighted(mouseX, mouseY, delta);
+        }
+    }
+    
+    protected void drawBackground(int mouseX, int mouseY, float delta) {
+        minecraft.getTextureManager().bindTexture(ScreenHelper.isDarkModeEnabled() ? RECIPE_GUI_DARK : RECIPE_GUI);
+        blit(bounds.x, bounds.y, 0, 222, bounds.width, bounds.height);
+    }
+    
+    protected void drawCurrentEntry(int mouseX, int mouseY, float delta) {
+        EntryStack entry = getCurrentEntry();
+        entry.setZ(200);
+        entry.render(getInnerBounds(), mouseX, mouseY, delta);
+    }
+    
+    protected void queueTooltip(int mouseX, int mouseY, float delta) {
+        EntryStack entry = getCurrentEntry();
+        QueuedTooltip tooltip = entry.getTooltip(mouseX, mouseY);
+        if (tooltip != null) {
+            ScreenHelper.getLastOverlay().addTooltip(tooltip);
+        }
+    }
+    
+    protected void drawHighlighted(int mouseX, int mouseY, float delta) {
+        RenderHelper.disableLighting();
+        RenderHelper.disableDepthTest();
+        RenderHelper.colorMask(true, true, true, false);
+        int color = ScreenHelper.isDarkModeEnabled() ? -1877929711 : -2130706433;
+        blitOffset = 300;
+        Rectangle bounds = getInnerBounds();
+        fillGradient(bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), color, color);
+        blitOffset = 0;
+        RenderHelper.colorMask(true, true, true, true);
+        RenderHelper.enableLighting();
+        RenderHelper.enableDepthTest();
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        if (!interactable)
+            return false;
+        if (containsMouse(mouseX, mouseY))
+            if (button == 0)
+                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentEntry());
+            else if (button == 1)
+                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentEntry());
+        return false;
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (!interactable)
+            return false;
+        if (containsMouse(PointHelper.fromMouse()))
+            if (ClientHelper.getInstance().getRecipeKeyBinding().matchesKey(int_1, int_2))
+                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentEntry());
+            else if (ClientHelper.getInstance().getUsageKeyBinding().matchesKey(int_1, int_2))
+                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentEntry());
+        return false;
+    }
+}

+ 24 - 1
src/main/java/me/shedaniel/rei/gui/widget/LabelWidget.java

@@ -6,6 +6,7 @@
 package me.shedaniel.rei.gui.widget;
 
 import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.impl.ScreenHelper;
 import net.minecraft.client.gui.Element;
 
 import java.util.Collections;
@@ -16,11 +17,30 @@ public class LabelWidget extends WidgetWithBounds {
     public int x;
     public int y;
     public String text;
+    private int defaultColor;
+    private boolean hasShadows = true;
     
     public LabelWidget(int x, int y, String text) {
         this.x = x;
         this.y = y;
         this.text = text;
+        this.defaultColor = ScreenHelper.isDarkModeEnabled() ? 0xFFBBBBBB : -1;
+    }
+    
+    public boolean isHasShadows() {
+        return hasShadows;
+    }
+    
+    public void setHasShadows(boolean hasShadows) {
+        this.hasShadows = hasShadows;
+    }
+    
+    public int getDefaultColor() {
+        return defaultColor;
+    }
+    
+    public void setDefaultColor(int defaultColor) {
+        this.defaultColor = defaultColor;
     }
     
     @Override
@@ -36,7 +56,10 @@ public class LabelWidget extends WidgetWithBounds {
     
     @Override
     public void render(int mouseX, int mouseY, float delta) {
-        drawCenteredString(font, text, x, y, -1);
+        int width = font.getStringWidth(text);
+        if (hasShadows)
+            font.drawWithShadow(text, x - width / 2, y, defaultColor);
+        else font.draw(text, x - width / 2, y, defaultColor);
     }
     
 }

+ 111 - 0
src/main/java/me/shedaniel/rei/gui/widget/PanelWidget.java

@@ -0,0 +1,111 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.gui.widget;
+
+import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.compat.RenderHelper;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.annotations.Experimental;
+import me.shedaniel.rei.gui.config.RecipeScreenType;
+import me.shedaniel.rei.impl.ScreenHelper;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.Identifier;
+
+import java.util.Collections;
+import java.util.List;
+
+@Experimental
+@Deprecated
+public class PanelWidget extends WidgetWithBounds {
+    
+    private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    private static final Identifier CHEST_GUI_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
+    
+    private Rectangle bounds;
+    private int color = -1;
+    
+    public PanelWidget(Rectangle bounds) {
+        this.bounds = bounds;
+    }
+    
+    @Override
+    public Rectangle getBounds() {
+        return bounds;
+    }
+    
+    @Override
+    public List<Widget> children() {
+        return Collections.emptyList();
+    }
+    
+    public void render() {
+        render(0, 0, 0);
+    }
+    
+    public int getColor() {
+        return color;
+    }
+    
+    public void setColor(int color) {
+        this.color = color;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        if (!isRendering())
+            return;
+        float red = ((color >> 16) & 0xFF) / 255f;
+        float green = ((color >> 8) & 0xFF) / 255f;
+        float blue = ((color >> 0) & 0xFF) / 255f;
+        float alpha = ((color >> 32) & 0xFF) / 255f;
+        RenderHelper.color4f(red, green, blue, alpha);
+        GuiLighting.disable();
+        minecraft.getTextureManager().bindTexture(ScreenHelper.isDarkModeEnabled() ? CHEST_GUI_TEXTURE_DARK : CHEST_GUI_TEXTURE);
+        int x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height;
+        int xTextureOffset = getXTextureOffset();
+        int yTextureOffset = getYTextureOffset();
+        
+        //Four Corners
+        this.blit(x, y, 106 + xTextureOffset, 124 + yTextureOffset, 4, 4);
+        this.blit(x + width - 4, y, 252 + xTextureOffset, 124 + yTextureOffset, 4, 4);
+        this.blit(x, y + height - 4, 106 + xTextureOffset, 186 + yTextureOffset, 4, 4);
+        this.blit(x + width - 4, y + height - 4, 252 + xTextureOffset, 186 + yTextureOffset, 4, 4);
+        
+        //Sides
+        for (int xx = 4; xx < width - 4; xx += 128) {
+            int thisWidth = Math.min(128, width - 4 - xx);
+            this.blit(x + xx, y, 110 + xTextureOffset, 124 + yTextureOffset, thisWidth, 4);
+            this.blit(x + xx, y + height - 4, 110 + xTextureOffset, 186 + yTextureOffset, thisWidth, 4);
+        }
+        for (int yy = 4; yy < height - 4; yy += 50) {
+            int thisHeight = Math.min(50, height - 4 - yy);
+            this.blit(x, y + yy, 106 + xTextureOffset, 128 + yTextureOffset, 4, thisHeight);
+            this.blit(x + width - 4, y + yy, 252 + xTextureOffset, 128 + yTextureOffset, 4, thisHeight);
+        }
+        fillGradient(x + 4, y + 4, x + width - 4, y + height - 4, getInnerColor(), getInnerColor());
+    }
+    
+    protected boolean isRendering() {
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().getRecipeScreenType() != RecipeScreenType.VILLAGER;
+    }
+    
+    protected int getInnerColor() {
+        return ScreenHelper.isDarkModeEnabled() ? -13750738 : -3750202;
+    }
+    
+    protected int getXTextureOffset() {
+        return 0;
+    }
+    
+    protected int getYTextureOffset() {
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().isUsingLightGrayRecipeBorder() ? 0 : 66;
+    }
+    
+}

+ 7 - 64
src/main/java/me/shedaniel/rei/gui/widget/RecipeBaseWidget.java

@@ -8,80 +8,23 @@ package me.shedaniel.rei.gui.widget;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
-import me.shedaniel.rei.gui.config.RecipeScreenType;
-import me.shedaniel.rei.impl.ScreenHelper;
-import net.minecraft.client.render.GuiLighting;
-import net.minecraft.util.Identifier;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 
-import java.util.Collections;
-import java.util.List;
-
-public class RecipeBaseWidget extends WidgetWithBounds {
-    
-    private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
-    private static final Identifier CHEST_GUI_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
-    
-    private Rectangle bounds;
+public class RecipeBaseWidget extends PanelWidget {
     
     public RecipeBaseWidget(Rectangle bounds) {
-        this.bounds = bounds;
-    }
-    
-    @Override
-    public Rectangle getBounds() {
-        return bounds;
-    }
-    
-    @Override
-    public List<Widget> children() {
-        return Collections.emptyList();
-    }
-    
-    public void render() {
-        render(0, 0, 0);
+        super(bounds);
     }
     
     @Override
-    public void render(int mouseX, int mouseY, float delta) {
-        if (!isRendering())
-            return;
-        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-        GuiLighting.disable();
-        minecraft.getTextureManager().bindTexture(ScreenHelper.isDarkModeEnabled() ? CHEST_GUI_TEXTURE_DARK : CHEST_GUI_TEXTURE);
-        int x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height;
-        int textureOffset = getTextureOffset();
-        
-        //Four Corners
-        this.blit(x, y, 106, 124 + textureOffset, 4, 4);
-        this.blit(x + width - 4, y, 252, 124 + textureOffset, 4, 4);
-        this.blit(x, y + height - 4, 106, 186 + textureOffset, 4, 4);
-        this.blit(x + width - 4, y + height - 4, 252, 186 + textureOffset, 4, 4);
-        
-        //Sides
-        for (int xx = 4; xx < width - 4; xx += 128) {
-            int thisWidth = Math.min(128, width - 4 - xx);
-            this.blit(x + xx, y, 110, 124 + textureOffset, thisWidth, 4);
-            this.blit(x + xx, y + height - 4, 110, 186 + textureOffset, thisWidth, 4);
-        }
-        for (int yy = 4; yy < height - 4; yy += 50) {
-            int thisHeight = Math.min(50, height - 4 - yy);
-            this.blit(x, y + yy, 106, 128 + textureOffset, 4, thisHeight);
-            this.blit(x + width - 4, y + yy, 252, 128 + textureOffset, 4, thisHeight);
-        }
-        fillGradient(x + 4, y + 4, x + width - 4, y + height - 4, getInnerColor(), getInnerColor());
-    }
-    
-    protected boolean isRendering() {
-        return RoughlyEnoughItemsCore.getConfigManager().getConfig().getRecipeScreenType() != RecipeScreenType.VILLAGER;
-    }
-    
-    protected int getInnerColor() {
-        return ScreenHelper.isDarkModeEnabled() ? -13750738 : -3750202;
+    protected int getYTextureOffset() {
+        return getTextureOffset();
     }
     
+    @ToBeRemoved
+    @Deprecated
     protected int getTextureOffset() {
         return RoughlyEnoughItemsCore.getConfigManager().getConfig().isUsingLightGrayRecipeBorder() ? 0 : 66;
     }
     
-    
 }

+ 1 - 1
src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java

@@ -20,7 +20,7 @@ public class SlotBaseWidget extends RecipeBaseWidget {
     }
     
     @Override
-    protected int getTextureOffset() {
+    protected int getYTextureOffset() {
         return -66;
     }
     

+ 58 - 20
src/main/java/me/shedaniel/rei/gui/widget/SlotWidget.java

@@ -11,10 +11,13 @@ import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.Renderer;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import me.shedaniel.rei.gui.renderers.FluidRenderer;
 import me.shedaniel.rei.gui.renderers.ItemStackRenderer;
 import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.gui.Element;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.ItemStack;
@@ -27,13 +30,15 @@ import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
-import java.util.stream.Collectors;
+import java.util.function.Function;
 
+@SuppressWarnings("deprecation")
 public class SlotWidget extends WidgetWithBounds {
     
     public static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
     public static final Identifier RECIPE_GUI_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
-    protected int x, y;
+    private static final Function<ItemStack, Renderer> ITEM_STACK_RENDERER = Renderer::fromItemStack;
+    private Rectangle bounds;
     private List<Renderer> renderers = new LinkedList<>();
     private boolean drawBackground, showToolTips, clickToMoreRecipes, drawHighlightedBackground;
     
@@ -49,8 +54,7 @@ public class SlotWidget extends WidgetWithBounds {
         this.renderers = renderers;
         this.drawBackground = drawBackground;
         this.showToolTips = showToolTips;
-        this.x = x;
-        this.y = y;
+        this.bounds = new Rectangle(x - 1, y - 1, 18, 18);
         this.clickToMoreRecipes = false;
         this.drawHighlightedBackground = true;
     }
@@ -60,20 +64,40 @@ public class SlotWidget extends WidgetWithBounds {
         this.clickToMoreRecipes = clickToMoreRecipes;
     }
     
+    /**
+     * @see WidgetWithBounds#getBounds()
+     */
+    @ToBeRemoved
+    @Deprecated
     public int getX() {
-        return x;
+        return bounds.x + 1;
     }
     
+    /**
+     * @see WidgetWithBounds#getBounds()
+     */
+    @ToBeRemoved
+    @Deprecated
     public void setX(int x) {
-        this.x = x;
+        bounds.x = x - 1;
     }
     
+    /**
+     * @see WidgetWithBounds#getBounds()
+     */
+    @ToBeRemoved
+    @Deprecated
     public int getY() {
-        return y;
+        return bounds.y + 1;
     }
     
+    /**
+     * @see WidgetWithBounds#getBounds()
+     */
+    @ToBeRemoved
+    @Deprecated
     public void setY(int y) {
-        this.y = y;
+        bounds.y = y - 1;
     }
     
     public boolean isShowToolTips() {
@@ -119,11 +143,11 @@ public class SlotWidget extends WidgetWithBounds {
         boolean darkTheme = ScreenHelper.isDarkModeEnabled();
         if (drawBackground) {
             minecraft.getTextureManager().bindTexture(darkTheme ? RECIPE_GUI_DARK : RECIPE_GUI);
-            blit(this.x - 1, this.y - 1, 0, 222, 18, 18);
+            blit(bounds.x, bounds.y, 0, 222, bounds.width, bounds.height);
         }
         boolean highlighted = containsMouse(mouseX, mouseY);
         renderer.setBlitOffset(200);
-        renderer.render(x + 8, y + 6, mouseX, mouseY, delta);
+        renderer.render(bounds.x + 9, bounds.y + 7, mouseX, mouseY, delta);
         if (highlighted && showToolTips) {
             QueuedTooltip queuedTooltip = renderer.getQueuedTooltip(delta);
             if (queuedTooltip != null) {
@@ -136,17 +160,17 @@ public class SlotWidget extends WidgetWithBounds {
             RenderSystem.colorMask(true, true, true, false);
             int color = darkTheme ? -1877929711 : -2130706433;
             setBlitOffset(300);
-            fillGradient(x, y, x + 16, y + 16, color, color);
+            fillGradient(bounds.x + 1, bounds.y + 1, bounds.getMaxX() - 1, bounds.getMaxY() - 1, color, color);
             setBlitOffset(0);
             RenderSystem.colorMask(true, true, true, true);
             RenderSystem.enableLighting();
             RenderSystem.enableDepthTest();
         }
     }
-    
     /**
      * @deprecated Not used anymore, see {@link Renderer#getQueuedTooltip(float)}
      */
+    @ToBeRemoved
     @Deprecated
     protected void queueTooltip(Fluid fluid, float delta) {
         ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltip(fluid)));
@@ -155,6 +179,7 @@ public class SlotWidget extends WidgetWithBounds {
     /**
      * @deprecated Not used anymore, see {@link Renderer#getQueuedTooltip(float)}
      */
+    @ToBeRemoved
     @Deprecated
     private List<String> getTooltip(Fluid fluid) {
         List<String> toolTip = Lists.newArrayList(EntryListWidget.tryGetFluidName(fluid));
@@ -176,6 +201,7 @@ public class SlotWidget extends WidgetWithBounds {
     /**
      * @deprecated Not used anymore, see {@link Renderer#getQueuedTooltip(float)}
      */
+    @ToBeRemoved
     @Deprecated
     protected void queueTooltip(ItemStack itemStack, float delta) {
         ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltip(itemStack)));
@@ -184,6 +210,7 @@ public class SlotWidget extends WidgetWithBounds {
     /**
      * @deprecated Not used anymore, see {@link Renderer#getQueuedTooltip(float)}
      */
+    @ToBeRemoved
     @Deprecated
     protected List<String> getTooltip(ItemStack itemStack) {
         List<String> toolTip = Lists.newArrayList(EntryListWidget.tryGetItemStackToolTip(itemStack, true));
@@ -202,6 +229,7 @@ public class SlotWidget extends WidgetWithBounds {
     /**
      * @deprecated See {@link ItemStackRenderer#getExtraToolTips(ItemStack)}
      */
+    @ToBeRemoved
     @Deprecated
     protected List<String> getExtraItemToolTips(ItemStack stack) {
         return Collections.emptyList();
@@ -210,11 +238,14 @@ public class SlotWidget extends WidgetWithBounds {
     /**
      * @deprecated See {@link FluidRenderer#getExtraToolTips(Fluid)}
      */
+    @ToBeRemoved
     @Deprecated
     protected List<String> getExtraFluidToolTips(Fluid fluid) {
         return Collections.emptyList();
     }
     
+    @ToBeRemoved
+    @Deprecated
     public ItemStack getCurrentItemStack() {
         if (getCurrentRenderer() instanceof ItemStackRenderer)
             return ((ItemStackRenderer) getCurrentRenderer()).getItemStack();
@@ -231,9 +262,10 @@ public class SlotWidget extends WidgetWithBounds {
      * @param itemList the list of items
      * @deprecated Use {@link SlotWidget#setRenderers(List)}
      */
+    @ToBeRemoved
     @Deprecated
     public void setItemList(List<ItemStack> itemList) {
-        this.setRenderers(itemList.stream().map(Renderer::fromItemStack).collect(Collectors.toList()));
+        setRenderers(CollectionUtils.map(itemList, ITEM_STACK_RENDERER));
     }
     
     public void setRenderers(List<Renderer> renderers) {
@@ -242,25 +274,30 @@ public class SlotWidget extends WidgetWithBounds {
     
     @Override
     public Rectangle getBounds() {
-        return new Rectangle(this.x - 1, this.y - 1, 18, 18);
+        return bounds;
     }
     
     @Override
     public boolean mouseClicked(double mouseX, double mouseY, int button) {
         if (!clickToMoreRecipes)
             return false;
-        if (isCurrentRendererItem() && getBounds().contains(mouseX, mouseY))
+        EntryStack entry = getCurrentRenderer().getEntry();
+        if (getBounds().contains(mouseX, mouseY))
             if (button == 0)
-                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentItemStack());
+                return ClientHelper.getInstance().executeRecipeKeyBind(entry);
             else if (button == 1)
-                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentItemStack());
+                return ClientHelper.getInstance().executeUsageKeyBind(entry);
         return false;
     }
     
+    @ToBeRemoved
+    @Deprecated
     public boolean isCurrentRendererItem() {
         return getCurrentRenderer() instanceof ItemStackRenderer;
     }
     
+    @ToBeRemoved
+    @Deprecated
     public boolean isCurrentRendererFluid() {
         return getCurrentRenderer() instanceof FluidRenderer;
     }
@@ -269,11 +306,12 @@ public class SlotWidget extends WidgetWithBounds {
     public boolean keyPressed(int int_1, int int_2, int int_3) {
         if (!clickToMoreRecipes)
             return false;
-        if (isCurrentRendererItem() && getBounds().contains(PointHelper.fromMouse()))
+        EntryStack entry = getCurrentRenderer().getEntry();
+        if (getBounds().contains(PointHelper.fromMouse()))
             if (ClientHelper.getInstance().getRecipeKeyBinding().matchesKey(int_1, int_2))
-                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentItemStack());
+                return ClientHelper.getInstance().executeRecipeKeyBind(entry);
             else if (ClientHelper.getInstance().getUsageKeyBinding().matchesKey(int_1, int_2))
-                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentItemStack());
+                return ClientHelper.getInstance().executeUsageKeyBind(entry);
         return false;
     }
     

+ 8 - 12
src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java

@@ -8,8 +8,8 @@ package me.shedaniel.rei.gui.widget;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.api.Renderer;
 import me.shedaniel.rei.impl.ScreenHelper;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.util.Formatting;
@@ -24,7 +24,7 @@ public class TabWidget extends WidgetWithBounds {
     public static final Identifier CHEST_GUI_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
     
     public boolean shown = false, selected = false;
-    public Renderer renderer;
+    public EntryStack logo;
     public int id;
     public String categoryName;
     public Rectangle bounds;
@@ -35,13 +35,13 @@ public class TabWidget extends WidgetWithBounds {
         this.bounds = bounds;
     }
     
-    public void setRenderer(RecipeCategory category, Renderer renderable, String categoryName, boolean selected) {
-        if (renderable == null) {
+    public void setRenderer(RecipeCategory category, EntryStack logo, String categoryName, boolean selected) {
+        if (logo == null) {
             shown = false;
-            this.renderer = null;
+            this.logo = null;
         } else {
             shown = true;
-            this.renderer = renderable;
+            this.logo = logo;
         }
         this.category = category;
         this.selected = selected;
@@ -60,10 +60,6 @@ public class TabWidget extends WidgetWithBounds {
         return shown;
     }
     
-    public Renderer getRenderer() {
-        return renderer;
-    }
-    
     @Override
     public List<Widget> children() {
         return Collections.emptyList();
@@ -76,8 +72,8 @@ public class TabWidget extends WidgetWithBounds {
             GuiLighting.disable();
             minecraft.getTextureManager().bindTexture(ScreenHelper.isDarkModeEnabled() ? CHEST_GUI_TEXTURE_DARK : CHEST_GUI_TEXTURE);
             this.blit(bounds.x, bounds.y + 2, selected ? 28 : 0, 192, 28, (selected ? 30 : 27));
-            renderer.setBlitOffset(100);
-            renderer.render(bounds.getCenterX(), bounds.getCenterY(), mouseX, mouseY, delta);
+            logo.setZ(100);
+            logo.render(new Rectangle(bounds.getCenterX() - 8, bounds.getCenterY() - 6, 16, 16), mouseX, mouseY, delta);
             if (containsMouse(mouseX, mouseY)) {
                 drawTooltip();
             }

+ 8 - 0
src/main/java/me/shedaniel/rei/gui/widget/Widget.java

@@ -26,4 +26,12 @@ public abstract class Widget extends AbstractParentElement implements Drawable {
      */
     protected final TextRenderer font = minecraft.textRenderer;
     
+    public int getZ() {
+        return this.blitOffset;
+    }
+    
+    public void setZ(int z) {
+        this.blitOffset = z;
+    }
+    
 }

+ 78 - 0
src/main/java/me/shedaniel/rei/impl/AbstractEntryStack.java

@@ -0,0 +1,78 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.impl;
+
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.ObjectHolder;
+import net.minecraft.client.gui.DrawableHelper;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Deprecated
+public abstract class AbstractEntryStack extends DrawableHelper implements EntryStack {
+    private Map<Settings, Object> settings = new HashMap<>();
+    
+    @Override
+    public <T> EntryStack setting(Settings<T> settings, T value) {
+        this.settings.put(settings, value);
+        return this;
+    }
+    
+    @Override
+    public <T> EntryStack removeSetting(Settings<T> settings) {
+        this.settings.remove(settings);
+        return this;
+    }
+    
+    @Override
+    public EntryStack clearSettings() {
+        this.settings.clear();
+        return this;
+    }
+    
+    protected Map<Settings, Object> getSettings() {
+        return settings;
+    }
+    
+    @Override
+    public <T> ObjectHolder<T> getSetting(Settings<T> settings) {
+        Object o = this.settings.get(settings);
+        if (o == null)
+            return new ObjectHolderImpl(settings.getDefaultValue());
+        return new ObjectHolderImpl(o);
+    }
+    
+    @Override
+    public boolean equals(EntryStack stack, boolean ignoreTags, boolean ignoreAmount) {
+        if (ignoreTags && ignoreAmount)
+            return equalsIgnoreTagsAndAmount(stack);
+        if (ignoreAmount)
+            return equalsIgnoreAmount(stack);
+        if (ignoreTags)
+            return equalsIgnoreTags(stack);
+        return equalsAll(stack);
+    }
+    
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof EntryStack))
+            return false;
+        EntryStack stack = (EntryStack) obj;
+        boolean checkTags = getSetting(Settings.CHECK_TAGS).value().get() || stack.getSetting(Settings.CHECK_TAGS).value().get();
+        return equals(stack, !checkTags, true);
+    }
+    
+    @Override
+    public int getZ() {
+        return blitOffset;
+    }
+    
+    @Override
+    public void setZ(int z) {
+        blitOffset = z;
+    }
+}

+ 10 - 20
src/main/java/me/shedaniel/rei/impl/BaseBoundsHandlerImpl.java

@@ -21,14 +21,7 @@ import java.util.function.Function;
 
 public class BaseBoundsHandlerImpl implements BaseBoundsHandler {
     
-    private static final Function<Rectangle, Long> RECTANGLE_LONG_FUNCTION = r -> r.x + 1000l * r.y + 1000000l * r.width + 1000000000l * r.height;
-    private static final Comparator<Pair<Pair<Class<?>, Float>, Function<Boolean, List<Rectangle>>>> LIST_PAIR_COMPARATOR;
-    private static final Comparator<? super Rectangle> RECTANGLE_COMPARER = Comparator.comparingLong(RECTANGLE_LONG_FUNCTION::apply);
-    
-    static {
-        Comparator<Pair<Pair<Class<?>, Float>, Function<Boolean, List<Rectangle>>>> comparator = Comparator.comparingDouble(value -> value.getLeft().getRight());
-        LIST_PAIR_COMPARATOR = comparator.reversed();
-    }
+    private static final Comparator<? super Rectangle> RECTANGLE_COMPARER = Comparator.comparingLong(Rectangle::hashCode);
     
     private long lastArea = -1;
     private List<Pair<Pair<Class<?>, Float>, Function<Boolean, List<Rectangle>>>> list = Lists.newArrayList();
@@ -63,19 +56,16 @@ public class BaseBoundsHandlerImpl implements BaseBoundsHandler {
     
     @Override
     public boolean shouldRecalculateArea(boolean isOnRightSide, Rectangle rectangle) {
-        long current = getStringFromCurrent(isOnRightSide);
+        long current = currentHashCode(isOnRightSide);
         if (lastArea == current)
             return false;
         lastArea = current;
         return true;
     }
     
-    private DisplayHelper.DisplayBoundsHandler getHandler() {
-        return RoughlyEnoughItemsCore.getDisplayHelper().getResponsibleBoundsHandler(MinecraftClient.getInstance().currentScreen.getClass());
-    }
-    
-    private long getStringFromCurrent(boolean isOnRightSide) {
-        return getLongFromAreas(isOnRightSide ? getHandler().getRightBounds(MinecraftClient.getInstance().currentScreen) : getHandler().getLeftBounds(MinecraftClient.getInstance().currentScreen), getCurrentExclusionZones(MinecraftClient.getInstance().currentScreen.getClass(), isOnRightSide, false));
+    private long currentHashCode(boolean isOnRightSide) {
+        DisplayHelper.DisplayBoundsHandler handler = RoughlyEnoughItemsCore.getDisplayHelper().getResponsibleBoundsHandler(MinecraftClient.getInstance().currentScreen.getClass());
+        return areasHashCode(isOnRightSide ? handler.getRightBounds(MinecraftClient.getInstance().currentScreen) : handler.getLeftBounds(MinecraftClient.getInstance().currentScreen), getCurrentExclusionZones(MinecraftClient.getInstance().currentScreen.getClass(), isOnRightSide, false));
     }
     
     @Override
@@ -103,11 +93,11 @@ public class BaseBoundsHandlerImpl implements BaseBoundsHandler {
         list.add(new Pair<>(new Pair<>(screenClass, 0f), supplier));
     }
     
-    public long getLongFromAreas(Rectangle rectangle, List<Rectangle> exclusionZones) {
-        long a = RECTANGLE_LONG_FUNCTION.apply(rectangle);
-        for (Rectangle exclusionZone : exclusionZones)
-            a -= RECTANGLE_LONG_FUNCTION.apply(exclusionZone);
-        return a;
+    private long areasHashCode(Rectangle rectangle, List<Rectangle> exclusionZones) {
+        int hashCode = 31 + (rectangle == null ? 0 : rectangle.hashCode());
+        for (Rectangle e : exclusionZones)
+            hashCode = 31 * hashCode + (e == null ? 0 : e.hashCode());
+        return hashCode;
     }
     
 }

+ 11 - 9
src/main/java/me/shedaniel/rei/impl/ClientHelperImpl.java

@@ -10,10 +10,7 @@ import com.google.common.collect.Maps;
 import io.netty.buffer.Unpooled;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.RoughlyEnoughItemsNetwork;
-import me.shedaniel.rei.api.ClientHelper;
-import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.api.RecipeDisplay;
-import me.shedaniel.rei.api.RecipeHelper;
+import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.gui.PreRecipeViewingScreen;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
@@ -155,16 +152,21 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
     }
     
     @Override
-    public boolean tryCheatingStack(ItemStack cheatedStack) {
+    public boolean tryCheatingEntry(EntryStack entry) {
+        if (entry.getType() != EntryStack.Type.ITEM)
+            return false;
+        ItemStack cheatedStack = entry.getItemStack().copy();
         if (RoughlyEnoughItemsCore.canUsePackets()) {
             try {
-                ClientSidePacketRegistry.INSTANCE.sendToServer(RoughlyEnoughItemsNetwork.CREATE_ITEMS_PACKET, new PacketByteBuf(Unpooled.buffer()).writeItemStack(cheatedStack.copy()));
+                ClientSidePacketRegistry.INSTANCE.sendToServer(RoughlyEnoughItemsNetwork.CREATE_ITEMS_PACKET, new PacketByteBuf(Unpooled.buffer()).writeItemStack(cheatedStack));
                 return true;
             } catch (Exception e) {
                 return false;
             }
         } else {
-            Identifier identifier = Registry.ITEM.getId(cheatedStack.getItem());
+            Identifier identifier = entry.getIdentifier().orElse(null);
+            if (identifier == null)
+                return false;
             String tagMessage = cheatedStack.copy().getTag() != null && !cheatedStack.copy().getTag().isEmpty() ? cheatedStack.copy().getTag().asString() : "";
             String og = cheatedStack.getCount() == 1 ? RoughlyEnoughItemsCore.getConfigManager().getConfig().getGiveCommand().replaceAll(" \\{count}", "") : RoughlyEnoughItemsCore.getConfigManager().getConfig().getGiveCommand();
             String madeUpCommand = og.replaceAll("\\{player_name}", MinecraftClient.getInstance().player.getEntityName()).replaceAll("\\{item_name}", identifier.getPath()).replaceAll("\\{item_identifier}", identifier.toString()).replaceAll("\\{nbt}", tagMessage).replaceAll("\\{count}", String.valueOf(cheatedStack.getCount()));
@@ -178,7 +180,7 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
     }
     
     @Override
-    public boolean executeRecipeKeyBind(ItemStack stack) {
+    public boolean executeRecipeKeyBind(EntryStack stack) {
         Map<RecipeCategory<?>, List<RecipeDisplay>> map = RecipeHelper.getInstance().getRecipesFor(stack);
         if (map.keySet().size() > 0)
             openRecipeViewingScreen(map);
@@ -186,7 +188,7 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
     }
     
     @Override
-    public boolean executeUsageKeyBind(ItemStack stack) {
+    public boolean executeUsageKeyBind(EntryStack stack) {
         Map<RecipeCategory<?>, List<RecipeDisplay>> map = RecipeHelper.getInstance().getUsagesFor(stack);
         if (map.keySet().size() > 0)
             openRecipeViewingScreen(map);

+ 14 - 8
src/main/java/me/shedaniel/rei/impl/DisplayHelperImpl.java

@@ -50,12 +50,13 @@ public class DisplayHelperImpl implements DisplayHelper {
     private Map<Class<?>, DisplayBoundsHandler<?>> handlerCache = Maps.newHashMap();
     private Map<Class, List<DisplayBoundsHandler<?>>> handlerSortedCache = Maps.newHashMap();
     private BaseBoundsHandler baseBoundsHandler;
-    private Class tempScreen;
+    private Class<?> tempScreen;
     
     @Override
     public List<DisplayBoundsHandler<?>> getSortedBoundsHandlers(Class<?> screenClass) {
-        if (handlerSortedCache.containsKey(screenClass))
-            return handlerSortedCache.get(screenClass);
+        List<DisplayBoundsHandler<?>> possibleCached = handlerSortedCache.get(screenClass);
+        if (possibleCached != null)
+            return possibleCached;
         tempScreen = screenClass;
         handlerSortedCache.put(screenClass, screenDisplayBoundsHandlers.stream().filter(this::filterResponsible).sorted(BOUNDS_HANDLER_COMPARATOR).collect(Collectors.toList()));
         return handlerSortedCache.get(screenClass);
@@ -68,14 +69,16 @@ public class DisplayHelperImpl implements DisplayHelper {
     
     @Override
     public DisplayBoundsHandler<?> getResponsibleBoundsHandler(Class<?> screenClass) {
-        if (handlerCache.containsKey(screenClass))
-            return handlerCache.get(screenClass);
-        tempScreen = screenClass;
-        handlerCache.put(screenClass, screenDisplayBoundsHandlers.stream().filter(this::filterResponsible).sorted(BOUNDS_HANDLER_COMPARATOR).findAny().orElse(EMPTY));
+        DisplayBoundsHandler<?> possibleCached = handlerCache.get(screenClass);
+        if (possibleCached != null)
+            return possibleCached;
+        List<DisplayBoundsHandler<?>> handlers = getSortedBoundsHandlers(screenClass);
+        handlerCache.put(screenClass, handlers.isEmpty() ? EMPTY : handlers.get(0));
         return handlerCache.get(screenClass);
     }
     
-    public boolean filterResponsible(DisplayBoundsHandler handler) {
+    @Deprecated
+    public boolean filterResponsible(DisplayBoundsHandler<?> handler) {
         return handler.getBaseSupportedClass().isAssignableFrom(tempScreen);
     }
     
@@ -89,14 +92,17 @@ public class DisplayHelperImpl implements DisplayHelper {
         return baseBoundsHandler;
     }
     
+    @Deprecated
     public void setBaseBoundsHandler(BaseBoundsHandler baseBoundsHandler) {
         this.baseBoundsHandler = baseBoundsHandler;
     }
     
+    @Deprecated
     public void resetData() {
         screenDisplayBoundsHandlers.clear();
     }
     
+    @Deprecated
     public void resetCache() {
         handlerCache.clear();
         handlerSortedCache.clear();

+ 96 - 0
src/main/java/me/shedaniel/rei/impl/EmptyEntryStack.java

@@ -0,0 +1,96 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.impl;
+
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.Entry;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import net.minecraft.util.Identifier;
+
+import javax.annotation.Nullable;
+import java.util.Optional;
+
+@Deprecated
+public class EmptyEntryStack extends AbstractEntryStack {
+    
+    @Deprecated
+    public static final EntryStack EMPTY = new EmptyEntryStack();
+    
+    private EmptyEntryStack() {
+    }
+    
+    @Override
+    public Optional<Identifier> getIdentifier() {
+        return Optional.empty();
+    }
+    
+    @Override
+    public Type getType() {
+        return Type.EMPTY;
+    }
+    
+    @Override
+    public int getAmount() {
+        return 0;
+    }
+    
+    @Override
+    public void setAmount(int amount) {
+    
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return true;
+    }
+    
+    @Override
+    public Entry toEntry() {
+        return null;
+    }
+    
+    @Override
+    public EntryStack copy() {
+        return this;
+    }
+    
+    @Override
+    public Object getObject() {
+        return null;
+    }
+    
+    @Override
+    public boolean equalsIgnoreTagsAndAmount(EntryStack stack) {
+        return stack.getType() == getType();
+    }
+    
+    @Override
+    public boolean equalsIgnoreTags(EntryStack stack) {
+        return stack.getType() == getType();
+    }
+    
+    @Override
+    public boolean equalsIgnoreAmount(EntryStack stack) {
+        return stack.getType() == getType();
+    }
+    
+    @Override
+    public boolean equalsAll(EntryStack stack) {
+        return stack.getType() == getType();
+    }
+    
+    @Override
+    @Nullable
+    public QueuedTooltip getTooltip(int mouseX, int mouseY) {
+        return null;
+    }
+    
+    @Override
+    public void render(Rectangle bounds, int mouseX, int mouseY, float delta) {
+    
+    }
+}

+ 8 - 22
src/main/java/me/shedaniel/rei/impl/EntryRegistryImpl.java

@@ -6,15 +6,12 @@
 package me.shedaniel.rei.impl;
 
 import com.google.common.collect.Lists;
-import me.shedaniel.rei.api.Entry;
 import me.shedaniel.rei.api.EntryRegistry;
-import net.minecraft.fluid.Fluid;
+import me.shedaniel.rei.api.EntryStack;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.Items;
 import net.minecraft.util.DefaultedList;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.TreeSet;
 import java.util.concurrent.CopyOnWriteArrayList;
@@ -22,16 +19,10 @@ import java.util.stream.Collectors;
 
 public class EntryRegistryImpl implements EntryRegistry {
     
-    private final CopyOnWriteArrayList<Entry> entries = Lists.newCopyOnWriteArrayList();
+    private final CopyOnWriteArrayList<EntryStack> entries = Lists.newCopyOnWriteArrayList();
     
     @Override
-    public List<Entry> getEntryList() {
-        return Collections.unmodifiableList(entries);
-    }
-    
-    @SuppressWarnings("deprecation")
-    @Override
-    public List<Entry> getModifiableEntryList() {
+    public List<EntryStack> getStacksList() {
         return entries;
     }
     
@@ -45,22 +36,17 @@ public class EntryRegistryImpl implements EntryRegistry {
     }
     
     @Override
-    public void registerItemStack(Item afterItem, ItemStack stack) {
+    public void registerEntryAfter(EntryStack afterEntry, EntryStack stack) {
         if (!stack.isEmpty() && !alreadyContain(stack))
-            if (afterItem == null || afterItem.equals(Items.AIR))
-                entries.add(Entry.create(stack));
+            if (afterEntry == null || afterEntry.isEmpty())
+                entries.add(stack);
             else {
                 int last = entries.size();
                 for (int i = 0; i < entries.size(); i++)
-                    if (entries.get(i).getEntryType() == Entry.Type.ITEM && entries.get(i).getItemStack().getItem().equals(afterItem))
+                    if (entries.get(i).equalsAll(afterEntry))
                         last = i + 1;
-                entries.add(last, Entry.create(stack));
+                entries.add(last, stack);
             }
     }
     
-    @Override
-    public void registerFluid(Fluid fluid) {
-        entries.add(Entry.create(fluid));
-    }
-    
 }

+ 15 - 0
src/main/java/me/shedaniel/rei/impl/FluidEntry.java

@@ -6,11 +6,14 @@
 package me.shedaniel.rei.impl;
 
 import me.shedaniel.rei.api.Entry;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nullable;
 
+@ToBeRemoved
+@Deprecated
 public class FluidEntry implements Entry {
     private Fluid fluid;
     
@@ -35,4 +38,16 @@ public class FluidEntry implements Entry {
     public Fluid getFluid() {
         return fluid;
     }
+    
+    @Override
+    public Entry clone() {
+        return this;
+    }
+    
+    @Override
+    public boolean equalsEntry(Entry other, boolean checkTags) {
+        if (other.getEntryType() == Type.FLUID) {
+            return other.getFluid().matchesType(getFluid());
+        } else return false;
+    }
 }

+ 186 - 0
src/main/java/me/shedaniel/rei/impl/FluidEntryStack.java

@@ -0,0 +1,186 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.impl;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.Entry;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.gui.widget.EntryListWidget;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandler;
+import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.texture.SpriteAtlasTexture;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.fluid.Fluids;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.registry.Registry;
+
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Deprecated
+public class FluidEntryStack extends AbstractEntryStack {
+    private static final Map<Fluid, Pair<Sprite, Integer>> FLUID_SPRITE_CACHE = new HashMap<>();
+    private Fluid fluid;
+    private int amount;
+    
+    public FluidEntryStack(Fluid fluid, int amount) {
+        this.fluid = fluid;
+        this.amount = amount;
+    }
+    
+    protected static Pair<Sprite, Integer> getOrLoadSprite(Fluid fluid) {
+        Pair<Sprite, Integer> possibleCached = FLUID_SPRITE_CACHE.get(fluid);
+        if (possibleCached != null)
+            return possibleCached;
+        
+        FluidRenderHandler fluidRenderHandler = FluidRenderHandlerRegistry.INSTANCE.get(fluid);
+        if (fluidRenderHandler == null)
+            return null;
+        Sprite[] sprites = fluidRenderHandler.getFluidSprites(MinecraftClient.getInstance().world, MinecraftClient.getInstance().world == null ? null : BlockPos.ORIGIN, fluid.getDefaultState());
+        int color = -1;
+        if (MinecraftClient.getInstance().world != null)
+            color = fluidRenderHandler.getFluidColor(MinecraftClient.getInstance().world, BlockPos.ORIGIN, fluid.getDefaultState());
+        Pair<Sprite, Integer> pair = new Pair<>(sprites[0], color);
+        FLUID_SPRITE_CACHE.put(fluid, pair);
+        return pair;
+    }
+    
+    @Override
+    public Optional<Identifier> getIdentifier() {
+        return Optional.ofNullable(Registry.FLUID.getId(getFluid()));
+    }
+    
+    @Override
+    public Type getType() {
+        return Type.FLUID;
+    }
+    
+    @Override
+    public int getAmount() {
+        return amount;
+    }
+    
+    @Override
+    public void setAmount(int amount) {
+        this.amount = Math.max(amount, 0);
+        if (isEmpty()) {
+            fluid = Fluids.EMPTY;
+        }
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return amount <= 0 || fluid == Fluids.EMPTY;
+    }
+    
+    @Override
+    public Entry toEntry() {
+        return Entry.create(getFluid());
+    }
+    
+    @Override
+    public EntryStack copy() {
+        EntryStack stack = EntryStack.create(fluid, amount);
+        for (Map.Entry<Settings, Object> entry : getSettings().entrySet()) {
+            stack.setting(entry.getKey(), entry.getValue());
+        }
+        return stack;
+    }
+    
+    @Override
+    public Object getObject() {
+        return fluid;
+    }
+    
+    @Override
+    public boolean equalsIgnoreTagsAndAmount(EntryStack stack) {
+        if (stack.getType() != Type.FLUID)
+            return false;
+        return fluid == stack.getFluid();
+    }
+    
+    @Override
+    public boolean equalsIgnoreTags(EntryStack stack) {
+        if (stack.getType() != Type.FLUID)
+            return false;
+        return fluid == stack.getFluid() && amount == stack.getAmount();
+    }
+    
+    @Override
+    public boolean equalsIgnoreAmount(EntryStack stack) {
+        if (stack.getType() != Type.FLUID)
+            return false;
+        return fluid == stack.getFluid();
+    }
+    
+    @Override
+    public boolean equalsAll(EntryStack stack) {
+        if (stack.getType() != Type.FLUID)
+            return false;
+        return fluid == stack.getFluid() && amount == stack.getAmount();
+    }
+    
+    @Nullable
+    @Override
+    public QueuedTooltip getTooltip(int mouseX, int mouseY) {
+        if (!getSetting(Settings.TOOLTIP_ENABLED).value().get() || isEmpty())
+            return null;
+        List<String> toolTip = Lists.newArrayList(EntryListWidget.tryGetEntryStackName(this));
+        toolTip.addAll(getSetting(Settings.TOOLTIP_APPEND_EXTRA).value().apply(this));
+        if (getSetting(Settings.TOOLTIP_APPEND_MOD).value().get() && RoughlyEnoughItemsCore.getConfigManager().getConfig().shouldAppendModNames()) {
+            final String modString = ClientHelper.getInstance().getFormattedModFromIdentifier(Registry.FLUID.getId(fluid));
+            boolean alreadyHasMod = false;
+            for (String s : toolTip)
+                if (s.equalsIgnoreCase(modString)) {
+                    alreadyHasMod = true;
+                    break;
+                }
+            if (!alreadyHasMod)
+                toolTip.add(modString);
+        }
+        return QueuedTooltip.create(toolTip);
+    }
+    
+    @Override
+    public void render(Rectangle bounds, int mouseX, int mouseY, float delta) {
+        if (getSetting(Settings.RENDER).value().get()) {
+            Pair<Sprite, Integer> pair = getOrLoadSprite(getFluid());
+            if (pair != null) {
+                Sprite sprite = pair.getLeft();
+                int color = pair.getRight();
+                int a = 255;
+                int r = (color >> 16 & 255);
+                int g = (color >> 8 & 255);
+                int b = (color & 255);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(SpriteAtlasTexture.BLOCK_ATLAS_TEX);
+                GuiLighting.disable();
+                Tessellator tess = Tessellator.getInstance();
+                BufferBuilder bb = tess.getBufferBuilder();
+                bb.begin(7, VertexFormats.POSITION_UV_COLOR);
+                bb.vertex(bounds.getMaxX(), bounds.y, getZ()).texture(sprite.getMaxU(), sprite.getMinV()).color(r, g, b, a).next();
+                bb.vertex(bounds.x, bounds.y, getZ()).texture(sprite.getMinU(), sprite.getMinV()).color(r, g, b, a).next();
+                bb.vertex(bounds.x, bounds.getMaxY(), getZ()).texture(sprite.getMinU(), sprite.getMaxV()).color(r, g, b, a).next();
+                bb.vertex(bounds.getMaxX(), bounds.getMaxY(), getZ()).texture(sprite.getMaxU(), sprite.getMaxV()).color(r, g, b, a).next();
+                tess.draw();
+            }
+        }
+    }
+}

+ 158 - 0
src/main/java/me/shedaniel/rei/impl/ItemEntryStack.java

@@ -0,0 +1,158 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.impl;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.compat.RenderHelper;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.Entry;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.ItemStackRenderOverlayHook;
+import me.shedaniel.rei.gui.widget.EntryListWidget;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.render.item.ItemRenderer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.registry.Registry;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+@Deprecated
+public class ItemEntryStack extends AbstractEntryStack {
+    
+    private ItemStack itemStack;
+    
+    public ItemEntryStack(ItemStack itemStack) {
+        this.itemStack = itemStack;
+    }
+    
+    @Override
+    public Optional<Identifier> getIdentifier() {
+        return Optional.ofNullable(Registry.ITEM.getId(getItem()));
+    }
+    
+    @Override
+    public Type getType() {
+        return Type.ITEM;
+    }
+    
+    @Override
+    public int getAmount() {
+        return itemStack.getCount();
+    }
+    
+    @Override
+    public void setAmount(int amount) {
+        itemStack.setCount(amount);
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return itemStack.isEmpty();
+    }
+    
+    @Override
+    public Entry toEntry() {
+        return Entry.create(getItemStack());
+    }
+    
+    @Override
+    public EntryStack copy() {
+        EntryStack stack = EntryStack.create(getItemStack().copy());
+        for (Map.Entry<Settings, Object> entry : getSettings().entrySet()) {
+            stack.setting(entry.getKey(), entry.getValue());
+        }
+        return stack;
+    }
+    
+    @Override
+    public Object getObject() {
+        return itemStack;
+    }
+    
+    @Override
+    public boolean equalsIgnoreTagsAndAmount(EntryStack stack) {
+        if (stack.getType() != Type.ITEM)
+            return false;
+        return itemStack.getItem() == stack.getItem();
+    }
+    
+    @Override
+    public boolean equalsAll(EntryStack stack) {
+        if (stack.getType() != Type.ITEM)
+            return false;
+        if (itemStack.getItem() != stack.getItem() || getAmount() != stack.getAmount())
+            return false;
+        return ItemStack.areTagsEqual(itemStack, stack.getItemStack());
+    }
+    
+    @Override
+    public boolean equalsIgnoreAmount(EntryStack stack) {
+        if (stack.getType() != Type.ITEM)
+            return false;
+        if (itemStack.getItem() != stack.getItem())
+            return false;
+        return ItemStack.areTagsEqual(itemStack, stack.getItemStack());
+    }
+    
+    @Override
+    public boolean equalsIgnoreTags(EntryStack stack) {
+        if (stack.getType() != Type.ITEM)
+            return false;
+        if (itemStack.getItem() != stack.getItem())
+            return false;
+        return getAmount() == stack.getAmount();
+    }
+    
+    @Nullable
+    @Override
+    public QueuedTooltip getTooltip(int mouseX, int mouseY) {
+        if (!getSetting(Settings.TOOLTIP_ENABLED).value().get() || isEmpty())
+            return null;
+        List<String> toolTip = Lists.newArrayList(EntryListWidget.tryGetItemStackToolTip(getItemStack(), true));
+        toolTip.addAll(getSetting(Settings.TOOLTIP_APPEND_EXTRA).value().apply(this));
+        if (getSetting(Settings.TOOLTIP_APPEND_MOD).value().get() && RoughlyEnoughItemsCore.getConfigManager().getConfig().shouldAppendModNames()) {
+            final String modString = ClientHelper.getInstance().getFormattedModFromItem(getItem());
+            boolean alreadyHasMod = false;
+            for (String s : toolTip)
+                if (s.equalsIgnoreCase(modString)) {
+                    alreadyHasMod = true;
+                    break;
+                }
+            if (!alreadyHasMod)
+                toolTip.add(modString);
+        }
+        return QueuedTooltip.create(toolTip);
+    }
+    
+    @Override
+    public void render(Rectangle bounds, int mouseX, int mouseY, float delta) {
+        if (getSetting(Settings.RENDER).value().get()) {
+            ItemStack stack = getItemStack().copy();
+            ((ItemStackRenderOverlayHook) (Object) stack).rei_setRenderOverlay(getSetting(Settings.Item.RENDER_OVERLAY).value().get());
+            RenderHelper.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+            ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+            itemRenderer.zOffset = getZ();
+            GuiLighting.enableForItems();
+            RenderHelper.colorMask(true, true, true, true);
+            RenderHelper.enableLighting();
+            RenderHelper.enableRescaleNormal();
+            RenderHelper.enableDepthTest();
+            int i1 = bounds.getCenterX() - 8;
+            int i2 = bounds.getCenterY() - 8;
+            itemRenderer.renderGuiItem(stack, i1, i2);
+            itemRenderer.renderGuiItemOverlay(MinecraftClient.getInstance().textRenderer, stack, i1, i2, getSetting(Settings.RENDER_COUNTS).value().get() ? getSetting(Settings.COUNTS).value().apply(this) : "");
+            itemRenderer.zOffset = 0.0F;
+        }
+    }
+}

+ 15 - 0
src/main/java/me/shedaniel/rei/impl/ItemStackEntry.java

@@ -6,11 +6,14 @@
 package me.shedaniel.rei.impl;
 
 import me.shedaniel.rei.api.Entry;
+import me.shedaniel.rei.api.annotations.ToBeRemoved;
 import net.minecraft.fluid.Fluid;
 import net.minecraft.item.ItemStack;
 
 import javax.annotation.Nullable;
 
+@ToBeRemoved
+@Deprecated
 public class ItemStackEntry implements Entry {
     private ItemStack itemStack;
     
@@ -35,4 +38,16 @@ public class ItemStackEntry implements Entry {
     public Fluid getFluid() {
         return null;
     }
+    
+    @Override
+    public Entry clone() {
+        return Entry.create(getItemStack().copy());
+    }
+    
+    @Override
+    public boolean equalsEntry(Entry other, boolean checkTags) {
+        if (other.getEntryType() == Type.ITEM) {
+            return checkTags ? ItemStack.areEqualIgnoreDamage(other.getItemStack(), getItemStack()) : other.getItemStack().isItemEqualIgnoreDamage(getItemStack());
+        } else return false;
+    }
 }

+ 51 - 0
src/main/java/me/shedaniel/rei/impl/ObjectHolderImpl.java

@@ -0,0 +1,51 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.impl;
+
+import me.shedaniel.rei.api.ObjectHolder;
+
+public class ObjectHolderImpl<T> implements ObjectHolder<T> {
+    private Object o;
+    
+    public ObjectHolderImpl(Object o) {
+        this.o = o;
+    }
+    
+    @Override
+    public int intValue() {
+        return (int) o;
+    }
+    
+    @Override
+    public long longValue() {
+        return (long) o;
+    }
+    
+    @Override
+    public boolean booleanValue() {
+        return (boolean) o;
+    }
+    
+    @Override
+    public float floatValue() {
+        return (float) o;
+    }
+    
+    @Override
+    public double doubleValue() {
+        return (double) o;
+    }
+    
+    @Override
+    public String stringValue() {
+        return (String) o;
+    }
+    
+    @Override
+    public T value() {
+        return (T) o;
+    }
+}

+ 28 - 21
src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java

@@ -55,21 +55,22 @@ public class RecipeHelperImpl implements RecipeHelper {
     private RecipeManager recipeManager;
     
     @Override
-    public List<ItemStack> findCraftableByItems(List<ItemStack> inventoryItems) {
-        List<ItemStack> craftables = new ArrayList<>();
+    public List<EntryStack> findCraftableEntriesByItems(List<ItemStack> inventoryItems) {
+        List<EntryStack> craftables = new ArrayList<>();
         for (List<RecipeDisplay> value : recipeCategoryListMap.values())
             for (RecipeDisplay recipeDisplay : value) {
                 int slotsCraftable = 0;
-                List<List<ItemStack>> requiredInput = (List<List<ItemStack>>) recipeDisplay.getRequiredItems();
-                for (List<ItemStack> slot : requiredInput) {
+                List<List<EntryStack>> requiredInput = (List<List<EntryStack>>) recipeDisplay.getRequiredEntries();
+                for (List<EntryStack> slot : requiredInput) {
                     if (slot.isEmpty()) {
                         slotsCraftable++;
                         continue;
                     }
                     boolean slotDone = false;
                     for (ItemStack possibleType : inventoryItems) {
-                        for (ItemStack slotPossible : slot)
-                            if (ItemStack.areItemsEqualIgnoreDamage(slotPossible, possibleType)) {
+                        EntryStack possibleEntryStack = EntryStack.create(possibleType);
+                        for (EntryStack slotPossible : slot)
+                            if (possibleEntryStack.equals(slotPossible)) {
                                 slotsCraftable++;
                                 slotDone = true;
                                 break;
@@ -78,8 +79,8 @@ public class RecipeHelperImpl implements RecipeHelper {
                             break;
                     }
                 }
-                if (slotsCraftable == recipeDisplay.getRequiredItems().size())
-                    craftables.addAll((List<ItemStack>) recipeDisplay.getOutput());
+                if (slotsCraftable == recipeDisplay.getRequiredEntries().size())
+                    craftables.addAll((List<EntryStack>) recipeDisplay.getOutputEntries());
             }
         return craftables.stream().distinct().collect(Collectors.toList());
     }
@@ -122,18 +123,21 @@ public class RecipeHelperImpl implements RecipeHelper {
     }
     
     @Override
-    public Map<RecipeCategory<?>, List<RecipeDisplay>> getRecipesFor(ItemStack stack) {
+    public Map<RecipeCategory<?>, List<RecipeDisplay>> getRecipesFor(EntryStack stack) {
         Map<Identifier, List<RecipeDisplay>> categoriesMap = new HashMap<>();
         categories.forEach(f -> categoriesMap.put(f.getIdentifier(), Lists.newArrayList()));
         for (Map.Entry<Identifier, List<RecipeDisplay>> entry : recipeCategoryListMap.entrySet()) {
             RecipeCategory category = getCategory(entry.getKey());
             for (RecipeDisplay recipeDisplay : entry.getValue())
-                for (ItemStack outputStack : (List<ItemStack>) recipeDisplay.getOutput())
-                    if (category.checkTags() ? ItemStack.areEqualIgnoreDamage(stack, outputStack) : ItemStack.areItemsEqualIgnoreDamage(stack, outputStack))
+                for (EntryStack outputStack : recipeDisplay.getOutputEntries())
+                    if (stack.equals(outputStack))
                         categoriesMap.get(recipeDisplay.getRecipeCategory()).add(recipeDisplay);
         }
-        for (LiveRecipeGenerator liveRecipeGenerator : liveRecipeGenerators)
-            ((Optional<List>) liveRecipeGenerator.getRecipeFor(stack)).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll(o));
+        for (LiveRecipeGenerator<?> liveRecipeGenerator : liveRecipeGenerators) {
+            liveRecipeGenerator.getRecipeFor(stack).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll((List<RecipeDisplay>) o));
+            if (stack.getType() == EntryStack.Type.ITEM)
+                liveRecipeGenerator.getRecipeFor(stack.getItemStack()).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll((List<RecipeDisplay>) o));
+        }
         Map<RecipeCategory<?>, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
         categories.forEach(category -> {
             if (categoriesMap.containsKey(category.getIdentifier()) && !categoriesMap.get(category.getIdentifier()).isEmpty())
@@ -156,16 +160,16 @@ public class RecipeHelperImpl implements RecipeHelper {
     }
     
     @Override
-    public Map<RecipeCategory<?>, List<RecipeDisplay>> getUsagesFor(ItemStack stack) {
+    public Map<RecipeCategory<?>, List<RecipeDisplay>> getUsagesFor(EntryStack stack) {
         Map<Identifier, List<RecipeDisplay>> categoriesMap = new HashMap<>();
         categories.forEach(f -> categoriesMap.put(f.getIdentifier(), Lists.newArrayList()));
         for (Map.Entry<Identifier, List<RecipeDisplay>> entry : recipeCategoryListMap.entrySet()) {
             RecipeCategory category = getCategory(entry.getKey());
             for (RecipeDisplay recipeDisplay : entry.getValue()) {
                 boolean found = false;
-                for (List<ItemStack> input : (List<List<ItemStack>>) recipeDisplay.getInput()) {
-                    for (ItemStack itemStack : input) {
-                        if (category.checkTags() ? ItemStack.areEqualIgnoreDamage(itemStack, stack) : ItemStack.areItemsEqualIgnoreDamage(itemStack, stack)) {
+                for (List<EntryStack> input : recipeDisplay.getInputEntries()) {
+                    for (EntryStack otherEntry : input) {
+                        if (otherEntry.equals(stack)) {
                             categoriesMap.get(recipeDisplay.getRecipeCategory()).add(recipeDisplay);
                             found = true;
                             break;
@@ -176,8 +180,11 @@ public class RecipeHelperImpl implements RecipeHelper {
                 }
             }
         }
-        for (LiveRecipeGenerator liveRecipeGenerator : liveRecipeGenerators)
-            ((Optional<List>) liveRecipeGenerator.getUsageFor(stack)).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll(o));
+        for (LiveRecipeGenerator<?> liveRecipeGenerator : liveRecipeGenerators) {
+            liveRecipeGenerator.getUsageFor(stack).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll((List<RecipeDisplay>) o));
+            if (stack.getType() == EntryStack.Type.ITEM)
+                liveRecipeGenerator.getUsageFor(stack.getItemStack()).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll((List<RecipeDisplay>) o));
+        }
         Map<RecipeCategory<?>, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
         categories.forEach(category -> {
             if (categoriesMap.containsKey(category.getIdentifier()) && !categoriesMap.get(category.getIdentifier()).isEmpty())
@@ -254,11 +261,11 @@ public class RecipeHelperImpl implements RecipeHelper {
                         if (((REIPluginV0) plugin).getMinimumVersion().compareTo((SemanticVersion) reiVersion) > 0) {
                             throw new IllegalStateException("Requires " + ((REIPluginV0) plugin).getMinimumVersion().getFriendlyString() + " version of REI!");
                         }
+                    ((REIPluginV0) plugin).registerBounds(RoughlyEnoughItemsCore.getDisplayHelper());
+                    ((REIPluginV0) plugin).registerEntries(RoughlyEnoughItemsCore.getEntryRegistry());
                     ((REIPluginV0) plugin).registerPluginCategories(this);
                     ((REIPluginV0) plugin).registerRecipeDisplays(this);
-                    ((REIPluginV0) plugin).registerBounds(RoughlyEnoughItemsCore.getDisplayHelper());
                     ((REIPluginV0) plugin).registerOthers(this);
-                    ((REIPluginV0) plugin).registerEntries(RoughlyEnoughItemsCore.getEntryRegistry());
                 } else {
                     throw new IllegalStateException("Invaild Plugin Class!");
                 }

+ 15 - 2
src/main/java/me/shedaniel/rei/impl/ScreenHelper.java

@@ -11,7 +11,7 @@ import com.google.common.collect.Sets;
 import me.shedaniel.cloth.hooks.ClothClientHooks;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
-import me.shedaniel.rei.gui.widget.SearchFieldWidget;
+import me.shedaniel.rei.gui.OverlaySearchField;
 import me.shedaniel.rei.listeners.ContainerScreenHooks;
 import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.fabric.api.event.client.ClientTickCallback;
@@ -29,13 +29,26 @@ import java.util.Optional;
 
 public class ScreenHelper implements ClientModInitializer {
     
-    public static SearchFieldWidget searchField;
+    /**
+     * @deprecated Use getters instead
+     */
+    @Deprecated
+    public static OverlaySearchField searchField;
     public static List<ItemStack> inventoryStacks = Lists.newArrayList();
     private static boolean overlayVisible = true;
     private static ContainerScreenOverlay overlay;
     private static AbstractContainerScreen<?> lastContainerScreen = null;
     private static LinkedHashSet<Screen> lastRecipeScreen = Sets.newLinkedHashSetWithExpectedSize(5);
     
+    public static OverlaySearchField getSearchField() {
+        return searchField;
+    }
+    
+    @Deprecated
+    public static void setSearchField(OverlaySearchField searchField) {
+        ScreenHelper.searchField = searchField;
+    }
+    
     public static void storeRecipeScreen(Screen screen) {
         while (lastRecipeScreen.size() >= 5)
             lastRecipeScreen.remove(Iterables.get(lastRecipeScreen, 0));

+ 8 - 6
src/main/java/me/shedaniel/rei/mixin/MixinBrewingRecipeRegistry.java

@@ -11,6 +11,7 @@ import me.shedaniel.rei.plugin.brewing.BrewingRecipe;
 import me.shedaniel.rei.plugin.brewing.DefaultBrewingDisplay;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemConvertible;
+import net.minecraft.item.ItemStack;
 import net.minecraft.item.PotionItem;
 import net.minecraft.potion.Potion;
 import net.minecraft.potion.PotionUtil;
@@ -22,7 +23,6 @@ import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
-import java.util.Arrays;
 import java.util.List;
 
 @Mixin(BrewingRecipeRegistry.class)
@@ -53,16 +53,18 @@ public class MixinBrewingRecipeRegistry {
             rei_registerPotionType(potion_1);
         if (!REGISTERED_POTION_TYPES.contains(potion_2))
             rei_registerPotionType(potion_2);
-        SELF_POTION_TYPES.stream().map(Ingredient::getMatchingStacksClient).forEach(itemStacks -> Arrays.stream(itemStacks).forEach(stack -> {
-            DefaultPlugin.registerBrewingDisplay(new DefaultBrewingDisplay(PotionUtil.setPotion(stack.copy(), potion_1), Ingredient.ofItems(new ItemConvertible[]{item_1}), PotionUtil.setPotion(stack.copy(), potion_2)));
-        }));
+        for (Ingredient type : SELF_POTION_TYPES) {
+            for (ItemStack stack : type.getStackArray()) {
+                DefaultPlugin.registerBrewingDisplay(new DefaultBrewingDisplay(PotionUtil.setPotion(stack.copy(), potion_1), Ingredient.ofItems(new ItemConvertible[]{item_1}), PotionUtil.setPotion(stack.copy(), potion_2)));
+            }
+        }
     }
     
     private static void rei_registerPotionType(Potion potion) {
         REGISTERED_POTION_TYPES.add(potion);
-        SELF_ITEM_RECIPES.forEach(recipe -> {
+        for (BrewingRecipe recipe : SELF_ITEM_RECIPES) {
             DefaultPlugin.registerBrewingDisplay(new DefaultBrewingDisplay(PotionUtil.setPotion(recipe.input.getStackForRender(), potion), recipe.ingredient, PotionUtil.setPotion(recipe.output.getStackForRender(), potion)));
-        });
+        }
     }
     
 }

+ 43 - 37
src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java

@@ -7,17 +7,17 @@ package me.shedaniel.rei.plugin;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import it.unimi.dsi.fastutil.objects.Object2FloatMap;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.DisplayHelper;
-import me.shedaniel.rei.api.Entry;
 import me.shedaniel.rei.api.EntryRegistry;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeHelper;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
 import me.shedaniel.rei.impl.ScreenHelper;
-import me.shedaniel.rei.plugin.blasting.DefaultBlastingCategory;
 import me.shedaniel.rei.plugin.blasting.DefaultBlastingDisplay;
 import me.shedaniel.rei.plugin.brewing.DefaultBrewingCategory;
 import me.shedaniel.rei.plugin.brewing.DefaultBrewingDisplay;
@@ -25,13 +25,12 @@ import me.shedaniel.rei.plugin.campfire.DefaultCampfireCategory;
 import me.shedaniel.rei.plugin.campfire.DefaultCampfireDisplay;
 import me.shedaniel.rei.plugin.composting.DefaultCompostingCategory;
 import me.shedaniel.rei.plugin.composting.DefaultCompostingDisplay;
+import me.shedaniel.rei.plugin.cooking.DefaultCookingCategory;
 import me.shedaniel.rei.plugin.crafting.DefaultCraftingCategory;
 import me.shedaniel.rei.plugin.crafting.DefaultCustomDisplay;
 import me.shedaniel.rei.plugin.crafting.DefaultShapedDisplay;
 import me.shedaniel.rei.plugin.crafting.DefaultShapelessDisplay;
-import me.shedaniel.rei.plugin.smelting.DefaultSmeltingCategory;
 import me.shedaniel.rei.plugin.smelting.DefaultSmeltingDisplay;
-import me.shedaniel.rei.plugin.smoking.DefaultSmokingCategory;
 import me.shedaniel.rei.plugin.smoking.DefaultSmokingDisplay;
 import me.shedaniel.rei.plugin.stonecutting.DefaultStoneCuttingCategory;
 import me.shedaniel.rei.plugin.stonecutting.DefaultStoneCuttingDisplay;
@@ -47,6 +46,8 @@ import net.minecraft.client.gui.screen.recipebook.RecipeBookProvider;
 import net.minecraft.enchantment.Enchantment;
 import net.minecraft.enchantment.EnchantmentHelper;
 import net.minecraft.fluid.EmptyFluid;
+import net.minecraft.fluid.Fluid;
+import net.minecraft.item.Item;
 import net.minecraft.item.ItemConvertible;
 import net.minecraft.item.ItemStack;
 import net.minecraft.item.Items;
@@ -97,26 +98,28 @@ public class DefaultPlugin implements REIPluginV0 {
         if (!RoughlyEnoughItemsCore.getConfigManager().getConfig().isLoadingDefaultPlugin()) {
             return;
         }
-        Registry.ITEM.stream().forEach(item -> {
-            entryRegistry.registerItemStack(item.getStackForRender());
+        for (Item item : Registry.ITEM) {
+            entryRegistry.registerEntry(EntryStack.create(item));
             try {
-                entryRegistry.registerItemStack(entryRegistry.getAllStacksFromItem(item));
+                for (ItemStack stack : entryRegistry.getAllStacksFromItem(item)) {
+                    entryRegistry.registerEntry(EntryStack.create(stack));
+                }
             } catch (Exception e) {
             }
-        });
-        Registry.ENCHANTMENT.forEach(enchantment -> {
+        }
+        for (Enchantment enchantment : Registry.ENCHANTMENT) {
             for (int i = enchantment.getMinimumLevel(); i <= enchantment.getMaximumLevel(); i++) {
                 Map<Enchantment, Integer> map = new HashMap<>();
                 map.put(enchantment, i);
                 ItemStack itemStack = new ItemStack(Items.ENCHANTED_BOOK);
                 EnchantmentHelper.set(map, itemStack);
-                entryRegistry.registerItemStack(Items.ENCHANTED_BOOK, itemStack);
+                entryRegistry.registerEntriesAfter(EntryStack.create(Items.ENCHANTED_BOOK), EntryStack.create(itemStack));
             }
-        });
-        Registry.FLUID.forEach(fluid -> {
+        }
+        for (Fluid fluid : Registry.FLUID) {
             if (!(fluid instanceof EmptyFluid))
-                entryRegistry.registerFluid(fluid);
-        });
+                entryRegistry.registerEntry(EntryStack.create(fluid));
+        }
     }
     
     @Override
@@ -125,9 +128,9 @@ public class DefaultPlugin implements REIPluginV0 {
             return;
         }
         recipeHelper.registerCategory(new DefaultCraftingCategory());
-        recipeHelper.registerCategory(new DefaultSmeltingCategory());
-        recipeHelper.registerCategory(new DefaultSmokingCategory());
-        recipeHelper.registerCategory(new DefaultBlastingCategory());
+        recipeHelper.registerCategory(new DefaultCookingCategory(SMELTING, EntryStack.create(Items.FURNACE), "category.rei.smelting"));
+        recipeHelper.registerCategory(new DefaultCookingCategory(SMOKING, EntryStack.create(Items.SMOKER), "category.rei.smoking"));
+        recipeHelper.registerCategory(new DefaultCookingCategory(BLASTING, EntryStack.create(Items.BLAST_FURNACE), "category.rei.blasting"));
         recipeHelper.registerCategory(new DefaultCampfireCategory());
         recipeHelper.registerCategory(new DefaultStoneCuttingCategory());
         recipeHelper.registerCategory(new DefaultBrewingCategory());
@@ -147,29 +150,32 @@ public class DefaultPlugin implements REIPluginV0 {
         recipeHelper.registerRecipes(BLASTING, BlastingRecipe.class, DefaultBlastingDisplay::new);
         recipeHelper.registerRecipes(CAMPFIRE, CampfireCookingRecipe.class, DefaultCampfireDisplay::new);
         recipeHelper.registerRecipes(STONE_CUTTING, StonecuttingRecipe.class, DefaultStoneCuttingDisplay::new);
-        BREWING_DISPLAYS.stream().forEachOrdered(display -> recipeHelper.registerDisplay(BREWING, display));
-        List<ItemStack> arrowStack = Collections.singletonList(Items.ARROW.getStackForRender());
-        RoughlyEnoughItemsCore.getEntryRegistry().getEntryList().stream().filter(stack -> stack.getEntryType() == Entry.Type.ITEM && stack.getItemStack().getItem().equals(Items.LINGERING_POTION)).forEach(entry -> {
-            List<List<ItemStack>> input = new ArrayList<>();
-            for (int i = 0; i < 4; i++)
-                input.add(arrowStack);
-            input.add(Collections.singletonList(entry.getItemStack()));
-            for (int i = 0; i < 4; i++)
-                input.add(arrowStack);
-            ItemStack outputStack = new ItemStack(Items.TIPPED_ARROW, 8);
-            PotionUtil.setPotion(outputStack, PotionUtil.getPotion(entry.getItemStack()));
-            PotionUtil.setCustomPotionEffects(outputStack, PotionUtil.getCustomPotionEffects(entry.getItemStack()));
-            List<ItemStack> output = Collections.singletonList(outputStack);
-            recipeHelper.registerDisplay(CRAFTING, new DefaultCustomDisplay(input, output));
-        });
+        for (DefaultBrewingDisplay display : BREWING_DISPLAYS) {
+            recipeHelper.registerDisplay(BREWING, display);
+        }
+        List<EntryStack> arrowStack = Collections.singletonList(EntryStack.create(Items.ARROW));
+        for (EntryStack entry : RoughlyEnoughItemsCore.getEntryRegistry().getStacksList()) {
+            if (entry.getItem() == Items.LINGERING_POTION) {
+                List<List<EntryStack>> input = new ArrayList<>();
+                for (int i = 0; i < 4; i++)
+                    input.add(arrowStack);
+                input.add(Collections.singletonList(EntryStack.create(entry.getItemStack())));
+                for (int i = 0; i < 4; i++)
+                    input.add(arrowStack);
+                ItemStack outputStack = new ItemStack(Items.TIPPED_ARROW, 8);
+                PotionUtil.setPotion(outputStack, PotionUtil.getPotion(entry.getItemStack()));
+                PotionUtil.setCustomPotionEffects(outputStack, PotionUtil.getCustomPotionEffects(entry.getItemStack()));
+                List<EntryStack> output = Collections.singletonList(EntryStack.create(outputStack).addSetting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
+                recipeHelper.registerDisplay(CRAFTING, new DefaultCustomDisplay(null, input, output));
+            }
+        }
         Map<ItemConvertible, Float> map = Maps.newLinkedHashMap();
         if (ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE.isEmpty())
             ComposterBlock.registerDefaultCompostableItems();
-        ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE.keySet().forEach(itemConvertible -> {
-            float chance = ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE.getOrDefault(itemConvertible, 0);
-            if (chance > 0)
-                map.put(itemConvertible, chance);
-        });
+        for (Object2FloatMap.Entry<ItemConvertible> entry : ComposterBlock.ITEM_TO_LEVEL_INCREASE_CHANCE.object2FloatEntrySet()) {
+            if (entry.getFloatValue() > 0)
+                map.put(entry.getKey(), entry.getFloatValue());
+        }
         List<ItemConvertible> stacks = new LinkedList<>(map.keySet());
         stacks.sort((first, second) -> {
             return (int) ((map.get(first) - map.get(second)) * 100);

+ 13 - 9
src/main/java/me/shedaniel/rei/plugin/autocrafting/DefaultCategoryHandler.java

@@ -10,6 +10,7 @@ import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
 import me.shedaniel.rei.RoughlyEnoughItemsNetwork;
 import me.shedaniel.rei.api.AutoTransferHandler;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.TransferRecipeDisplay;
 import me.shedaniel.rei.listeners.RecipeBookGuiHooks;
 import me.shedaniel.rei.server.ContainerInfo;
@@ -43,7 +44,7 @@ public class DefaultCategoryHandler implements AutoTransferHandler {
             return Result.createNotApplicable();
         if (recipe.getHeight() > containerInfo.getCraftingHeight(container) || recipe.getWidth() > containerInfo.getCraftingWidth(container))
             return Result.createFailed(I18n.translate("error.rei.transfer.too_small", containerInfo.getCraftingWidth(container), containerInfo.getCraftingHeight(container)));
-        List<List<ItemStack>> input = recipe.getOrganisedInput(containerInfo, container);
+        List<List<EntryStack>> input = recipe.getOrganisedInputEntries(containerInfo, container);
         IntList intList = hasItems(input);
         if (!intList.isEmpty())
             return Result.createFailed("error.rei.not.enough.materials", intList);
@@ -60,10 +61,12 @@ public class DefaultCategoryHandler implements AutoTransferHandler {
         buf.writeBoolean(Screen.hasShiftDown());
         
         buf.writeInt(input.size());
-        for (List<ItemStack> stacks : input) {
+        for (List<EntryStack> stacks : input) {
             buf.writeInt(stacks.size());
-            for (ItemStack stack : stacks) {
-                buf.writeItemStack(stack);
+            for (EntryStack stack : stacks) {
+                if (stack.getItemStack() != null)
+                    buf.writeItemStack(stack.getItemStack());
+                else buf.writeItemStack(ItemStack.EMPTY);
             }
         }
         ClientSidePacketRegistry.INSTANCE.sendToServer(RoughlyEnoughItemsNetwork.MOVE_ITEMS_PACKET, buf);
@@ -79,7 +82,7 @@ public class DefaultCategoryHandler implements AutoTransferHandler {
         return ClientSidePacketRegistry.INSTANCE.canServerReceive(RoughlyEnoughItemsNetwork.MOVE_ITEMS_PACKET);
     }
     
-    public IntList hasItems(List<List<ItemStack>> inputs) {
+    public IntList hasItems(List<List<EntryStack>> inputs) {
         // Create a clone of player's inventory, and count
         DefaultedList<ItemStack> copyMain = DefaultedList.of();
         for (ItemStack stack : MinecraftClient.getInstance().player.inventory.main) {
@@ -87,13 +90,14 @@ public class DefaultCategoryHandler implements AutoTransferHandler {
         }
         IntList intList = new IntArrayList();
         for (int i = 0; i < inputs.size(); i++) {
-            List<ItemStack> possibleStacks = inputs.get(i);
+            List<EntryStack> possibleStacks = inputs.get(i);
             boolean done = possibleStacks.isEmpty();
-            for (ItemStack possibleStack : possibleStacks) {
+            for (EntryStack possibleStack : possibleStacks) {
                 if (!done) {
-                    int invRequiredCount = possibleStack.getCount();
+                    int invRequiredCount = possibleStack.getAmount();
                     for (ItemStack stack : copyMain) {
-                        if (ItemStack.areItemsEqualIgnoreDamage(possibleStack, stack)) {
+                        EntryStack entryStack = EntryStack.create(stack);
+                        if (entryStack.equals(possibleStack)) {
                             while (invRequiredCount > 0 && !stack.isEmpty()) {
                                 invRequiredCount--;
                                 stack.decrement(1);

+ 0 - 92
src/main/java/me/shedaniel/rei/plugin/blasting/DefaultBlastingCategory.java

@@ -1,92 +0,0 @@
-/*
- * Roughly Enough Items by Danielshe.
- * Licensed under the MIT License.
- */
-
-package me.shedaniel.rei.plugin.blasting;
-
-import com.mojang.blaze3d.systems.RenderSystem;
-import it.unimi.dsi.fastutil.ints.IntList;
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
-import me.shedaniel.rei.api.Renderer;
-import me.shedaniel.rei.api.TransferRecipeCategory;
-import me.shedaniel.rei.gui.renderers.RecipeRenderer;
-import me.shedaniel.rei.gui.widget.RecipeArrowWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
-import me.shedaniel.rei.gui.widget.Widget;
-import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.block.Blocks;
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gui.DrawableHelper;
-import net.minecraft.client.render.GuiLighting;
-import net.minecraft.client.resource.language.I18n;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.function.Supplier;
-
-public class DefaultBlastingCategory implements TransferRecipeCategory<DefaultBlastingDisplay> {
-    
-    @Override
-    public Identifier getIdentifier() {
-        return DefaultPlugin.BLASTING;
-    }
-    
-    @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.BLAST_FURNACE));
-    }
-    
-    @Override
-    public String getCategoryName() {
-        return I18n.translate("category.rei.blasting");
-    }
-    
-    @Override
-    public RecipeRenderer getSimpleRenderer(DefaultBlastingDisplay recipe) {
-        return Renderer.fromRecipe(() -> Arrays.asList(recipe.getInput().get(0)), recipe::getOutput);
-    }
-    
-    @Override
-    public List<Widget> setupDisplay(Supplier<DefaultBlastingDisplay> recipeDisplaySupplier, Rectangle bounds) {
-        final DefaultBlastingDisplay recipeDisplay = recipeDisplaySupplier.get();
-        Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
-        List<Widget> widgets = new LinkedList<>(Arrays.asList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-                GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 54, 82, 54);
-                int height = MathHelper.ceil((System.currentTimeMillis() / 250 % 14d) / 1f);
-                blit(startPoint.x + 2, startPoint.y + 21 + (14 - height), 82, 77 + (14 - height), 14, height);
-            }
-        }));
-        widgets.add(new RecipeArrowWidget(startPoint.x + 24, startPoint.y + 18, true));
-        List<List<ItemStack>> input = recipeDisplay.getInput();
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, Renderer.fromItemStacks(input.get(0)), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 37, Renderer.fromItemStacks(() -> recipeDisplay.getFuel(), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.smelting.fuel"))), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, Renderer.fromItemStacks(recipeDisplay.getOutput()), false, true, true));
-        return widgets;
-    }
-    
-    @Override
-    public void renderRedSlots(List<Widget> widgets, Rectangle bounds, DefaultBlastingDisplay display, IntList redSlots) {
-        Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
-        RenderSystem.translatef(0, 0, 400);
-        if (redSlots.contains(0)) {
-            DrawableHelper.fill(startPoint.x + 1, startPoint.y + 1, startPoint.x + 1 + 16, startPoint.y + 1 + 16, 822018048);
-        }
-        RenderSystem.translatef(0, 0, -400);
-    }
-    
-}

+ 3 - 65
src/main/java/me/shedaniel/rei/plugin/blasting/DefaultBlastingDisplay.java

@@ -5,81 +5,19 @@
 
 package me.shedaniel.rei.plugin.blasting;
 
-import me.shedaniel.rei.api.TransferRecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import me.shedaniel.rei.server.ContainerInfo;
-import net.minecraft.block.entity.FurnaceBlockEntity;
-import net.minecraft.container.Container;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.recipe.AbstractCookingRecipe;
+import me.shedaniel.rei.plugin.cooking.DefaultCookingDisplay;
 import net.minecraft.recipe.BlastingRecipe;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-public class DefaultBlastingDisplay implements TransferRecipeDisplay {
-    
-    private BlastingRecipe display;
-    private List<List<ItemStack>> input;
-    private List<ItemStack> output;
+public class DefaultBlastingDisplay extends DefaultCookingDisplay {
     
     public DefaultBlastingDisplay(BlastingRecipe recipe) {
-        this.display = recipe;
-        this.input = recipe.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.input.add(FurnaceBlockEntity.createFuelTimeMap().keySet().stream().map(Item::getStackForRender).collect(Collectors.toList()));
-        this.output = Collections.singletonList(recipe.getOutput());
-    }
-    
-    @Override
-    public Optional<Identifier> getRecipeLocation() {
-        return Optional.ofNullable(display).map(AbstractCookingRecipe::getId);
-    }
-    
-    @Override
-    public List<List<ItemStack>> getInput() {
-        return input;
-    }
-    
-    public List<ItemStack> getFuel() {
-        return input.get(1);
-    }
-    
-    @Override
-    public List<ItemStack> getOutput() {
-        return output;
+        super(recipe);
     }
     
     @Override
     public Identifier getRecipeCategory() {
         return DefaultPlugin.BLASTING;
     }
-    
-    @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return input;
-    }
-    
-    public Optional<BlastingRecipe> getOptionalRecipe() {
-        return Optional.ofNullable(display);
-    }
-    
-    @Override
-    public int getWidth() {
-        return 1;
-    }
-    
-    @Override
-    public int getHeight() {
-        return 1;
-    }
-    
-    @Override
-    public List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container) {
-        return display.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-    }
 }

+ 10 - 13
src/main/java/me/shedaniel/rei/plugin/brewing/DefaultBrewingCategory.java

@@ -8,24 +8,21 @@ package me.shedaniel.rei.plugin.brewing;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.api.Renderer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.client.resource.language.I18n;
-import net.minecraft.item.ItemStack;
 import net.minecraft.item.Items;
-import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.function.Supplier;
@@ -38,8 +35,8 @@ public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisp
     }
     
     @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.BREWING_STAND));
+    public EntryStack getLogo() {
+        return EntryStack.create(Blocks.BREWING_STAND);
     }
     
     @Override
@@ -63,12 +60,12 @@ public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisp
                 blit(startPoint.x + 44, startPoint.y + 28, 103, 163, width, 4);
             }
         }));
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, Renderer.fromItemStack(new ItemStack(Items.BLAZE_POWDER)), false, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 40, startPoint.y + 1, Renderer.fromItemStacks(() -> recipeDisplay.getInput().get(0), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.input"))), false, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 63, startPoint.y + 1, Renderer.fromItemStacks(() -> recipeDisplay.getInput().get(1), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.reactant"))), false, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 40, startPoint.y + 35, Renderer.fromItemStacks(() -> recipeDisplay.getOutput(0), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.result"))), false, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 63, startPoint.y + 42, Renderer.fromItemStacks(() -> recipeDisplay.getOutput(1), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.result"))), false, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 86, startPoint.y + 35, Renderer.fromItemStacks(() -> recipeDisplay.getOutput(2), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.result"))), false, true, true));
+        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 1).entry(EntryStack.create(Items.BLAZE_POWDER)).noBackground());
+        widgets.add(EntryWidget.create(startPoint.x + 40, startPoint.y + 1).entries(recipeDisplay.getInputEntries().get(0)).noBackground());
+        widgets.add(EntryWidget.create(startPoint.x + 63, startPoint.y + 1).entries(recipeDisplay.getInputEntries().get(1)).noBackground());
+        widgets.add(EntryWidget.create(startPoint.x + 40, startPoint.y + 35).entries(recipeDisplay.getOutput(0)).noBackground());
+        widgets.add(EntryWidget.create(startPoint.x + 63, startPoint.y + 42).entries(recipeDisplay.getOutput(1)).noBackground());
+        widgets.add(EntryWidget.create(startPoint.x + 86, startPoint.y + 35).entries(recipeDisplay.getOutput(2)).noBackground());
         return widgets;
     }
     

+ 29 - 16
src/main/java/me/shedaniel/rei/plugin/brewing/DefaultBrewingDisplay.java

@@ -6,36 +6,49 @@
 package me.shedaniel.rei.plugin.brewing;
 
 import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.block.Blocks;
+import net.minecraft.client.resource.language.I18n;
 import net.minecraft.item.ItemStack;
+import net.minecraft.item.PotionItem;
 import net.minecraft.recipe.Ingredient;
+import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
 public class DefaultBrewingDisplay implements RecipeDisplay {
     
-    private ItemStack input, output;
-    private Ingredient reactant;
+    private EntryStack input, output;
+    private List<EntryStack> reactant;
     
     public DefaultBrewingDisplay(ItemStack input, Ingredient reactant, ItemStack output) {
-        this.input = input;
-        this.reactant = reactant;
-        this.output = output;
+        this.input = EntryStack.create(input).setting(EntryStack.Settings.TOOLTIP_APPEND_EXTRA, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.input")));
+        if (this.input.getItem() instanceof PotionItem)
+            this.input = this.input.setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE);
+        this.reactant = new ArrayList<>();
+        for (ItemStack stack : reactant.getStackArray()) {
+            EntryStack entryStack = EntryStack.create(stack);
+            if (stack.getItem() instanceof PotionItem)
+                entryStack.setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE);
+            entryStack.setting(EntryStack.Settings.TOOLTIP_APPEND_EXTRA, s -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.reactant")));
+            this.reactant.add(entryStack);
+        }
+        this.output = EntryStack.create(output).setting(EntryStack.Settings.TOOLTIP_APPEND_EXTRA, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.brewing.result")));
+        if (this.output.getItem() instanceof PotionItem)
+            this.output = this.output.setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE);
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
-        return Lists.newArrayList(Collections.singletonList(input), Arrays.asList(reactant.getMatchingStacksClient()));
+    public List<List<EntryStack>> getInputEntries() {
+        return Lists.newArrayList(Collections.singletonList(input), reactant);
     }
     
     @Override
-    public List<ItemStack> getOutput() {
+    public List<EntryStack> getOutputEntries() {
         return Collections.singletonList(output);
     }
     
@@ -44,17 +57,17 @@ public class DefaultBrewingDisplay implements RecipeDisplay {
         return DefaultPlugin.BREWING;
     }
     
-    public List<ItemStack> getOutput(int slot) {
-        List<ItemStack> stack = new ArrayList<>();
+    public List<EntryStack> getOutput(int slot) {
+        List<EntryStack> stack = new ArrayList<>();
         for (int i = 0; i < slot * 2; i++)
-            stack.add(new ItemStack(Blocks.AIR));
+            stack.add(EntryStack.empty());
         for (int i = 0; i < 6 - slot * 2; i++)
-            stack.addAll(getOutput());
+            stack.addAll(getOutputEntries());
         return stack;
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return Collections.singletonList(Collections.singletonList(ItemStack.EMPTY));
+    public List<List<EntryStack>> getRequiredEntries() {
+        return getInputEntries();
     }
 }

+ 6 - 7
src/main/java/me/shedaniel/rei/plugin/campfire/DefaultCampfireCategory.java

@@ -8,11 +8,11 @@ package me.shedaniel.rei.plugin.campfire;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.api.Renderer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeArrowWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.plugin.DefaultPlugin;
@@ -20,7 +20,6 @@ import net.minecraft.block.Blocks;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.client.resource.language.I18n;
-import net.minecraft.item.ItemStack;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
@@ -37,8 +36,8 @@ public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDi
     }
     
     @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.CAMPFIRE));
+    public EntryStack getLogo() {
+        return EntryStack.create(Blocks.CAMPFIRE);
     }
     
     @Override
@@ -65,8 +64,8 @@ public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDi
             }
         }));
         widgets.add(new RecipeArrowWidget(startPoint.x + 24, startPoint.y + 18, true));
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 11, Renderer.fromItemStacks(recipeDisplaySupplier.get().getInput().get(0)), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
+        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 11).entries(recipeDisplaySupplier.get().getInputEntries().get(0)));
+        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 19).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground());
         return widgets;
     }
     

+ 19 - 12
src/main/java/me/shedaniel/rei/plugin/campfire/DefaultCampfireDisplay.java

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.plugin.campfire;
 
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.item.ItemStack;
@@ -14,7 +15,7 @@ import net.minecraft.recipe.Ingredient;
 import net.minecraft.util.DefaultedList;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -22,8 +23,8 @@ import java.util.stream.Collectors;
 
 public class DefaultCampfireDisplay implements RecipeDisplay {
     
-    private List<List<ItemStack>> inputs;
-    private List<ItemStack> output;
+    private List<List<EntryStack>> inputs;
+    private List<EntryStack> output;
     private int cookTime;
     private CampfireCookingRecipe display;
     
@@ -33,8 +34,14 @@ public class DefaultCampfireDisplay implements RecipeDisplay {
     }
     
     public DefaultCampfireDisplay(DefaultedList<Ingredient> ingredients, ItemStack output, int cookTime) {
-        this.inputs = ingredients.stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.output = Collections.singletonList(output);
+        this.inputs = ingredients.stream().map(i -> {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : i.getStackArray()) {
+                entries.add(EntryStack.create(stack));
+            }
+            return entries;
+        }).collect(Collectors.toList());
+        this.output = Collections.singletonList(EntryStack.create(output));
         this.cookTime = cookTime;
     }
     
@@ -48,23 +55,23 @@ public class DefaultCampfireDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
+    public List<List<EntryStack>> getInputEntries() {
         return inputs;
     }
     
     @Override
-    public List<ItemStack> getOutput() {
-        return this.output;
+    public List<EntryStack> getOutputEntries() {
+        return output;
     }
     
     @Override
-    public Identifier getRecipeCategory() {
-        return DefaultPlugin.CAMPFIRE;
+    public List<List<EntryStack>> getRequiredEntries() {
+        return inputs;
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return getInput();
+    public Identifier getRecipeCategory() {
+        return DefaultPlugin.CAMPFIRE;
     }
     
 }

+ 16 - 18
src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingCategory.java

@@ -9,11 +9,11 @@ import com.google.common.collect.Lists;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.api.Renderer;
 import me.shedaniel.rei.gui.renderers.RecipeRenderer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
@@ -21,14 +21,13 @@ import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.item.ItemConvertible;
-import net.minecraft.item.ItemStack;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.function.Supplier;
 
 public class DefaultCompostingCategory implements RecipeCategory<DefaultCompostingDisplay> {
@@ -39,8 +38,8 @@ public class DefaultCompostingCategory implements RecipeCategory<DefaultComposti
     }
     
     @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.COMPOSTER));
+    public EntryStack getLogo() {
+        return EntryStack.create(Blocks.COMPOSTER);
     }
     
     @Override
@@ -76,24 +75,23 @@ public class DefaultCompostingCategory implements RecipeCategory<DefaultComposti
                 this.blit(startingPoint.x, startingPoint.y, 28, 221, 55, 26);
             }
         });
-        List<ItemConvertible> stacks = new LinkedList<>(recipeDisplaySupplier.get().getItemsByOrder());
+        List<EntryStack> stacks = new LinkedList<>(recipeDisplaySupplier.get().getItemsByOrder());
         int i = 0;
         for (int y = 0; y < 6; y++)
             for (int x = 0; x < 8; x++) {
                 int finalI = i;
-                widgets.add(new SlotWidget(bounds.getCenterX() - 72 + x * 18, bounds.y + y * 18, stacks.size() > i ? Renderer.fromItemStacks(() -> Collections.singletonList(new ItemStack(stacks.get(finalI))), true, stack -> {
-                    final List<String>[] thing = new List[]{null};
-                    recipeDisplaySupplier.get().getInputMap().forEach((itemProvider, aFloat) -> {
-                        if (itemProvider.asItem().equals(stack.getItem()))
-                            thing[0] = Arrays.asList(I18n.translate("text.rei.composting.chance", MathHelper.fastFloor(aFloat * 100)));
-                    });
-                    if (thing[0] != null)
-                        return thing[0];
-                    return null;
-                }) : Renderer.empty(), true, true, true));
+                EntryStack entryStack = stacks.size() > i ? stacks.get(finalI) : EntryStack.empty();
+                if (entryStack.getType() != EntryStack.Type.EMPTY)
+                    for (Map.Entry<ItemConvertible, Float> entry : recipeDisplaySupplier.get().getInputMap().entrySet()) {
+                        if (entry.getKey().asItem().equals(entryStack.getItem())) {
+                            entryStack = entryStack.setting(EntryStack.Settings.TOOLTIP_APPEND_EXTRA, s -> Collections.singletonList(I18n.translate("text.rei.composting.chance", MathHelper.fastFloor(entry.getValue() * 100))));
+                            break;
+                        }
+                    }
+                widgets.add(EntryWidget.create(bounds.getCenterX() - 72 + x * 18, bounds.y + y * 18).entry(entryStack));
                 i++;
             }
-        widgets.add(new SlotWidget(startingPoint.x + 34, startingPoint.y + 5, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
+        widgets.add(EntryWidget.create(startingPoint.x + 34, startingPoint.y + 5).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground());
         return widgets;
     }
     

+ 16 - 16
src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingDisplay.java

@@ -5,9 +5,9 @@
 
 package me.shedaniel.rei.plugin.composting;
 
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.item.Item;
 import net.minecraft.item.ItemConvertible;
 import net.minecraft.item.ItemStack;
 import net.minecraft.util.Identifier;
@@ -17,17 +17,17 @@ import java.util.stream.Collectors;
 
 public class DefaultCompostingDisplay implements RecipeDisplay {
     
-    private List<ItemConvertible> order, allItems;
+    private List<EntryStack> order, allItems;
     private Map<ItemConvertible, Float> inputMap;
-    private ItemStack[] output;
+    private List<EntryStack> output;
     private int page;
     
     public DefaultCompostingDisplay(int page, List<ItemConvertible> order, Map<ItemConvertible, Float> inputMap, List<ItemConvertible> allItems, ItemStack[] output) {
         this.page = page;
-        this.order = order;
+        this.order = order.stream().map(EntryStack::create).collect(Collectors.toList());
         this.inputMap = inputMap;
-        this.output = output;
-        this.allItems = allItems;
+        this.output = Arrays.asList(output).stream().map(EntryStack::create).collect(Collectors.toList());
+        this.allItems = allItems.stream().map(EntryStack::create).collect(Collectors.toList());
     }
     
     public int getPage() {
@@ -35,11 +35,11 @@ public class DefaultCompostingDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
-        List<List<ItemStack>> lists = new ArrayList<>();
-        allItems.stream().forEachOrdered(itemProvider -> {
-            lists.add(Arrays.asList(itemProvider.asItem().getStackForRender()));
-        });
+    public List<List<EntryStack>> getInputEntries() {
+        List<List<EntryStack>> lists = new ArrayList<>();
+        for (EntryStack allItem : allItems) {
+            lists.add(Collections.singletonList(allItem));
+        }
         return lists;
     }
     
@@ -48,8 +48,8 @@ public class DefaultCompostingDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<ItemStack> getOutput() {
-        return Arrays.asList(output);
+    public List<EntryStack> getOutputEntries() {
+        return output;
     }
     
     @Override
@@ -58,11 +58,11 @@ public class DefaultCompostingDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return Arrays.asList(new LinkedList<>(allItems.stream().map(ItemConvertible::asItem).map(Item::getStackForRender).collect(Collectors.toList())));
+    public List<List<EntryStack>> getRequiredEntries() {
+        return Collections.singletonList(allItems);
     }
     
-    public List<ItemConvertible> getItemsByOrder() {
+    public List<EntryStack> getItemsByOrder() {
         return order;
     }
     

+ 41 - 36
src/main/java/me/shedaniel/rei/plugin/smoking/DefaultSmokingCategory.java → src/main/java/me/shedaniel/rei/plugin/cooking/DefaultCookingCategory.java

@@ -3,66 +3,62 @@
  * Licensed under the MIT License.
  */
 
-package me.shedaniel.rei.plugin.smoking;
+package me.shedaniel.rei.plugin.cooking;
 
 import it.unimi.dsi.fastutil.ints.IntList;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
-import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.math.compat.RenderHelper;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.Renderer;
 import me.shedaniel.rei.api.TransferRecipeCategory;
 import me.shedaniel.rei.gui.renderers.RecipeRenderer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeArrowWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.block.Blocks;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.DrawableHelper;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.client.resource.language.I18n;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
 import java.util.Arrays;
-import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.function.Supplier;
 
-public class DefaultSmokingCategory implements TransferRecipeCategory<DefaultSmokingDisplay> {
+public class DefaultCookingCategory implements TransferRecipeCategory<DefaultCookingDisplay> {
+    private Identifier identifier;
+    private EntryStack logo;
+    private String categoryName;
     
-    @Override
-    public Identifier getIdentifier() {
-        return DefaultPlugin.SMOKING;
-    }
-    
-    @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.SMOKER));
-    }
-    
-    @Override
-    public String getCategoryName() {
-        return I18n.translate("category.rei.smoking");
+    public DefaultCookingCategory(Identifier identifier, EntryStack logo, String categoryName) {
+        this.identifier = identifier;
+        this.logo = logo;
+        this.categoryName = categoryName;
     }
     
     @Override
-    public RecipeRenderer getSimpleRenderer(DefaultSmokingDisplay recipe) {
-        return Renderer.fromRecipe(() -> Arrays.asList(recipe.getInput().get(0)), recipe::getOutput);
+    public void renderRedSlots(List<Widget> widgets, Rectangle bounds, DefaultCookingDisplay display, IntList redSlots) {
+        Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
+        RenderHelper.translatef(0, 0, 400);
+        if (redSlots.contains(0)) {
+            DrawableHelper.fill(startPoint.x + 1, startPoint.y + 1, startPoint.x + 1 + 16, startPoint.y + 1 + 16, 822018048);
+        }
+        RenderHelper.translatef(0, 0, -400);
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultSmokingDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(Supplier<DefaultCookingDisplay> recipeDisplaySupplier, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
         List<Widget> widgets = new LinkedList<>(Arrays.asList(new RecipeBaseWidget(bounds) {
             @Override
             public void render(int mouseX, int mouseY, float delta) {
                 super.render(mouseX, mouseY, delta);
-                RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+                RenderHelper.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
                 MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 54, 82, 54);
@@ -71,21 +67,30 @@ public class DefaultSmokingCategory implements TransferRecipeCategory<DefaultSmo
             }
         }));
         widgets.add(new RecipeArrowWidget(startPoint.x + 24, startPoint.y + 18, true));
-        List<List<ItemStack>> input = recipeDisplaySupplier.get().getInput();
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, Renderer.fromItemStacks(input.get(0)), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 37, Renderer.fromItemStacks(() -> recipeDisplaySupplier.get().getFuel(), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.smelting.fuel"))), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
+        List<List<EntryStack>> input = recipeDisplaySupplier.get().getInputEntries();
+        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 1).entries(input.get(0)));
+        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 37).entries(input.get(1)));
+        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 19).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground());
         return widgets;
     }
     
     @Override
-    public void renderRedSlots(List<Widget> widgets, Rectangle bounds, DefaultSmokingDisplay display, IntList redSlots) {
-        Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
-        RenderSystem.translatef(0, 0, 400);
-        if (redSlots.contains(0)) {
-            DrawableHelper.fill(startPoint.x + 1, startPoint.y + 1, startPoint.x + 1 + 16, startPoint.y + 1 + 16, 822018048);
-        }
-        RenderSystem.translatef(0, 0, -400);
+    public RecipeRenderer getSimpleRenderer(DefaultCookingDisplay recipe) {
+        return Renderer.fromRecipeEntries(() -> Arrays.asList(recipe.getInputEntries().get(0)), recipe::getOutputEntries);
+    }
+    
+    @Override
+    public Identifier getIdentifier() {
+        return identifier;
     }
     
+    @Override
+    public EntryStack getLogo() {
+        return logo;
+    }
+    
+    @Override
+    public String getCategoryName() {
+        return I18n.translate(categoryName);
+    }
 }

+ 84 - 0
src/main/java/me/shedaniel/rei/plugin/cooking/DefaultCookingDisplay.java

@@ -0,0 +1,84 @@
+/*
+ * Roughly Enough Items by Danielshe.
+ * Licensed under the MIT License.
+ */
+
+package me.shedaniel.rei.plugin.cooking;
+
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.TransferRecipeDisplay;
+import me.shedaniel.rei.server.ContainerInfo;
+import net.minecraft.block.entity.FurnaceBlockEntity;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.container.Container;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemStack;
+import net.minecraft.recipe.AbstractCookingRecipe;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.Identifier;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public abstract class DefaultCookingDisplay implements TransferRecipeDisplay {
+    private AbstractCookingRecipe recipe;
+    private List<List<EntryStack>> input;
+    private List<EntryStack> output;
+    
+    public DefaultCookingDisplay(AbstractCookingRecipe recipe) {
+        this.recipe = recipe;
+        this.input = recipe.getPreviewInputs().stream().map(i -> {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : i.getStackArray()) {
+                entries.add(EntryStack.create(stack));
+            }
+            return entries;
+        }).collect(Collectors.toList());
+        this.input.add(FurnaceBlockEntity.createFuelTimeMap().keySet().stream().map(Item::getStackForRender).map(EntryStack::create).map(e -> e.setting(EntryStack.Settings.TOOLTIP_APPEND_EXTRA, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.smelting.fuel")))).collect(Collectors.toList()));
+        this.output = Collections.singletonList(EntryStack.create(recipe.getOutput()));
+    }
+    
+    @Override
+    public Optional<Identifier> getRecipeLocation() {
+        return Optional.ofNullable(recipe).map(AbstractCookingRecipe::getId);
+    }
+    
+    @Override
+    public List<List<EntryStack>> getInputEntries() {
+        return input;
+    }
+    
+    @Override
+    public List<EntryStack> getOutputEntries() {
+        return output;
+    }
+    
+    public List<EntryStack> getFuel() {
+        return input.get(1);
+    }
+    
+    @Override
+    public List<List<EntryStack>> getRequiredEntries() {
+        return input;
+    }
+    
+    @Deprecated
+    public Optional<AbstractCookingRecipe> getOptionalRecipe() {
+        return Optional.ofNullable(recipe);
+    }
+    
+    @Override
+    public int getWidth() {
+        return 1;
+    }
+    
+    @Override
+    public int getHeight() {
+        return 1;
+    }
+    
+    @Override
+    public List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container) {
+        return recipe.getPreviewInputs().stream().map(i -> Arrays.asList(i.getStackArray())).collect(Collectors.toList());
+    }
+}

+ 8 - 6
src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCraftingCategory.java

@@ -9,9 +9,11 @@ import com.google.common.collect.Lists;
 import it.unimi.dsi.fastutil.ints.IntList;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.EntryStack;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.rei.api.Renderer;
 import me.shedaniel.rei.api.TransferRecipeCategory;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
@@ -71,20 +73,20 @@ public class DefaultCraftingCategory implements TransferRecipeCategory<DefaultCr
                 blit(startPoint.x, startPoint.y, 0, 0, 116, 54);
             }
         }));
-        List<List<ItemStack>> input = recipeDisplaySupplier.get().getInput();
-        List<SlotWidget> slots = Lists.newArrayList();
+        List<List<EntryStack>> input = recipeDisplaySupplier.get().getInputEntries();
+        List<EntryWidget> slots = Lists.newArrayList();
         for (int y = 0; y < 3; y++)
             for (int x = 0; x < 3; x++)
-                slots.add(new SlotWidget(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18, Lists.newArrayList(), true, true, true));
+                slots.add(EntryWidget.create(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18));
         for (int i = 0; i < input.size(); i++) {
             if (recipeDisplaySupplier.get() instanceof DefaultShapedDisplay) {
                 if (!input.get(i).isEmpty())
-                    slots.get(getSlotWithSize(recipeDisplaySupplier.get(), i, 3)).setRenderers(Collections.singletonList(Renderer.fromItemStacks(input.get(i))));
+                    slots.get(getSlotWithSize(recipeDisplaySupplier.get(), i, 3)).entries(input.get(i));
             } else if (!input.get(i).isEmpty())
-                slots.get(i).setRenderers(Collections.singletonList(Renderer.fromItemStacks(input.get(i))));
+                slots.get(i).entries(input.get(i));
         }
         widgets.addAll(slots);
-        widgets.add(new SlotWidget(startPoint.x + 95, startPoint.y + 19, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
+        widgets.add(EntryWidget.create(startPoint.x + 95, startPoint.y + 19).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground());
         return widgets;
     }
     

+ 5 - 5
src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCraftingDisplay.java

@@ -6,11 +6,11 @@
 package me.shedaniel.rei.plugin.crafting;
 
 import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.TransferRecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import me.shedaniel.rei.server.ContainerInfo;
 import net.minecraft.container.Container;
-import net.minecraft.item.ItemStack;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.util.Identifier;
 
@@ -37,13 +37,13 @@ public interface DefaultCraftingDisplay extends TransferRecipeDisplay {
     Optional<Recipe<?>> getOptionalRecipe();
     
     @Override
-    default List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container) {
-        List<List<ItemStack>> list = Lists.newArrayListWithCapacity(containerInfo.getCraftingWidth(container) * containerInfo.getCraftingHeight(container));
+    default List<List<EntryStack>> getOrganisedInputEntries(ContainerInfo<Container> containerInfo, Container container) {
+        List<List<EntryStack>> list = Lists.newArrayListWithCapacity(containerInfo.getCraftingWidth(container) * containerInfo.getCraftingHeight(container));
         for (int i = 0; i < containerInfo.getCraftingWidth(container) * containerInfo.getCraftingHeight(container); i++) {
             list.add(Lists.newArrayList());
         }
-        for (int i = 0; i < getInput().size(); i++) {
-            List<ItemStack> stacks = getInput().get(i);
+        for (int i = 0; i < getInputEntries().size(); i++) {
+            List<EntryStack> stacks = getInputEntries().get(i);
             list.set(DefaultCraftingCategory.getSlotWithSize(this, i, containerInfo.getCraftingWidth(container)), stacks);
         }
         return list;

+ 13 - 7
src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCustomDisplay.java

@@ -6,6 +6,8 @@
 package me.shedaniel.rei.plugin.crafting;
 
 import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.item.ItemStack;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.util.Identifier;
@@ -15,20 +17,24 @@ import java.util.Optional;
 
 public class DefaultCustomDisplay implements DefaultCraftingDisplay {
     
-    private List<List<ItemStack>> input;
-    private List<ItemStack> output;
+    private List<List<EntryStack>> input;
+    private List<EntryStack> output;
     private Recipe<?> possibleRecipe;
     private int width, height;
     
     public DefaultCustomDisplay(List<List<ItemStack>> input, List<ItemStack> output, Recipe<?> possibleRecipe) {
+        this(possibleRecipe, CollectionUtils.map(input, i -> CollectionUtils.map(i, EntryStack::create)), CollectionUtils.map(output, EntryStack::create));
+    }
+    
+    public DefaultCustomDisplay(Recipe<?> possibleRecipe, List<List<EntryStack>> input, List<EntryStack> output) {
         this.input = input;
         this.output = output;
         this.possibleRecipe = possibleRecipe;
         List<Boolean> row = Lists.newArrayList(false, false, false);
         List<Boolean> column = Lists.newArrayList(false, false, false);
         for (int i = 0; i < 9; i++)
-            if (i < input.size()) {
-                List<ItemStack> stacks = input.get(i);
+            if (i < this.input.size()) {
+                List<EntryStack> stacks = this.input.get(i);
                 if (stacks.stream().filter(stack -> !stack.isEmpty()).count() > 0) {
                     row.set((i - (i % 3)) / 3, true);
                     column.set(i % 3, true);
@@ -52,17 +58,17 @@ public class DefaultCustomDisplay implements DefaultCraftingDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
+    public List<List<EntryStack>> getInputEntries() {
         return input;
     }
     
     @Override
-    public List<ItemStack> getOutput() {
+    public List<EntryStack> getOutputEntries() {
         return output;
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
+    public List<List<EntryStack>> getRequiredEntries() {
         return input;
     }
     

+ 20 - 8
src/main/java/me/shedaniel/rei/plugin/crafting/DefaultShapedDisplay.java

@@ -5,12 +5,14 @@
 
 package me.shedaniel.rei.plugin.crafting;
 
+import me.shedaniel.rei.api.EntryStack;
 import net.minecraft.item.ItemStack;
+import net.minecraft.item.PotionItem;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.recipe.ShapedRecipe;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -19,13 +21,23 @@ import java.util.stream.Collectors;
 public class DefaultShapedDisplay implements DefaultCraftingDisplay {
     
     private ShapedRecipe display;
-    private List<List<ItemStack>> input;
-    private List<ItemStack> output;
+    private List<List<EntryStack>> input;
+    private List<EntryStack> output;
     
     public DefaultShapedDisplay(ShapedRecipe recipe) {
         this.display = recipe;
-        this.input = recipe.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.output = Collections.singletonList(recipe.getOutput());
+        this.input = recipe.getPreviewInputs().stream().map(i -> {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : i.getStackArray()) {
+                if (stack.getItem() instanceof PotionItem)
+                    entries.add(EntryStack.create(stack).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
+                else entries.add(EntryStack.create(stack));
+            }
+            return entries;
+        }).collect(Collectors.toList());
+        if (recipe.getOutput().getItem() instanceof PotionItem)
+            this.output = Collections.singletonList(EntryStack.create(recipe.getOutput()).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
+        else this.output = Collections.singletonList(EntryStack.create(recipe.getOutput()));
     }
     
     @Override
@@ -34,17 +46,17 @@ public class DefaultShapedDisplay implements DefaultCraftingDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
+    public List<List<EntryStack>> getInputEntries() {
         return input;
     }
     
     @Override
-    public List<ItemStack> getOutput() {
+    public List<EntryStack> getOutputEntries() {
         return output;
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
+    public List<List<EntryStack>> getRequiredEntries() {
         return input;
     }
     

+ 20 - 8
src/main/java/me/shedaniel/rei/plugin/crafting/DefaultShapelessDisplay.java

@@ -5,12 +5,14 @@
 
 package me.shedaniel.rei.plugin.crafting;
 
+import me.shedaniel.rei.api.EntryStack;
 import net.minecraft.item.ItemStack;
+import net.minecraft.item.PotionItem;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.recipe.ShapelessRecipe;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -19,13 +21,23 @@ import java.util.stream.Collectors;
 public class DefaultShapelessDisplay implements DefaultCraftingDisplay {
     
     private ShapelessRecipe display;
-    private List<List<ItemStack>> input;
-    private List<ItemStack> output;
+    private List<List<EntryStack>> input;
+    private List<EntryStack> output;
     
     public DefaultShapelessDisplay(ShapelessRecipe recipe) {
         this.display = recipe;
-        this.input = recipe.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.output = Collections.singletonList(recipe.getOutput());
+        this.input = recipe.getPreviewInputs().stream().map(i -> {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : i.getStackArray()) {
+                if (stack.getItem() instanceof PotionItem)
+                    entries.add(EntryStack.create(stack).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
+                else entries.add(EntryStack.create(stack));
+            }
+            return entries;
+        }).collect(Collectors.toList());
+        if (recipe.getOutput().getItem() instanceof PotionItem)
+            this.output = Collections.singletonList(EntryStack.create(recipe.getOutput()).setting(EntryStack.Settings.CHECK_TAGS, EntryStack.Settings.TRUE));
+        else this.output = Collections.singletonList(EntryStack.create(recipe.getOutput()));
     }
     
     @Override
@@ -39,17 +51,17 @@ public class DefaultShapelessDisplay implements DefaultCraftingDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
+    public List<List<EntryStack>> getInputEntries() {
         return input;
     }
     
     @Override
-    public List<ItemStack> getOutput() {
+    public List<EntryStack> getOutputEntries() {
         return output;
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
+    public List<List<EntryStack>> getRequiredEntries() {
         return input;
     }
     

+ 0 - 91
src/main/java/me/shedaniel/rei/plugin/smelting/DefaultSmeltingCategory.java

@@ -1,91 +0,0 @@
-/*
- * Roughly Enough Items by Danielshe.
- * Licensed under the MIT License.
- */
-
-package me.shedaniel.rei.plugin.smelting;
-
-import it.unimi.dsi.fastutil.ints.IntList;
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
-import com.mojang.blaze3d.systems.RenderSystem;
-import me.shedaniel.rei.api.Renderer;
-import me.shedaniel.rei.api.TransferRecipeCategory;
-import me.shedaniel.rei.gui.renderers.RecipeRenderer;
-import me.shedaniel.rei.gui.widget.RecipeArrowWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
-import me.shedaniel.rei.gui.widget.Widget;
-import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.block.Blocks;
-import net.minecraft.client.MinecraftClient;
-import net.minecraft.client.gui.DrawableHelper;
-import net.minecraft.client.render.GuiLighting;
-import net.minecraft.client.resource.language.I18n;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.function.Supplier;
-
-public class DefaultSmeltingCategory implements TransferRecipeCategory<DefaultSmeltingDisplay> {
-    
-    @Override
-    public Identifier getIdentifier() {
-        return DefaultPlugin.SMELTING;
-    }
-    
-    @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.FURNACE));
-    }
-    
-    @Override
-    public String getCategoryName() {
-        return I18n.translate("category.rei.smelting");
-    }
-    
-    @Override
-    public RecipeRenderer getSimpleRenderer(DefaultSmeltingDisplay recipe) {
-        return Renderer.fromRecipe(() -> Arrays.asList(recipe.getInput().get(0)), recipe::getOutput);
-    }
-    
-    @Override
-    public List<Widget> setupDisplay(Supplier<DefaultSmeltingDisplay> recipeDisplaySupplier, Rectangle bounds) {
-        Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
-        List<Widget> widgets = new LinkedList<>(Arrays.asList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-                GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 54, 82, 54);
-                int height = MathHelper.ceil((System.currentTimeMillis() / 250 % 14d) / 1f);
-                blit(startPoint.x + 2, startPoint.y + 21 + (14 - height), 82, 77 + (14 - height), 14, height);
-            }
-        }));
-        widgets.add(new RecipeArrowWidget(startPoint.x + 24, startPoint.y + 18, true));
-        List<List<ItemStack>> input = recipeDisplaySupplier.get().getInput();
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, Renderer.fromItemStacks(input.get(0)), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 37, Renderer.fromItemStacks(() -> recipeDisplaySupplier.get().getFuel(), true, stack -> Collections.singletonList(Formatting.YELLOW.toString() + I18n.translate("category.rei.smelting.fuel"))), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
-        return widgets;
-    }
-    
-    @Override
-    public void renderRedSlots(List<Widget> widgets, Rectangle bounds, DefaultSmeltingDisplay display, IntList redSlots) {
-        Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
-        RenderSystem.translatef(0, 0, 400);
-        if (redSlots.contains(0)) {
-            DrawableHelper.fill(startPoint.x + 1, startPoint.y + 1, startPoint.x + 1 + 16, startPoint.y + 1 + 16, 822018048);
-        }
-        RenderSystem.translatef(0, 0, -400);
-    }
-    
-}

+ 3 - 65
src/main/java/me/shedaniel/rei/plugin/smelting/DefaultSmeltingDisplay.java

@@ -5,81 +5,19 @@
 
 package me.shedaniel.rei.plugin.smelting;
 
-import me.shedaniel.rei.api.TransferRecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import me.shedaniel.rei.server.ContainerInfo;
-import net.minecraft.block.entity.FurnaceBlockEntity;
-import net.minecraft.container.Container;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.recipe.AbstractCookingRecipe;
+import me.shedaniel.rei.plugin.cooking.DefaultCookingDisplay;
 import net.minecraft.recipe.SmeltingRecipe;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-public class DefaultSmeltingDisplay implements TransferRecipeDisplay {
-    
-    private SmeltingRecipe display;
-    private List<List<ItemStack>> input;
-    private List<ItemStack> output;
+public class DefaultSmeltingDisplay extends DefaultCookingDisplay {
     
     public DefaultSmeltingDisplay(SmeltingRecipe recipe) {
-        this.display = recipe;
-        this.input = recipe.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.input.add(FurnaceBlockEntity.createFuelTimeMap().keySet().stream().map(Item::getStackForRender).collect(Collectors.toList()));
-        this.output = Collections.singletonList(recipe.getOutput());
-    }
-    
-    @Override
-    public Optional<Identifier> getRecipeLocation() {
-        return Optional.ofNullable(display).map(AbstractCookingRecipe::getId);
-    }
-    
-    @Override
-    public List<List<ItemStack>> getInput() {
-        return input;
-    }
-    
-    public List<ItemStack> getFuel() {
-        return input.get(1);
-    }
-    
-    @Override
-    public List<ItemStack> getOutput() {
-        return output;
+        super(recipe);
     }
     
     @Override
     public Identifier getRecipeCategory() {
         return DefaultPlugin.SMELTING;
     }
-    
-    @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return input;
-    }
-    
-    public Optional<SmeltingRecipe> getOptionalRecipe() {
-        return Optional.ofNullable(display);
-    }
-    
-    @Override
-    public int getWidth() {
-        return 1;
-    }
-    
-    @Override
-    public int getHeight() {
-        return 1;
-    }
-    
-    @Override
-    public List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container) {
-        return display.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-    }
 }

+ 3 - 66
src/main/java/me/shedaniel/rei/plugin/smoking/DefaultSmokingDisplay.java

@@ -5,82 +5,19 @@
 
 package me.shedaniel.rei.plugin.smoking;
 
-import me.shedaniel.rei.api.TransferRecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import me.shedaniel.rei.server.ContainerInfo;
-import net.minecraft.block.entity.FurnaceBlockEntity;
-import net.minecraft.container.Container;
-import net.minecraft.item.Item;
-import net.minecraft.item.ItemStack;
-import net.minecraft.recipe.AbstractCookingRecipe;
+import me.shedaniel.rei.plugin.cooking.DefaultCookingDisplay;
 import net.minecraft.recipe.SmokingRecipe;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-public class DefaultSmokingDisplay implements TransferRecipeDisplay {
-    
-    private SmokingRecipe display;
-    private List<List<ItemStack>> input;
-    private List<ItemStack> output;
+public class DefaultSmokingDisplay extends DefaultCookingDisplay {
     
     public DefaultSmokingDisplay(SmokingRecipe recipe) {
-        this.display = recipe;
-        this.input = recipe.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.input.add(FurnaceBlockEntity.createFuelTimeMap().keySet().stream().map(Item::getStackForRender).collect(Collectors.toList()));
-        this.output = Collections.singletonList(recipe.getOutput());
-    }
-    
-    @Override
-    public Optional<Identifier> getRecipeLocation() {
-        return Optional.ofNullable(display).map(AbstractCookingRecipe::getId);
-    }
-    
-    @Override
-    public List<List<ItemStack>> getInput() {
-        return input;
-    }
-    
-    public List<ItemStack> getFuel() {
-        return input.get(1);
-    }
-    
-    @Override
-    public List<ItemStack> getOutput() {
-        return output;
+        super(recipe);
     }
     
     @Override
     public Identifier getRecipeCategory() {
         return DefaultPlugin.SMOKING;
     }
-    
-    @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return input;
-    }
-    
-    public Optional<SmokingRecipe> getOptionalRecipe() {
-        return Optional.ofNullable(display);
-    }
-    
-    @Override
-    public int getWidth() {
-        return 1;
-    }
-    
-    @Override
-    public int getHeight() {
-        return 1;
-    }
-    
-    @Override
-    public List<List<ItemStack>> getOrganisedInput(ContainerInfo<Container> containerInfo, Container container) {
-        return display.getPreviewInputs().stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-    }
-    
 }

+ 6 - 5
src/main/java/me/shedaniel/rei/plugin/stonecutting/DefaultStoneCuttingCategory.java

@@ -8,10 +8,11 @@ package me.shedaniel.rei.plugin.stonecutting;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
 import me.shedaniel.rei.api.Renderer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
@@ -34,8 +35,8 @@ public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneC
     }
     
     @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Blocks.STONECUTTER));
+    public EntryStack getLogo() {
+        return EntryStack.create(Blocks.STONECUTTER);
     }
     
     @Override
@@ -56,8 +57,8 @@ public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneC
                 blit(startPoint.x, startPoint.y, 0, 221, 82, 26);
             }
         }));
-        widgets.add(new SlotWidget(startPoint.x + 4, startPoint.y + 5, Renderer.fromItemStacks(recipeDisplaySupplier.get().getInput().get(0)), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 5, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
+        widgets.add(EntryWidget.create(startPoint.x + 4, startPoint.y + 5).entries(recipeDisplaySupplier.get().getInputEntries().get(0)).noBackground());
+        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 5).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground());
         return widgets;
     }
     

+ 17 - 11
src/main/java/me/shedaniel/rei/plugin/stonecutting/DefaultStoneCuttingDisplay.java

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.plugin.stonecutting;
 
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.item.ItemStack;
@@ -14,7 +15,7 @@ import net.minecraft.recipe.StonecuttingRecipe;
 import net.minecraft.util.DefaultedList;
 import net.minecraft.util.Identifier;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -22,8 +23,8 @@ import java.util.stream.Collectors;
 
 public class DefaultStoneCuttingDisplay implements RecipeDisplay {
     
-    private List<List<ItemStack>> inputs;
-    private List<ItemStack> output;
+    private List<List<EntryStack>> inputs;
+    private List<EntryStack> output;
     private StonecuttingRecipe display;
     
     public DefaultStoneCuttingDisplay(StonecuttingRecipe recipe) {
@@ -32,8 +33,14 @@ public class DefaultStoneCuttingDisplay implements RecipeDisplay {
     }
     
     public DefaultStoneCuttingDisplay(DefaultedList<Ingredient> ingredients, ItemStack output) {
-        this.inputs = ingredients.stream().map(i -> Arrays.asList(i.getMatchingStacksClient())).collect(Collectors.toList());
-        this.output = Collections.singletonList(output);
+        this.inputs = ingredients.stream().map(i -> {
+            List<EntryStack> entries = new ArrayList<>();
+            for (ItemStack stack : i.getStackArray()) {
+                entries.add(EntryStack.create(stack));
+            }
+            return entries;
+        }).collect(Collectors.toList());
+        this.output = Collections.singletonList(EntryStack.create(output));
     }
     
     @Override
@@ -42,13 +49,13 @@ public class DefaultStoneCuttingDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
+    public List<List<EntryStack>> getInputEntries() {
         return inputs;
     }
     
     @Override
-    public List<ItemStack> getOutput() {
-        return this.output;
+    public List<EntryStack> getOutputEntries() {
+        return output;
     }
     
     @Override
@@ -57,8 +64,7 @@ public class DefaultStoneCuttingDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return getInput();
+    public List<List<EntryStack>> getRequiredEntries() {
+        return getInputEntries();
     }
-    
 }

+ 6 - 7
src/main/java/me/shedaniel/rei/plugin/stripping/DefaultStrippingCategory.java

@@ -8,16 +8,15 @@ package me.shedaniel.rei.plugin.stripping;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.api.Renderer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
-import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.client.resource.language.I18n;
-import net.minecraft.item.ItemStack;
 import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
 
@@ -34,8 +33,8 @@ public class DefaultStrippingCategory implements RecipeCategory<DefaultStripping
     }
     
     @Override
-    public Renderer getIcon() {
-        return Renderer.fromItemStack(new ItemStack(Items.IRON_AXE));
+    public EntryStack getLogo() {
+        return EntryStack.create(Items.IRON_AXE);
     }
     
     @Override
@@ -56,8 +55,8 @@ public class DefaultStrippingCategory implements RecipeCategory<DefaultStripping
                 blit(startPoint.x, startPoint.y, 0, 221, 82, 26);
             }
         }));
-        widgets.add(new SlotWidget(startPoint.x + 4, startPoint.y + 5, Renderer.fromItemStacks(recipeDisplaySupplier.get().getInput().get(0)), true, true, true));
-        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 5, Renderer.fromItemStacks(recipeDisplaySupplier.get().getOutput()), false, true, true));
+        widgets.add(EntryWidget.create(startPoint.x + 4, startPoint.y + 5).entry(recipeDisplaySupplier.get().getIn()));
+        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 5).entry(recipeDisplaySupplier.get().getOut()).noBackground());
         return widgets;
     }
     

+ 10 - 10
src/main/java/me/shedaniel/rei/plugin/stripping/DefaultStrippingDisplay.java

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.plugin.stripping;
 
+import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.item.ItemStack;
@@ -15,28 +16,28 @@ import java.util.List;
 
 public class DefaultStrippingDisplay implements RecipeDisplay {
     
-    private ItemStack in, out;
+    private EntryStack in, out;
     
     public DefaultStrippingDisplay(ItemStack in, ItemStack out) {
-        this.in = in;
-        this.out = out;
+        this.in = EntryStack.create(in);
+        this.out = EntryStack.create(out);
     }
     
-    public final ItemStack getIn() {
+    public final EntryStack getIn() {
         return in;
     }
     
-    public final ItemStack getOut() {
+    public final EntryStack getOut() {
         return out;
     }
     
     @Override
-    public List<List<ItemStack>> getInput() {
+    public List<List<EntryStack>> getInputEntries() {
         return Collections.singletonList(Collections.singletonList(in));
     }
     
     @Override
-    public List<ItemStack> getOutput() {
+    public List<EntryStack> getOutputEntries() {
         return Collections.singletonList(out);
     }
     
@@ -46,8 +47,7 @@ public class DefaultStrippingDisplay implements RecipeDisplay {
     }
     
     @Override
-    public List<List<ItemStack>> getRequiredItems() {
-        return getInput();
+    public List<List<EntryStack>> getRequiredEntries() {
+        return getInputEntries();
     }
-    
 }

+ 161 - 0
src/main/java/me/shedaniel/rei/utils/CollectionUtils.java

@@ -0,0 +1,161 @@
+package me.shedaniel.rei.utils;
+
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.annotations.Internal;
+
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+@Internal
+public class CollectionUtils {
+    public static final <T> T findFirstOrNullEquals(List<T> list, T obj) {
+        for (T t : list) {
+            if (t.equals(obj))
+                return t;
+        }
+        return null;
+    }
+    
+    public static final <T> T findFirstOrNull(List<T> list, Predicate<T> predicate) {
+        for (T t : list) {
+            if (predicate.test(t))
+                return t;
+        }
+        return null;
+    }
+    
+    public static final <T> boolean anyMatch(List<T> list, Predicate<T> predicate) {
+        for (T t : list) {
+            if (predicate.test(t))
+                return true;
+        }
+        return false;
+    }
+    
+    public static final boolean anyMatchEqualsAll(List<EntryStack> list, EntryStack stack) {
+        for (EntryStack t : list) {
+            if (t.equalsAll(stack))
+                return true;
+        }
+        return false;
+    }
+    
+    public static final <T> List<T> filter(List<T> list, Predicate<T> predicate) {
+        List<T> l = null;
+        for (T t : list) {
+            if (predicate.test(t)) {
+                if (l == null) l = new LinkedList<>();
+                l.add(t);
+            }
+        }
+        return l == null ? Collections.emptyList() : l;
+    }
+    
+    public static final <T, R> List<R> map(List<T> list, Function<T, R> function) {
+        List<R> l = new LinkedList<>();
+        for (T t : list) {
+            l.add(function.apply(t));
+        }
+        return l;
+    }
+    
+    public static final <T, R> List<R> map(T[] list, Function<T, R> function) {
+        List<R> l = new LinkedList<>();
+        for (T t : list) {
+            l.add(function.apply(t));
+        }
+        return l;
+    }
+    
+    public static final <T, R> Optional<R> mapAndMax(List<T> list, Function<T, R> function, Comparator<R> comparator) {
+        if (list.isEmpty())
+            return Optional.empty();
+        List<R> copyOf = CollectionUtils.map(list, function);
+        copyOf.sort(comparator);
+        return Optional.ofNullable(copyOf.get(copyOf.size() - 1));
+    }
+    
+    public static final <T, R> Optional<R> mapAndMax(T[] list, Function<T, R> function, Comparator<R> comparator) {
+        if (list.length <= 0)
+            return Optional.empty();
+        List<R> copyOf = CollectionUtils.map(list, function);
+        copyOf.sort(comparator);
+        return Optional.ofNullable(copyOf.get(copyOf.size() - 1));
+    }
+    
+    public static final <T> Optional<T> max(List<T> list, Comparator<T> comparator) {
+        if (list.isEmpty())
+            return Optional.empty();
+        ArrayList<T> ts = new ArrayList<>(list);
+        ts.sort(comparator);
+        return Optional.ofNullable(ts.get(ts.size() - 1));
+    }
+    
+    public static final <T> Optional<T> max(T[] list, Comparator<T> comparator) {
+        if (list.length <= 0)
+            return Optional.empty();
+        T[] copyOf = list.clone();
+        Arrays.sort(copyOf, comparator);
+        return Optional.ofNullable(copyOf[copyOf.length - 1]);
+    }
+    
+    public static final String joinToString(List<String> list, String separator) {
+        StringJoiner joiner = new StringJoiner(separator);
+        for (String t : list) {
+            joiner.add(t);
+        }
+        return joiner.toString();
+    }
+    
+    public static final String joinToString(String[] list, String separator) {
+        StringJoiner joiner = new StringJoiner(separator);
+        for (String t : list) {
+            joiner.add(t);
+        }
+        return joiner.toString();
+    }
+    
+    public static final <T> String mapAndJoinToString(List<T> list, Function<T, String> function, String separator) {
+        StringJoiner joiner = new StringJoiner(separator);
+        for (T t : list) {
+            joiner.add(function.apply(t));
+        }
+        return joiner.toString();
+    }
+    
+    public static final <T> String mapAndJoinToString(T[] list, Function<T, String> function, String separator) {
+        StringJoiner joiner = new StringJoiner(separator);
+        for (T t : list) {
+            joiner.add(function.apply(t));
+        }
+        return joiner.toString();
+    }
+    
+    public static final <T, R> List<R> filterAndMap(List<T> list, Predicate<T> predicate, Function<T, R> function) {
+        List<R> l = null;
+        for (T t : list) {
+            if (predicate.test(t)) {
+                if (l == null) l = new LinkedList<>();
+                l.add(function.apply(t));
+            }
+        }
+        return l == null ? Collections.emptyList() : l;
+    }
+    
+    public static final <T> int sumInt(List<T> list, Function<T, Integer> function) {
+        int sum = 0;
+        for (T t : list) {
+            sum += function.apply(t);
+        }
+        return sum;
+    }
+    
+    public static final <T> double sumDouble(List<T> list, Function<T, Double> function) {
+        double sum = 0;
+        for (T t : list) {
+            sum += function.apply(t);
+        }
+        return sum;
+    }
+}