shedaniel il y a 5 ans
Parent
commit
ee873c61e2

+ 1 - 1
gradle.properties

@@ -1,4 +1,4 @@
-mod_version=3.3.6
+mod_version=3.3.7
 minecraft_version=1.15.1
 yarn_version=1.15.1+build.1
 fabricloader_version=0.7.2+build.174

+ 0 - 1
src/main/java/me/shedaniel/rei/api/RecipeCategory.java

@@ -121,7 +121,6 @@ public interface RecipeCategory<T extends RecipeDisplay> {
      *
      * @return the amount of recipes, returns -1 if not fixed
      */
-    @Deprecated
     default int getFixedRecipesPerPage() {
         return -1;
     }

+ 5 - 1
src/main/java/me/shedaniel/rei/api/RecipeHelper.java

@@ -178,7 +178,11 @@ public interface RecipeHelper {
      * @deprecated {@link RecipeHelper#isDisplayVisible(RecipeDisplay)} )}
      */
     @Deprecated
-    boolean isDisplayVisible(RecipeDisplay display, boolean respectConfig);
+    default boolean isDisplayVisible(RecipeDisplay display, boolean respectConfig) {
+        return isDisplayVisible(display);
+    }
+    
+    boolean isDisplayNotVisible(RecipeDisplay display);
     
     /**
      * Checks if the display is visible by asking recipe visibility handlers

+ 4 - 100
src/main/java/me/shedaniel/rei/gui/entries/RecipeEntry.java

@@ -5,107 +5,11 @@
 
 package me.shedaniel.rei.gui.entries;
 
-import me.shedaniel.rei.api.EntryStack;
-import net.minecraft.client.gui.DrawableHelper;
-import net.minecraft.util.Identifier;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import me.shedaniel.rei.impl.RenderingEntry;
 
-import java.util.Optional;
-
-public abstract class RecipeEntry extends DrawableHelper implements EntryStack {
-    @Override
-    public Optional<Identifier> getIdentifier() {
-        return Optional.empty();
-    }
-    
-    @Override
-    public Type getType() {
-        return Type.RENDER;
-    }
-    
-    @Override
-    public int getAmount() {
-        return 0;
-    }
-    
-    @Override
-    public void setAmount(int amount) {
-    
-    }
-    
-    @Override
-    public boolean isEmpty() {
-        return false;
-    }
-    
-    @Override
-    public EntryStack copy() {
-        return this;
-    }
-    
-    @Override
-    public Object getObject() {
-        return null;
-    }
-    
-    @Override
-    public boolean equals(EntryStack stack, boolean ignoreTags, boolean ignoreAmount) {
-        return stack == this;
-    }
-    
-    @Override
-    public boolean equalsIgnoreTagsAndAmount(EntryStack stack) {
-        return stack == this;
-    }
-    
-    @Override
-    public boolean equalsIgnoreTags(EntryStack stack) {
-        return stack == this;
-    }
-    
-    @Override
-    public boolean equalsIgnoreAmount(EntryStack stack) {
-        return stack == this;
-    }
-    
-    @Override
-    public boolean equalsAll(EntryStack stack) {
-        return stack == this;
-    }
-    
-    @Override
-    public int getZ() {
-        return getBlitOffset();
-    }
-    
-    @Override
-    public void setZ(int z) {
-        setBlitOffset(z);
-    }
-    
-    @Override
-    public <T> EntryStack setting(Settings<T> settings, T value) {
-        return this;
-    }
-    
-    @Override
-    public <T> EntryStack removeSetting(Settings<T> settings) {
-        return this;
-    }
-    
-    @Override
-    public EntryStack clearSettings() {
-        return this;
-    }
-    
-    @Override
-    public <T> EntryStack addSetting(Settings<T> settings, T value) {
-        return this;
-    }
-    
-    @Override
-    public <T> T get(Settings<T> settings) {
-        return settings.getDefaultValue();
-    }
+public abstract class RecipeEntry extends RenderingEntry {
+    public abstract QueuedTooltip getTooltip(int mouseX, int mouseY);
     
     public abstract int getHeight();
     

+ 78 - 60
src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java

@@ -46,7 +46,8 @@ public class RecipeHelperImpl implements RecipeHelper {
     private final List<ScreenClickArea> screenClickAreas = Lists.newLinkedList();
     private final int[] recipeCount = {0};
     private final Map<Identifier, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
-    private final List<RecipeCategory<?>> categories = Lists.newLinkedList();
+    private final Map<RecipeCategory<?>, Identifier> categories = Maps.newLinkedHashMap();
+    private final Map<Identifier, RecipeCategory<?>> reversedCategories = Maps.newHashMap();
     private final Map<Identifier, ButtonAreaSupplier> autoCraftAreaSupplierMap = Maps.newLinkedHashMap();
     private final Map<Identifier, List<List<EntryStack>>> categoryWorkingStations = Maps.newLinkedHashMap();
     private final List<DisplayVisibilityHandler> displayVisibilityHandlers = Lists.newLinkedList();
@@ -88,9 +89,10 @@ public class RecipeHelperImpl implements RecipeHelper {
     
     @Override
     public void registerCategory(RecipeCategory<?> category) {
-        categories.add(category);
-        recipeCategoryListMap.put(category.getIdentifier(), Lists.newLinkedList());
-        categoryWorkingStations.put(category.getIdentifier(), Lists.newLinkedList());
+        categories.put(category, category.getIdentifier());
+        reversedCategories.put(category.getIdentifier(), category);
+        recipeCategoryListMap.put(category.getIdentifier(), Lists.newArrayList());
+        categoryWorkingStations.put(category.getIdentifier(), Lists.newArrayList());
     }
     
     @SafeVarargs
@@ -126,32 +128,40 @@ public class RecipeHelperImpl implements RecipeHelper {
     
     @Override
     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 (EntryStack outputStack : recipeDisplay.getOutputEntries())
-                    if (stack.equals(outputStack))
-                        categoriesMap.get(recipeDisplay.getRecipeCategory()).add(recipeDisplay);
+        Map<RecipeCategory<?>, List<RecipeDisplay>> result = Maps.newLinkedHashMap();
+        for (Map.Entry<RecipeCategory<?>, Identifier> entry : categories.entrySet()) {
+            RecipeCategory<?> category = entry.getKey();
+            Identifier categoryId = entry.getValue();
+            Set<RecipeDisplay> set = Sets.newLinkedHashSet();
+            for (RecipeDisplay display : recipeCategoryListMap.get(categoryId)) {
+                for (EntryStack outputStack : display.getOutputEntries())
+                    if (stack.equals(outputStack) && isDisplayVisible(display)) {
+                        set.add(display);
+                        break;
+                    }
+            }
+            if (!set.isEmpty())
+                CollectionUtils.getOrPutEmptyList(result, category).addAll(set);
         }
         for (LiveRecipeGenerator<RecipeDisplay> liveRecipeGenerator : liveRecipeGenerators) {
-            liveRecipeGenerator.getRecipeFor(stack).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll(o));
+            RecipeCategory<?> category = getCategory(liveRecipeGenerator.getCategoryIdentifier());
+            Optional<List<RecipeDisplay>> recipeFor = liveRecipeGenerator.getRecipeFor(stack);
+            if (recipeFor.isPresent()) {
+                Set<RecipeDisplay> set = Sets.newLinkedHashSet();
+                for (RecipeDisplay display : recipeFor.get()) {
+                    if (isDisplayVisible(display))
+                        set.add(display);
+                }
+                if (!set.isEmpty())
+                    CollectionUtils.getOrPutEmptyList(result, category).addAll(set);
+            }
         }
-        Map<RecipeCategory<?>, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
-        categories.forEach(category -> {
-            if (categoriesMap.containsKey(category.getIdentifier()) && !categoriesMap.get(category.getIdentifier()).isEmpty())
-                recipeCategoryListMap.put(category, categoriesMap.get(category.getIdentifier()).stream().filter(this::isDisplayVisible).collect(Collectors.toList()));
-        });
-        for (RecipeCategory<?> category : Lists.newArrayList(recipeCategoryListMap.keySet()))
-            if (recipeCategoryListMap.get(category).isEmpty())
-                recipeCategoryListMap.remove(category);
-        return recipeCategoryListMap;
+        return result;
     }
     
     @Override
     public RecipeCategory<?> getCategory(Identifier identifier) {
-        return CollectionUtils.findFirstOrNull(categories, category -> category.getIdentifier().equals(identifier));
+        return reversedCategories.get(identifier);
     }
     
     @Override
@@ -171,44 +181,48 @@ public class RecipeHelperImpl implements RecipeHelper {
     
     @Override
     public Map<RecipeCategory<?>, List<RecipeDisplay>> getUsagesFor(EntryStack stack) {
-        Map<Identifier, Set<RecipeDisplay>> categoriesMap = new HashMap<>();
-        categories.forEach(f -> categoriesMap.put(f.getIdentifier(), Sets.newLinkedHashSet()));
-        for (Map.Entry<Identifier, List<RecipeDisplay>> entry : recipeCategoryListMap.entrySet()) {
-            boolean isWorkstationCategory = isStackWorkStationOfCategory(entry.getKey(), stack);
-            for (RecipeDisplay recipeDisplay : entry.getValue()) {
+        Map<RecipeCategory<?>, List<RecipeDisplay>> result = Maps.newLinkedHashMap();
+        for (Map.Entry<RecipeCategory<?>, Identifier> entry : categories.entrySet()) {
+            Set<RecipeDisplay> set = Sets.newLinkedHashSet();
+            RecipeCategory<?> category = entry.getKey();
+            Identifier categoryId = entry.getValue();
+            for (RecipeDisplay display : recipeCategoryListMap.get(categoryId)) {
                 back:
-                for (List<EntryStack> input : recipeDisplay.getInputEntries()) {
+                for (List<EntryStack> input : display.getInputEntries()) {
                     for (EntryStack otherEntry : input) {
                         if (otherEntry.equals(stack)) {
-                            categoriesMap.get(recipeDisplay.getRecipeCategory()).add(recipeDisplay);
+                            if (isDisplayVisible(display))
+                                set.add(display);
                             break back;
                         }
                     }
                 }
             }
-            if (isWorkstationCategory) {
-                for (RecipeDisplay recipeDisplay : entry.getValue()) {
-                    categoriesMap.get(recipeDisplay.getRecipeCategory()).add(recipeDisplay);
-                }
+            if (isStackWorkStationOfCategory(categoryId, stack)) {
+                set.addAll(recipeCategoryListMap.get(categoryId));
             }
+            if (!set.isEmpty())
+                CollectionUtils.getOrPutEmptyList(result, category).addAll(set);
         }
         for (LiveRecipeGenerator<RecipeDisplay> liveRecipeGenerator : liveRecipeGenerators) {
-            liveRecipeGenerator.getUsageFor(stack).ifPresent(o -> categoriesMap.get(liveRecipeGenerator.getCategoryIdentifier()).addAll(o));
-        }
-        Map<RecipeCategory<?>, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
-        for (RecipeCategory<?> category : categories) {
-            if (categoriesMap.containsKey(category.getIdentifier()) && !categoriesMap.get(category.getIdentifier()).isEmpty())
-                recipeCategoryListMap.put(category, CollectionUtils.filterSetToList(categoriesMap.get(category.getIdentifier()), this::isDisplayVisible));
+            RecipeCategory<?> category = getCategory(liveRecipeGenerator.getCategoryIdentifier());
+            Optional<List<RecipeDisplay>> recipeFor = liveRecipeGenerator.getUsageFor(stack);
+            if (recipeFor.isPresent()) {
+                Set<RecipeDisplay> set = Sets.newLinkedHashSet();
+                for (RecipeDisplay display : recipeFor.get()) {
+                    if (isDisplayVisible(display))
+                        set.add(display);
+                }
+                if (!set.isEmpty())
+                    CollectionUtils.getOrPutEmptyList(result, category).addAll(set);
+            }
         }
-        for (RecipeCategory<?> category : Lists.newArrayList(recipeCategoryListMap.keySet()))
-            if (recipeCategoryListMap.get(category).isEmpty())
-                recipeCategoryListMap.remove(category);
-        return recipeCategoryListMap;
+        return result;
     }
     
     @Override
     public List<RecipeCategory<?>> getAllCategories() {
-        return Collections.unmodifiableList(categories);
+        return Lists.newArrayList(categories.keySet());
     }
     
     @Override
@@ -234,6 +248,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         this.recipeManager = recipeManager;
         this.recipeCategoryListMap.clear();
         this.categories.clear();
+        this.reversedCategories.clear();
         this.autoCraftAreaSupplierMap.clear();
         this.screenClickAreas.clear();
         this.categoryWorkingStations.clear();
@@ -331,8 +346,9 @@ public class RecipeHelperImpl implements RecipeHelper {
         ((DisplayHelperImpl) DisplayHelper.getInstance()).resetCache();
         ScreenHelper.getOptionalOverlay().ifPresent(overlay -> overlay.shouldReInit = true);
         
+        displayVisibilityHandlers.sort(VISIBILITY_HANDLER_COMPARATOR);
         long usedTime = System.currentTimeMillis() - startTime;
-        RoughlyEnoughItemsCore.LOGGER.info("[REI] Registered %d stack entries, %d recipes displays, %d exclusion zones suppliers, %d bounds handler, %d visibility handlers and %d categories (%s) in %d ms.", EntryRegistry.getInstance().getStacksList().size(), recipeCount[0], DisplayHelper.getInstance().getBaseBoundsHandler().supplierSize(), DisplayHelper.getInstance().getAllBoundsHandlers().size(), getDisplayVisibilityHandlers().size(), categories.size(), categories.stream().map(RecipeCategory::getCategoryName).collect(Collectors.joining(", ")), usedTime);
+        RoughlyEnoughItemsCore.LOGGER.info("[REI] Registered %d stack entries, %d recipes displays, %d exclusion zones suppliers, %d bounds handler, %d visibility handlers and %d categories (%s) in %d ms.", EntryRegistry.getInstance().getStacksList().size(), recipeCount[0], DisplayHelper.getInstance().getBaseBoundsHandler().supplierSize(), DisplayHelper.getInstance().getAllBoundsHandlers().size(), getDisplayVisibilityHandlers().size(), categories.size(), categories.keySet().stream().map(RecipeCategory::getCategoryName).collect(Collectors.joining(", ")), usedTime);
     }
     
     @Override
@@ -359,15 +375,18 @@ public class RecipeHelperImpl implements RecipeHelper {
     
     @Override
     public Map<RecipeCategory<?>, List<RecipeDisplay>> getAllRecipes() {
-        Map<RecipeCategory<?>, List<RecipeDisplay>> map = Maps.newLinkedHashMap();
-        for (RecipeCategory<?> recipeCategory : categories) {
-            if (recipeCategoryListMap.containsKey(recipeCategory.getIdentifier())) {
-                List<RecipeDisplay> list = CollectionUtils.filter(recipeCategoryListMap.get(recipeCategory.getIdentifier()), this::isDisplayVisible);
-                if (!list.isEmpty())
-                    map.put(recipeCategory, list);
+        Map<RecipeCategory<?>, List<RecipeDisplay>> result = Maps.newLinkedHashMap();
+        for (Map.Entry<RecipeCategory<?>, Identifier> entry : categories.entrySet()) {
+            RecipeCategory<?> category = entry.getKey();
+            Identifier categoryId = entry.getValue();
+            List<RecipeDisplay> displays = recipeCategoryListMap.get(categoryId);
+            if (displays != null) {
+                displays.removeIf(this::isDisplayNotVisible);
+                if (!displays.isEmpty())
+                    result.put(category, Lists.newArrayList(displays));
             }
         }
-        return map;
+        return result;
     }
     
     @Override
@@ -391,22 +410,21 @@ public class RecipeHelperImpl implements RecipeHelper {
     }
     
     @Override
-    public boolean isDisplayVisible(RecipeDisplay display, boolean respectConfig) {
-        return isDisplayVisible(display);
+    public boolean isDisplayNotVisible(RecipeDisplay display) {
+        return !isDisplayVisible(display);
     }
     
     @Override
     public boolean isDisplayVisible(RecipeDisplay display) {
         RecipeCategory<?> category = getCategory(display.getRecipeCategory());
-        List<DisplayVisibilityHandler> list = getDisplayVisibilityHandlers().stream().sorted(VISIBILITY_HANDLER_COMPARATOR).collect(Collectors.toList());
-        for (DisplayVisibilityHandler displayVisibilityHandler : list) {
-            try {
+        try {
+            for (DisplayVisibilityHandler displayVisibilityHandler : displayVisibilityHandlers) {
                 ActionResult visibility = displayVisibilityHandler.handleDisplay(category, display);
                 if (visibility != ActionResult.PASS)
                     return visibility == ActionResult.SUCCESS;
-            } catch (Throwable throwable) {
-                RoughlyEnoughItemsCore.LOGGER.error("[REI] Failed to check if the recipe is visible!", throwable);
             }
+        } catch (Throwable throwable) {
+            RoughlyEnoughItemsCore.LOGGER.error("[REI] Failed to check if the recipe is visible!", throwable);
         }
         return true;
     }

+ 112 - 0
src/main/java/me/shedaniel/rei/impl/RenderingEntry.java

@@ -0,0 +1,112 @@
+package me.shedaniel.rei.impl;
+
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.util.Identifier;
+
+import javax.annotation.Nullable;
+import java.util.Optional;
+
+public abstract class RenderingEntry extends DrawableHelper implements EntryStack {
+    @Override
+    public Optional<Identifier> getIdentifier() {
+        return Optional.empty();
+    }
+    
+    @Override
+    public Type getType() {
+        return Type.RENDER;
+    }
+    
+    @Override
+    public int getAmount() {
+        return 0;
+    }
+    
+    @Override
+    public void setAmount(int amount) {
+    
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+    
+    @Override
+    public EntryStack copy() {
+        return this;
+    }
+    
+    @Override
+    public Object getObject() {
+        return null;
+    }
+    
+    @Override
+    public boolean equals(EntryStack stack, boolean ignoreTags, boolean ignoreAmount) {
+        return stack == this;
+    }
+    
+    @Override
+    public boolean equalsIgnoreTagsAndAmount(EntryStack stack) {
+        return stack == this;
+    }
+    
+    @Override
+    public boolean equalsIgnoreTags(EntryStack stack) {
+        return stack == this;
+    }
+    
+    @Override
+    public boolean equalsIgnoreAmount(EntryStack stack) {
+        return stack == this;
+    }
+    
+    @Override
+    public boolean equalsAll(EntryStack stack) {
+        return stack == this;
+    }
+    
+    @Override
+    public int getZ() {
+        return getBlitOffset();
+    }
+    
+    @Override
+    public void setZ(int z) {
+        setBlitOffset(z);
+    }
+    
+    @Override
+    public <T> EntryStack setting(Settings<T> settings, T value) {
+        return this;
+    }
+    
+    @Override
+    public <T> EntryStack removeSetting(Settings<T> settings) {
+        return this;
+    }
+    
+    @Override
+    public EntryStack clearSettings() {
+        return this;
+    }
+    
+    @Override
+    public <T> EntryStack addSetting(Settings<T> settings, T value) {
+        return this;
+    }
+    
+    @Override
+    public <T> T get(Settings<T> settings) {
+        return settings.getDefaultValue();
+    }
+    
+    @Nullable
+    @Override
+    public QueuedTooltip getTooltip(int mouseX, int mouseY) {
+        return null;
+    }
+}

+ 26 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java

@@ -30,6 +30,8 @@ import me.shedaniel.rei.plugin.crafting.DefaultShapedDisplay;
 import me.shedaniel.rei.plugin.crafting.DefaultShapelessDisplay;
 import me.shedaniel.rei.plugin.fuel.DefaultFuelCategory;
 import me.shedaniel.rei.plugin.fuel.DefaultFuelDisplay;
+import me.shedaniel.rei.plugin.information.DefaultInformationCategory;
+import me.shedaniel.rei.plugin.information.DefaultInformationDisplay;
 import me.shedaniel.rei.plugin.smelting.DefaultSmeltingDisplay;
 import me.shedaniel.rei.plugin.smoking.DefaultSmokingDisplay;
 import me.shedaniel.rei.plugin.stonecutting.DefaultStoneCuttingCategory;
@@ -69,9 +71,11 @@ public class DefaultPlugin implements REIPluginV0 {
     public static final Identifier PLUGIN = new Identifier("roughlyenoughitems", "default_plugin");
     public static final Identifier COMPOSTING = new Identifier("minecraft", "plugins/composting");
     public static final Identifier FUEL = new Identifier("minecraft", "plugins/fuel");
+    public static final Identifier INFO = new Identifier("roughlyenoughitems", "plugins/information");
     private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
     private static final Identifier DISPLAY_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/display_dark.png");
     private static final List<DefaultBrewingDisplay> BREWING_DISPLAYS = Lists.newArrayList();
+    private static final List<DefaultInformationDisplay> INFO_DISPLAYS = Lists.newArrayList();
     
     public static Identifier getDisplayTexture() {
         return ScreenHelper.isDarkModeEnabled() ? DISPLAY_TEXTURE_DARK : DISPLAY_TEXTURE;
@@ -81,6 +85,10 @@ public class DefaultPlugin implements REIPluginV0 {
         BREWING_DISPLAYS.add(display);
     }
     
+    public static void registerInfoDisplay(DefaultInformationDisplay display) {
+        INFO_DISPLAYS.add(display);
+    }
+    
     @Override
     public Identifier getPluginIdentifier() {
         return PLUGIN;
@@ -91,6 +99,11 @@ public class DefaultPlugin implements REIPluginV0 {
         return SemanticVersion.parse("3.2.33");
     }
     
+    @Override
+    public void preRegister() {
+        INFO_DISPLAYS.clear();
+    }
+    
     @Override
     public void registerEntries(EntryRegistry entryRegistry) {
         if (!ConfigObject.getInstance().isLoadingDefaultPlugin()) {
@@ -149,6 +162,13 @@ public class DefaultPlugin implements REIPluginV0 {
         if (!ConfigObject.getInstance().isLoadingDefaultPlugin()) {
             return;
         }
+        //        DefaultPlugin.registerInfoDisplay(DefaultInformationDisplay.createFromEntry(EntryStack.create(Items.FURNACE), new LiteralText("Furnace Info"))
+        //                .lines(new LiteralText("Furnace is a nice block, crafted using 8 cobblestone."),
+        //                        new LiteralText("An amazing tool to burn lil taters."),
+        //                        new LiteralText("Now available in a store next to you."),
+        //                        new LiteralText("Now with 60% off for an limited time!"),
+        //                        new LiteralText("Get it with coupon code: ").append(new LiteralText("TATERS").formatted(Formatting.BOLD))
+        //                        ));
         recipeHelper.registerRecipes(CRAFTING, ShapelessRecipe.class, DefaultShapelessDisplay::new);
         recipeHelper.registerRecipes(CRAFTING, ShapedRecipe.class, DefaultShapedDisplay::new);
         recipeHelper.registerRecipes(SMELTING, SmeltingRecipe.class, DefaultSmeltingDisplay::new);
@@ -186,9 +206,7 @@ public class DefaultPlugin implements REIPluginV0 {
                 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);
-        });
+        stacks.sort((first, second) -> (int) ((map.get(first) - map.get(second)) * 100));
         for (int i = 0; i < stacks.size(); i += MathHelper.clamp(48, 1, stacks.size() - i)) {
             List<ItemConvertible> thisStacks = Lists.newArrayList();
             for (int j = i; j < i + 48; j++)
@@ -203,6 +221,10 @@ public class DefaultPlugin implements REIPluginV0 {
     
     @Override
     public void postRegister() {
+        RecipeHelper.getInstance().registerCategory(new DefaultInformationCategory());
+        for (DefaultInformationDisplay display : INFO_DISPLAYS) {
+            RecipeHelper.getInstance().registerDisplay(INFO, display);
+        }
         // Sit tight! This will be a fast journey!
         long time = System.currentTimeMillis();
         for (EntryStack stack : EntryRegistry.getInstance().getStacksList())
@@ -321,6 +343,7 @@ public class DefaultPlugin implements REIPluginV0 {
         recipeHelper.registerWorkingStations(COMPOSTING, EntryStack.create(Items.COMPOSTER));
         recipeHelper.removeAutoCraftButton(FUEL);
         recipeHelper.removeAutoCraftButton(COMPOSTING);
+        recipeHelper.removeAutoCraftButton(INFO);
         recipeHelper.registerScreenClickArea(new Rectangle(88, 32, 28, 23), CraftingTableScreen.class, CRAFTING);
         recipeHelper.registerScreenClickArea(new Rectangle(137, 29, 10, 13), InventoryScreen.class, CRAFTING);
         recipeHelper.registerScreenClickArea(new Rectangle(97, 16, 14, 30), BrewingStandScreen.class, BREWING);

+ 0 - 1
src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingCategory.java

@@ -104,7 +104,6 @@ public class DefaultCompostingCategory implements RecipeCategory<DefaultComposti
         return 140;
     }
     
-    @SuppressWarnings("deprecation")
     @Override
     public int getFixedRecipesPerPage() {
         return 1;

+ 275 - 0
src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java

@@ -0,0 +1,275 @@
+package me.shedaniel.rei.plugin.information;
+
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
+import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
+import me.shedaniel.math.api.Point;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.gui.entries.RecipeEntry;
+import me.shedaniel.rei.gui.widget.*;
+import me.shedaniel.rei.impl.RenderingEntry;
+import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.plugin.DefaultPlugin;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.BufferRenderer;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.client.util.Texts;
+import net.minecraft.client.util.math.Matrix4f;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+
+import javax.annotation.Nullable;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+public class DefaultInformationCategory implements RecipeCategory<DefaultInformationDisplay> {
+    @Override
+    public Identifier getIdentifier() {
+        return DefaultPlugin.INFO;
+    }
+    
+    @Override
+    public String getCategoryName() {
+        return I18n.translate("category.rei.information");
+    }
+    
+    @Override
+    public RecipeEntry getSimpleRenderer(DefaultInformationDisplay recipe) {
+        Text name = recipe.getName();
+        return new RecipeEntry() {
+            @Override
+            public int getHeight() {
+                return 10 + MinecraftClient.getInstance().textRenderer.fontHeight;
+            }
+            
+            @Nullable
+            @Override
+            public QueuedTooltip getTooltip(int mouseX, int mouseY) {
+                return null;
+            }
+            
+            @Override
+            public void render(Rectangle rectangle, int mouseX, int mouseY, float delta) {
+                MinecraftClient.getInstance().textRenderer.draw(name.asFormattedString(), rectangle.x + 5, rectangle.y + 6, -1);
+            }
+        };
+    }
+    
+    @Override
+    public EntryStack getLogo() {
+        return new RenderingEntry() {
+            @Override
+            public void render(Rectangle bounds, int mouseX, int mouseY, float delta) {
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
+                Matrix4f matrix4f = Matrix4f.method_24021(-1.2f, -1, 0);
+                DefaultInformationCategory.innerBlit(matrix4f, bounds.getCenterX() - 8, bounds.getCenterX() + 8, bounds.getCenterY() - 8, bounds.getCenterY() + 8, 0, 116f / 256f, (116f + 16f) / 256f, 0f, 16f / 256f);
+            }
+        };
+    }
+    
+    protected static void innerBlit(Matrix4f matrix4f, int xStart, int xEnd, int yStart, int yEnd, int z, float uStart, float uEnd, float vStart, float vEnd) {
+        BufferBuilder bufferBuilder = Tessellator.getInstance().getBuffer();
+        bufferBuilder.begin(7, VertexFormats.POSITION_TEXTURE);
+        bufferBuilder.vertex(matrix4f, xStart, yEnd, z).texture(uStart, vEnd).next();
+        bufferBuilder.vertex(matrix4f, xEnd, yEnd, z).texture(uEnd, vEnd).next();
+        bufferBuilder.vertex(matrix4f, xEnd, yStart, z).texture(uEnd, vStart).next();
+        bufferBuilder.vertex(matrix4f, xStart, yStart, z).texture(uStart, vStart).next();
+        bufferBuilder.end();
+        RenderSystem.enableAlphaTest();
+        BufferRenderer.draw(bufferBuilder);
+    }
+    
+    @Override
+    public List<Widget> setupDisplay(Supplier<DefaultInformationDisplay> recipeDisplaySupplier, Rectangle bounds) {
+        DefaultInformationDisplay display = recipeDisplaySupplier.get();
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(new LabelWidget(new Point(bounds.getCenterX(), bounds.y + 3), display.getName().asFormattedString()).noShadow().color(ScreenHelper.isDarkModeEnabled() ? 0xFFBBBBBB : 0xFF404040));
+        widgets.add(EntryWidget.create(bounds.getCenterX() - 8, bounds.y + 15).entries(display.getEntryStacks()));
+        Rectangle rectangle = new Rectangle(bounds.getCenterX() - (bounds.width / 2), bounds.y + 35, bounds.width, bounds.height - 40);
+        widgets.add(new SlotBaseWidget(rectangle));
+        widgets.add(new ScrollableTextWidget(rectangle, display.getTexts()));
+        return widgets;
+    }
+    
+    @Override
+    public int getDisplayHeight() {
+        return 140;
+    }
+    
+    @Override
+    public int getFixedRecipesPerPage() {
+        return 1;
+    }
+    
+    private static class ScrollableTextWidget extends WidgetWithBounds {
+        private Rectangle bounds;
+        private List<Text> texts;
+        private double target;
+        private double scroll;
+        private long start;
+        private long duration;
+        
+        public ScrollableTextWidget(Rectangle bounds, List<Text> texts) {
+            this.bounds = bounds;
+            this.texts = Lists.newArrayList();
+            for (Text text : texts) {
+                if (!this.texts.isEmpty())
+                    this.texts.add(null);
+                this.texts.addAll(Texts.wrapLines(text, bounds.width - 11, MinecraftClient.getInstance().textRenderer, true, false));
+            }
+        }
+        
+        @Override
+        public boolean mouseScrolled(double double_1, double double_2, double double_3) {
+            if (containsMouse(double_1, double_2)) {
+                offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+                return true;
+            }
+            return false;
+        }
+        
+        public void offset(double value, boolean animated) {
+            scrollTo(target + value, animated);
+        }
+        
+        public void scrollTo(double value, boolean animated) {
+            scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
+        }
+        
+        public void scrollTo(double value, boolean animated, long duration) {
+            target = clamp(value);
+            
+            if (animated) {
+                start = System.currentTimeMillis();
+                this.duration = duration;
+            } else
+                scroll = target;
+        }
+        
+        public final double clamp(double v) {
+            return clamp(v, DynamicEntryListWidget.SmoothScrollingSettings.CLAMP_EXTENSION);
+        }
+        
+        public final double clamp(double v, double clampExtension) {
+            return MathHelper.clamp(v, -clampExtension, getMaxScroll() + clampExtension);
+        }
+        
+        protected int getMaxScroll() {
+            return Math.max(0, this.getMaxScrollPosition() - this.getBounds().height + 4);
+        }
+        
+        protected int getMaxScrollPosition() {
+            int i = 0;
+            for (Text entry : texts) {
+                i += entry == null ? 4 : font.fontHeight;
+            }
+            return i;
+        }
+        
+        @Override
+        public Rectangle getBounds() {
+            return bounds;
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, float delta) {
+            updatePosition(delta);
+            Rectangle innerBounds = new Rectangle(bounds.x + 1, bounds.y + 1, bounds.width - 7, bounds.height - 2);
+            ScissorsHandler.INSTANCE.scissor(innerBounds);
+            int currentY = (int) -scroll + innerBounds.y;
+            for (Text text : texts) {
+                if (text != null && currentY + font.fontHeight >= innerBounds.y && currentY <= innerBounds.getMaxY()) {
+                    font.draw(text.asFormattedString(), innerBounds.x + 2, currentY + 2, ScreenHelper.isDarkModeEnabled() ? 0xFFBBBBBB : 0xFF090909);
+                }
+                currentY += text == null ? 4 : font.fontHeight;
+            }
+            ScissorsHandler.INSTANCE.removeLastScissor();
+            ScissorsHandler.INSTANCE.scissor(bounds);
+            RenderSystem.enableBlend();
+            RenderSystem.blendFuncSeparate(770, 771, 0, 1);
+            RenderSystem.disableAlphaTest();
+            RenderSystem.shadeModel(7425);
+            RenderSystem.disableTexture();
+            renderScrollBar();
+            RenderSystem.enableTexture();
+            RenderSystem.shadeModel(7424);
+            RenderSystem.enableAlphaTest();
+            RenderSystem.disableBlend();
+            ScissorsHandler.INSTANCE.removeLastScissor();
+        }
+        
+        @SuppressWarnings("deprecation")
+        private void renderScrollBar() {
+            int maxScroll = getMaxScroll();
+            int scrollbarPositionMinX = getBounds().getMaxX() - 7;
+            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
+            Tessellator tessellator = Tessellator.getInstance();
+            BufferBuilder buffer = tessellator.getBuffer();
+            if (maxScroll > 0) {
+                int height = (int) (((this.getBounds().height - 2f) * (this.getBounds().height - 2f)) / this.getMaxScrollPosition());
+                height = MathHelper.clamp(height, 32, this.getBounds().height - 2);
+                height -= Math.min((scroll < 0 ? (int) -scroll : scroll > maxScroll ? (int) scroll - maxScroll : 0), height * .95);
+                height = Math.max(10, height);
+                int minY = Math.min(Math.max((int) scroll * (this.getBounds().height - 2 - height) / maxScroll + getBounds().y + 1, getBounds().y + 1), getBounds().getMaxY() -1 - height);
+    
+                boolean hovered = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.fromMouse());
+                int bottomC = hovered ? 168 : 128;
+                int topC = hovered ? 222 : 172;
+                
+                // Black Bar
+                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+                buffer.vertex(scrollbarPositionMinX, this.getBounds().y + 1, 0.0D).texture(0, 1).color(0, 0, 0, 255).next();
+                buffer.vertex(scrollbarPositionMaxX, this.getBounds().y + 1, 0.0D).texture(1, 1).color(0, 0, 0, 255).next();
+                buffer.vertex(scrollbarPositionMaxX, getBounds().getMaxY() - 1, 0.0D).texture(1, 0).color(0, 0, 0, 255).next();
+                buffer.vertex(scrollbarPositionMinX, getBounds().getMaxY() - 1, 0.0D).texture(0, 0).color(0, 0, 0, 255).next();
+                tessellator.draw();
+                
+                // Bottom
+                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+                buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).texture(0, 1).color(bottomC, bottomC, bottomC, 255).next();
+                buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).texture(1, 1).color(bottomC, bottomC, bottomC, 255).next();
+                buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).texture(1, 0).color(bottomC, bottomC, bottomC, 255).next();
+                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(bottomC, bottomC, bottomC, 255).next();
+                tessellator.draw();
+                
+                // Top
+                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
+                buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).texture(0, 1).color(topC, topC, topC, 255).next();
+                buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).texture(1, 1).color(topC, topC, topC, 255).next();
+                buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).texture(1, 0).color(topC, topC, topC, 255).next();
+                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(topC, topC, topC, 255).next();
+                tessellator.draw();
+            }
+        }
+        
+        private void updatePosition(float delta) {
+            target = clamp(target);
+            if (target < 0) {
+                target -= target * (1 - ClothConfigInitializer.getBounceBackMultiplier()) * delta / 3;
+            } else if (target > getMaxScroll()) {
+                target = (target - getMaxScroll()) * (1 - (1 - ClothConfigInitializer.getBounceBackMultiplier()) * delta / 3) + getMaxScroll();
+            }
+            if (!DynamicNewSmoothScrollingEntryListWidget.Precision.almostEquals(scroll, target, DynamicNewSmoothScrollingEntryListWidget.Precision.FLOAT_EPSILON))
+                scroll = (float) DynamicNewSmoothScrollingEntryListWidget.Interpolation.expoEase(scroll, target, Math.min((System.currentTimeMillis() - start) / ((double) duration), 1));
+            else
+                scroll = target;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+    }
+}

+ 75 - 0
src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationDisplay.java

@@ -0,0 +1,75 @@
+package me.shedaniel.rei.plugin.information;
+
+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.text.Text;
+import net.minecraft.util.Identifier;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class DefaultInformationDisplay implements RecipeDisplay {
+    private List<EntryStack> entryStacks;
+    private List<Text> texts;
+    private Text name;
+    
+    protected DefaultInformationDisplay(List<EntryStack> entryStacks, Text name) {
+        this.entryStacks = entryStacks;
+        this.name = name;
+        this.texts = Lists.newArrayList();
+    }
+    
+    public static DefaultInformationDisplay createFromEntries(List<EntryStack> entryStacks, Text name) {
+        return new DefaultInformationDisplay(entryStacks, name);
+    }
+    
+    public static DefaultInformationDisplay createFromEntry(EntryStack entryStack, Text name) {
+        return createFromEntries(Collections.singletonList(entryStack), name);
+    }
+    
+    @Override
+    public List<List<EntryStack>> getInputEntries() {
+        return Collections.singletonList(entryStacks);
+    }
+    
+    @Override
+    public List<EntryStack> getOutputEntries() {
+        return Collections.emptyList();
+    }
+    
+    public DefaultInformationDisplay line(Text line) {
+        texts.add(line);
+        return this;
+    }
+    
+    public DefaultInformationDisplay lines(Text... lines) {
+        texts.addAll(Arrays.asList(lines));
+        return this;
+    }
+    
+    public DefaultInformationDisplay lines(Collection<Text> lines) {
+        texts.addAll(lines);
+        return this;
+    }
+    
+    List<EntryStack> getEntryStacks() {
+        return entryStacks;
+    }
+    
+    Text getName() {
+        return name;
+    }
+    
+    List<Text> getTexts() {
+        return texts;
+    }
+    
+    @Override
+    public Identifier getRecipeCategory() {
+        return DefaultPlugin.INFO;
+    }
+}

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

@@ -16,6 +16,14 @@ import java.util.function.Predicate;
 
 @Internal
 public class CollectionUtils {
+    public static <A, B> List<B> getOrPutEmptyList(Map<A, List<B>> map, A key) {
+        List<B> b = map.get(key);
+        if (b != null)
+            return b;
+        map.put(key, Lists.newArrayList());
+        return map.get(key);
+    }
+    
     public static <T> T findFirstOrNullEquals(List<T> list, T obj) {
         for (T t : list) {
             if (t.equals(obj))

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

@@ -25,6 +25,7 @@
   "category.rei.brewing.result": "Resulted Potion",
   "category.rei.composting": "Composting",
   "category.rei.stripping": "Stripping",
+  "category.rei.information": "Information",
   "text.rei.composting.chance": "§e%d%% Chance",
   "text.rei.composting.page": "Page %d",
   "text.rei.config": "Config",

BIN
src/main/resources/assets/roughlyenoughitems/textures/gui/display.png


BIN
src/main/resources/assets/roughlyenoughitems/textures/gui/display_dark.png


+ 12 - 12
src/main/resources/fabric.mod.json

@@ -5,7 +5,7 @@
   "description": "To allow players to view items and recipes.",
   "version": "${version}",
   "authors": [
-    "Danielshe"
+    "shedaniel"
   ],
   "contact": {
     "homepage": "https://minecraft.curseforge.com/projects/roughly-enough-items",
@@ -45,19 +45,19 @@
   ],
   "custom": {
     "rei:translators": {
-      "English": "Danielshe",
+      "English": "shedaniel",
       "Japanese": ["swordglowsblue", "hinataaki"],
-      "Simplified Chinese": ["XuyuEre", "Danielshe"],
-      "Traditional Chinese": ["hugoalh", "gxy17886", "Danielshe"],
-      "French": "Yanis48",
-      "German": "MelanX",
-      "Estonian": "Madis0",
-      "Portuguese": "thiagokenis",
-      "LOLCAT": "Danielshe",
-      "Upside Down English": "Danielshe",
+      "Simplified Chinese": ["XuyuEre", "shedaniel", "SciUniv_Moring", "Takakura-Anri"],
+      "Traditional Chinese": ["hugoalh", "gxy17886", "shedaniel"],
+      "French": ["Yanis48", "Koockies"],
+      "German": ["MelanX", "guntram7"],
+      "Estonian": ["Madis0"],
+      "Portuguese": ["thiagokenis"],
+      "LOLCAT": ["shedaniel", "RaxedMC"],
+      "Upside Down English": ["shedaniel"],
       "Brazilian Portuguese": ["thiagokenis", "joaoh1"],
-      "Bulgarian": "geniiii",
-      "Russian": "MrYonter"
+      "Bulgarian": ["geniiii"],
+      "Russian": ["MrYonter", "kwmika1girl", "LimyChitou", "Great_Manalal", "s3rbug", "TheByKotik", "ebogish"]
     }
   }
 }