Parcourir la source

Added FocusedStackProvider api.
Optimised item rendering.

Signed-off-by: shedaniel <daniel@shedaniel.me>

shedaniel il y a 5 ans
Parent
commit
2a585e713b

+ 1 - 1
gradle.properties

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

+ 40 - 0
src/main/java/me/shedaniel/rei/api/FocusedStackProvider.java

@@ -0,0 +1,40 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api;
+
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.util.TypedActionResult;
+import org.jetbrains.annotations.NotNull;
+
+public interface FocusedStackProvider {
+    /**
+     * @return the priority of this handler, higher priorities will be called first.
+     */
+    default double getPriority() {
+        return 0d;
+    }
+    
+    @NotNull
+    TypedActionResult<EntryStack> provide(Screen screen);
+}

+ 11 - 1
src/main/java/me/shedaniel/rei/api/OptimalEntryStack.java

@@ -24,15 +24,25 @@
 package me.shedaniel.rei.api;
 
 import me.shedaniel.math.Rectangle;
+import net.minecraft.client.render.VertexConsumerProvider;
 import net.minecraft.client.util.math.MatrixStack;
 import org.jetbrains.annotations.ApiStatus;
 
 @ApiStatus.Internal
 public interface OptimalEntryStack {
+    static int groupingHashFrom(EntryStack stack) {
+        if (stack instanceof OptimalEntryStack) return ((OptimalEntryStack) stack).groupingHash();
+        return stack.getClass().hashCode();
+    }
+    
+    default int groupingHash() {
+        return getClass().hashCode();
+    }
+    
     default void optimisedRenderStart(MatrixStack matrices, float delta) {
     }
     
-    default void optimisedRenderBase(MatrixStack matrices, Rectangle bounds, int mouseX, int mouseY, float delta) {
+    default void optimisedRenderBase(MatrixStack matrices, VertexConsumerProvider.Immediate immediate, Rectangle bounds, int mouseX, int mouseY, float delta) {
     }
     
     default void optimisedRenderOverlay(MatrixStack matrices, Rectangle bounds, int mouseX, int mouseY, float delta) {

+ 8 - 0
src/main/java/me/shedaniel/rei/api/RecipeHelper.java

@@ -27,12 +27,14 @@ import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.ContainerScreen;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.recipe.RecipeManager;
 import net.minecraft.util.Identifier;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.List;
 import java.util.Map;
@@ -53,6 +55,12 @@ public interface RecipeHelper {
     
     AutoTransferHandler registerAutoCraftingHandler(AutoTransferHandler handler);
     
+    void registerFocusedStackProvider(FocusedStackProvider provider);
+    
+    @Nullable
+    @ApiStatus.Internal
+    EntryStack getScreenFocusedStack(Screen screen);
+    
     List<AutoTransferHandler> getSortedAutoCraftingHandler();
     
     /**

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

@@ -642,14 +642,9 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
             ScreenHelper.toggleOverlayVisible();
             return true;
         }
-        ItemStack itemStack = null;
-        if (MinecraftClient.getInstance().currentScreen instanceof ContainerScreen) {
-            ContainerScreen<?> containerScreen = (ContainerScreen<?>) MinecraftClient.getInstance().currentScreen;
-            if (containerScreen.focusedSlot != null && !containerScreen.focusedSlot.getStack().isEmpty())
-                itemStack = containerScreen.focusedSlot.getStack();
-        }
-        if (itemStack != null && !itemStack.isEmpty()) {
-            EntryStack stack = EntryStack.create(itemStack.copy());
+        EntryStack stack = RecipeHelper.getInstance().getScreenFocusedStack(MinecraftClient.getInstance().currentScreen);
+        if (stack != null && !stack.isEmpty()) {
+            stack = stack.copy();
             if (ConfigObject.getInstance().getRecipeKeybind().matchesKey(keyCode, scanCode)) {
                 return ClientHelper.getInstance().openView(ClientHelper.ViewSearchBuilder.builder().addRecipesFor(stack).setOutputNotice(stack).fillPreferredOpenedCategory());
             } else if (ConfigObject.getInstance().getUsageKeybind().matchesKey(keyCode, scanCode)) {

+ 33 - 30
src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -57,7 +57,10 @@ import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.*;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.*;
 import java.util.function.Supplier;
 import java.util.stream.Collectors;
@@ -244,32 +247,32 @@ public class EntryListWidget extends WidgetWithBounds {
             scrolling.renderScrollBar(0, 1, REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
         } else {
             if (debugTime) {
-                int size = 0;
-                long time = 0;
+                int[] size = {0};
+                long[] time = {0};
                 for (Widget widget : renders) {
                     widget.render(matrices, mouseX, mouseY, delta);
                 }
                 long totalTimeStart = System.nanoTime();
                 if (ConfigObject.getInstance().doesFastEntryRendering()) {
-                    for (Map.Entry<? extends Class<? extends EntryStack>, List<EntryListEntry>> entry : entries.stream().collect(Collectors.groupingBy(entryListEntry -> entryListEntry.getCurrentEntry().getClass())).entrySet()) {
-                        List<EntryListEntry> list = entry.getValue();
-                        if (list.isEmpty())
-                            continue;
-                        EntryListEntry firstWidget = list.get(0);
+                    entries.stream().collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).forEach((integer, entries) -> {
+                        if (entries.isEmpty()) return;
+                        EntryListEntry firstWidget = entries.get(0);
                         EntryStack first = firstWidget.getCurrentEntry();
                         if (first instanceof OptimalEntryStack) {
                             OptimalEntryStack firstStack = (OptimalEntryStack) first;
                             firstStack.optimisedRenderStart(matrices, delta);
                             long l = System.nanoTime();
-                            for (EntryListEntry listEntry : list) {
+                            VertexConsumerProvider.Immediate immediate = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers();
+                            for (EntryListEntry listEntry : entries) {
                                 EntryStack currentEntry = listEntry.getCurrentEntry();
                                 currentEntry.setZ(100);
                                 listEntry.drawBackground(matrices, mouseX, mouseY, delta);
-                                ((OptimalEntryStack) currentEntry).optimisedRenderBase(matrices, listEntry.getInnerBounds(), mouseX, mouseY, delta);
+                                ((OptimalEntryStack) currentEntry).optimisedRenderBase(matrices, immediate, listEntry.getInnerBounds(), mouseX, mouseY, delta);
                                 if (!currentEntry.isEmpty())
-                                    size++;
+                                    size[0]++;
                             }
-                            for (EntryListEntry listEntry : list) {
+                            immediate.draw();
+                            for (EntryListEntry listEntry : entries) {
                                 EntryStack currentEntry = listEntry.getCurrentEntry();
                                 ((OptimalEntryStack) currentEntry).optimisedRenderOverlay(matrices, listEntry.getInnerBounds(), mouseX, mouseY, delta);
                                 if (listEntry.containsMouse(mouseX, mouseY)) {
@@ -277,33 +280,33 @@ public class EntryListWidget extends WidgetWithBounds {
                                     listEntry.drawHighlighted(matrices, mouseX, mouseY, delta);
                                 }
                             }
-                            time += (System.nanoTime() - l);
+                            time[0] += (System.nanoTime() - l);
                             firstStack.optimisedRenderEnd(matrices, delta);
                         } else {
-                            for (EntryListEntry listEntry : list) {
+                            for (EntryListEntry listEntry : entries) {
                                 if (listEntry.getCurrentEntry().isEmpty())
                                     continue;
-                                size++;
+                                size[0]++;
                                 long l = System.nanoTime();
                                 listEntry.render(matrices, mouseX, mouseY, delta);
-                                time += (System.nanoTime() - l);
+                                time[0] += (System.nanoTime() - l);
                             }
                         }
-                    }
+                    });
                 } else {
                     for (EntryListEntry entry : entries) {
                         if (entry.getCurrentEntry().isEmpty())
                             continue;
-                        size++;
+                        size[0]++;
                         long l = System.nanoTime();
                         entry.render(matrices, mouseX, mouseY, delta);
-                        time += (System.nanoTime() - l);
+                        time[0] += (System.nanoTime() - l);
                     }
                 }
                 long totalTime = System.nanoTime() - totalTimeStart;
                 int z = getZ();
                 setZ(500);
-                Text debugText = new LiteralText(String.format("%d entries, avg. %.0fns, ttl. %.0fms, %s fps", size, time / (double) size, totalTime / 1000000d, minecraft.fpsDebugString.split(" ")[0]));
+                Text debugText = new LiteralText(String.format("%d entries, avg. %.0fns, ttl. %.0fms, %s fps", size[0], time[0] / (double) size[0], totalTime / 1000000d, minecraft.fpsDebugString.split(" ")[0]));
                 int stringWidth = font.getWidth(debugText);
                 fillGradient(matrices, Math.min(bounds.x, minecraft.currentScreen.width - stringWidth - 2), bounds.y, bounds.x + stringWidth + 2, bounds.y + font.fontHeight + 2, -16777216, -16777216);
                 VertexConsumerProvider.Immediate immediate = VertexConsumerProvider.immediate(Tessellator.getInstance().getBuffer());
@@ -319,22 +322,22 @@ public class EntryListWidget extends WidgetWithBounds {
                     widget.render(matrices, mouseX, mouseY, delta);
                 }
                 if (ConfigObject.getInstance().doesFastEntryRendering()) {
-                    for (Map.Entry<? extends Class<? extends EntryStack>, List<EntryListEntry>> entry : entries.stream().collect(Collectors.groupingBy(entryListEntry -> entryListEntry.getCurrentEntry().getClass())).entrySet()) {
-                        List<EntryListEntry> list = entry.getValue();
-                        if (list.isEmpty())
-                            continue;
-                        EntryListEntry firstWidget = list.get(0);
+                    entries.stream().collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).forEach((integer, entries) -> {
+                        if (entries.isEmpty()) return;
+                        EntryListEntry firstWidget = entries.get(0);
                         EntryStack first = firstWidget.getCurrentEntry();
                         if (first instanceof OptimalEntryStack) {
                             OptimalEntryStack firstStack = (OptimalEntryStack) first;
                             firstStack.optimisedRenderStart(matrices, delta);
-                            for (EntryListEntry listEntry : list) {
+                            VertexConsumerProvider.Immediate immediate = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers();
+                            for (EntryListEntry listEntry : entries) {
                                 EntryStack currentEntry = listEntry.getCurrentEntry();
                                 currentEntry.setZ(100);
                                 listEntry.drawBackground(matrices, mouseX, mouseY, delta);
-                                ((OptimalEntryStack) currentEntry).optimisedRenderBase(matrices, listEntry.getInnerBounds(), mouseX, mouseY, delta);
+                                ((OptimalEntryStack) currentEntry).optimisedRenderBase(matrices, immediate, listEntry.getInnerBounds(), mouseX, mouseY, delta);
                             }
-                            for (EntryListEntry listEntry : list) {
+                            immediate.draw();
+                            for (EntryListEntry listEntry : entries) {
                                 EntryStack currentEntry = listEntry.getCurrentEntry();
                                 ((OptimalEntryStack) currentEntry).optimisedRenderOverlay(matrices, listEntry.getInnerBounds(), mouseX, mouseY, delta);
                                 if (listEntry.containsMouse(mouseX, mouseY)) {
@@ -344,13 +347,13 @@ public class EntryListWidget extends WidgetWithBounds {
                             }
                             firstStack.optimisedRenderEnd(matrices, delta);
                         } else {
-                            for (EntryListEntry listEntry : list) {
+                            for (EntryListEntry listEntry : entries) {
                                 if (listEntry.getCurrentEntry().isEmpty())
                                     continue;
                                 listEntry.render(matrices, mouseX, mouseY, delta);
                             }
                         }
-                    }
+                    });
                 } else {
                     for (EntryListEntry listEntry : entries) {
                         if (listEntry.getCurrentEntry().isEmpty())

+ 43 - 16
src/main/java/me/shedaniel/rei/impl/ItemEntryStack.java

@@ -25,6 +25,7 @@ package me.shedaniel.rei.impl;
 
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.platform.GlStateManager;
+import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.*;
@@ -53,7 +54,6 @@ import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -293,22 +293,52 @@ public class ItemEntryStack extends AbstractEntryStack implements OptimalEntrySt
     @Override
     public void render(MatrixStack matrices, Rectangle bounds, int mouseX, int mouseY, float delta) {
         optimisedRenderStart(matrices, delta);
-        optimisedRenderBase(matrices, bounds, mouseX, mouseY, delta);
+        VertexConsumerProvider.Immediate immediate = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers();
+        optimisedRenderBase(matrices, immediate, bounds, mouseX, mouseY, delta);
+        immediate.draw();
         optimisedRenderOverlay(matrices, bounds, mouseX, mouseY, delta);
         optimisedRenderEnd(matrices, delta);
     }
     
-    @SuppressWarnings("deprecation")
     @Override
     public void optimisedRenderStart(MatrixStack matrices, float delta) {
-        MinecraftClient.getInstance().getTextureManager().bindTexture(SpriteAtlasTexture.BLOCK_ATLAS_TEX);
-        GlStateManager.enableRescaleNormal();
+        optimisedRenderStart(matrices, delta, true);
     }
     
     @SuppressWarnings("deprecation")
+    public void optimisedRenderStart(MatrixStack matrices, float delta, boolean isOptimised) {
+        MinecraftClient.getInstance().getTextureManager().bindTexture(SpriteAtlasTexture.BLOCK_ATLAS_TEX);
+        MinecraftClient.getInstance().getTextureManager().getTexture(SpriteAtlasTexture.BLOCK_ATLAS_TEX).setFilter(false, false);
+        RenderSystem.pushMatrix();
+        RenderSystem.enableRescaleNormal();
+        RenderSystem.enableAlphaTest();
+        RenderSystem.defaultAlphaFunc();
+        RenderSystem.enableBlend();
+        RenderSystem.blendFunc(GlStateManager.SrcFactor.SRC_ALPHA, GlStateManager.DstFactor.ONE_MINUS_SRC_ALPHA);
+        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        if (isOptimised) {
+            boolean sideLit = getModelFromStack(itemStack).isSideLit();
+            if (!sideLit)
+                DiffuseLighting.disableGuiDepthLighting();
+        }
+    }
+    
     @Override
     public void optimisedRenderEnd(MatrixStack matrices, float delta) {
-        GlStateManager.disableRescaleNormal();
+        optimisedRenderEnd(matrices, delta, true);
+    }
+    
+    @SuppressWarnings("deprecation")
+    public void optimisedRenderEnd(MatrixStack matrices, float delta, boolean isOptimised) {
+        RenderSystem.enableDepthTest();
+        RenderSystem.disableAlphaTest();
+        RenderSystem.disableRescaleNormal();
+        if (isOptimised) {
+            boolean sideLit = getModelFromStack(itemStack).isSideLit();
+            if (!sideLit)
+                DiffuseLighting.enableGuiDepthLighting();
+        }
+        RenderSystem.popMatrix();
     }
     
     private BakedModel getModelFromStack(ItemStack stack) {
@@ -316,22 +346,19 @@ public class ItemEntryStack extends AbstractEntryStack implements OptimalEntrySt
     }
     
     @Override
-    public void optimisedRenderBase(MatrixStack matrices, Rectangle bounds, int mouseX, int mouseY, float delta) {
+    public int groupingHash() {
+        return 1738923 + (getModelFromStack(itemStack).isSideLit() ? 1 : 0);
+    }
+    
+    @Override
+    public void optimisedRenderBase(MatrixStack matrices, VertexConsumerProvider.Immediate immediate, Rectangle bounds, int mouseX, int mouseY, float delta) {
         if (!isEmpty() && get(Settings.RENDER).get()) {
             ItemStack stack = getItemStack();
             ((ItemStackHook) (Object) stack).rei_setRenderEnchantmentGlint(get(Settings.Item.RENDER_ENCHANTMENT_GLINT).get());
             matrices.push();
             matrices.translate(bounds.getCenterX(), bounds.getCenterY(), 100.0F + getZ());
             matrices.scale(bounds.getWidth(), (bounds.getWidth() + bounds.getHeight()) / -2f, bounds.getHeight());
-            VertexConsumerProvider.Immediate immediate = MinecraftClient.getInstance().getBufferBuilders().getEntityVertexConsumers();
-            BakedModel model = getModelFromStack(stack);
-            boolean sideLit = !model.isSideLit();
-            if (sideLit)
-                DiffuseLighting.disableGuiDepthLighting();
-            MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Mode.GUI, false, matrices, immediate, 15728880, OverlayTexture.DEFAULT_UV, model);
-            immediate.draw();
-            if (sideLit)
-                DiffuseLighting.enableGuiDepthLighting();
+            MinecraftClient.getInstance().getItemRenderer().renderItem(stack, ModelTransformation.Mode.GUI, false, matrices, immediate, 15728880, OverlayTexture.DEFAULT_UV, getModelFromStack(stack));
             matrices.pop();
             ((ItemStackHook) (Object) stack).rei_setRenderEnchantmentGlint(false);
         }

+ 53 - 13
src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java

@@ -36,13 +36,17 @@ import me.shedaniel.rei.impl.subsets.SubsetsRegistryImpl;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
+import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.ContainerScreen;
 import net.minecraft.recipe.Recipe;
 import net.minecraft.recipe.RecipeManager;
 import net.minecraft.util.ActionResult;
 import net.minecraft.util.Identifier;
+import net.minecraft.util.TypedActionResult;
 import net.minecraft.util.Util;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 import java.util.*;
 import java.util.function.Consumer;
@@ -54,26 +58,23 @@ import java.util.stream.Collectors;
 @Environment(EnvType.CLIENT)
 public class RecipeHelperImpl implements RecipeHelper {
     
-    private static final Comparator<DisplayVisibilityHandler> VISIBILITY_HANDLER_COMPARATOR;
+    private static final Comparator<FocusedStackProvider> FOCUSED_STACK_PROVIDER_COMPARATOR = Comparator.comparingDouble(FocusedStackProvider::getPriority).reversed();
+    private static final Comparator<DisplayVisibilityHandler> VISIBILITY_HANDLER_COMPARATOR = Comparator.comparingDouble(DisplayVisibilityHandler::getPriority).reversed();
     @SuppressWarnings("rawtypes")
     private static final Comparator<Recipe> RECIPE_COMPARATOR = Comparator.comparing((Recipe o) -> o.getId().getNamespace()).thenComparing(o -> o.getId().getPath());
     
-    static {
-        Comparator<DisplayVisibilityHandler> comparator = Comparator.comparingDouble(DisplayVisibilityHandler::getPriority);
-        VISIBILITY_HANDLER_COMPARATOR = comparator.reversed();
-    }
-    
-    private final List<AutoTransferHandler> autoTransferHandlers = Lists.newLinkedList();
-    private final List<RecipeFunction> recipeFunctions = Lists.newLinkedList();
-    private final List<ScreenClickArea> screenClickAreas = Lists.newLinkedList();
+    private final List<FocusedStackProvider> focusedStackProviders = Lists.newArrayList();
+    private final List<AutoTransferHandler> autoTransferHandlers = Lists.newArrayList();
+    private final List<RecipeFunction> recipeFunctions = Lists.newArrayList();
+    private final List<ScreenClickArea> screenClickAreas = Lists.newArrayList();
     private final int[] recipeCount = {0};
     private final Map<Identifier, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
     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();
-    private final List<LiveRecipeGenerator<RecipeDisplay>> liveRecipeGenerators = Lists.newLinkedList();
+    private final Map<Identifier, ButtonAreaSupplier> autoCraftAreaSupplierMap = Maps.newHashMap();
+    private final Map<Identifier, List<List<EntryStack>>> categoryWorkingStations = Maps.newHashMap();
+    private final List<DisplayVisibilityHandler> displayVisibilityHandlers = Lists.newArrayList();
+    private final List<LiveRecipeGenerator<RecipeDisplay>> liveRecipeGenerators = Lists.newArrayList();
     private RecipeManager recipeManager;
     private boolean arePluginsLoading = false;
     
@@ -362,6 +363,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         this.displayVisibilityHandlers.clear();
         this.liveRecipeGenerators.clear();
         this.autoTransferHandlers.clear();
+        this.focusedStackProviders.clear();
         ((SubsetsRegistryImpl) SubsetsRegistry.INSTANCE).reset();
         ((FluidSupportProviderImpl) FluidSupportProvider.INSTANCE).reset();
         ((DisplayHelperImpl) DisplayHelper.getInstance()).resetData();
@@ -424,6 +426,23 @@ public class RecipeHelperImpl implements RecipeHelper {
                     return -1f;
                 }
             });
+        registerFocusedStackProvider(new FocusedStackProvider() {
+            @Override
+            @NotNull
+            public TypedActionResult<EntryStack> provide(Screen screen) {
+                if (screen instanceof ContainerScreen) {
+                    ContainerScreen<?> containerScreen = (ContainerScreen<?>) screen;
+                    if (containerScreen.focusedSlot != null && !containerScreen.focusedSlot.getStack().isEmpty())
+                        return TypedActionResult.success(EntryStack.create(containerScreen.focusedSlot.getStack()));
+                }
+                return TypedActionResult.pass(EntryStack.empty());
+            }
+            
+            @Override
+            public double getPriority() {
+                return -1.0;
+            }
+        });
         DisplayHelper.getInstance().registerHandler(new OverlayDecider() {
             @Override
             public boolean isHandingScreen(Class<?> screen) {
@@ -478,6 +497,27 @@ public class RecipeHelperImpl implements RecipeHelper {
         return handler;
     }
     
+    @Override
+    public void registerFocusedStackProvider(FocusedStackProvider provider) {
+        focusedStackProviders.add(provider);
+        focusedStackProviders.sort(FOCUSED_STACK_PROVIDER_COMPARATOR);
+    }
+    
+    @Override
+    @Nullable
+    public EntryStack getScreenFocusedStack(Screen screen) {
+        for (FocusedStackProvider provider : focusedStackProviders) {
+            TypedActionResult<EntryStack> result = Objects.requireNonNull(provider.provide(screen));
+            if (result.getResult() == ActionResult.SUCCESS) {
+                if (!result.getValue().isEmpty())
+                    return result.getValue();
+                return null;
+            } else if (result.getResult() == ActionResult.FAIL)
+                return null;
+        }
+        return null;
+    }
+    
     @Override
     public List<AutoTransferHandler> getSortedAutoCraftingHandler() {
         return autoTransferHandlers.stream().sorted(Comparator.comparingDouble(AutoTransferHandler::getPriority).reversed()).collect(Collectors.toList());