diff --git a/src/main/java/codechicken/nei/recipe/DefaultOverlayHandler.java b/src/main/java/codechicken/nei/recipe/DefaultOverlayHandler.java index f1e3d664..8198111b 100644 --- a/src/main/java/codechicken/nei/recipe/DefaultOverlayHandler.java +++ b/src/main/java/codechicken/nei/recipe/DefaultOverlayHandler.java @@ -197,7 +197,7 @@ public class DefaultOverlayHandler implements IOverlayHandler { return ingredSlots; } - private void moveIngredients(GuiContainer gui, List assignedIngredients, int multiplier) { + protected void moveIngredients(GuiContainer gui, List assignedIngredients, int multiplier) { final Slot craftingSlot = getCraftingResultSlot(gui); for (Slot slot : gui.inventorySlots.inventorySlots) { @@ -249,7 +249,7 @@ public class DefaultOverlayHandler implements IOverlayHandler { } } - private int calculateRecipeQuantity(List assignedIngredients) { + protected int calculateRecipeQuantity(List assignedIngredients) { int quantity = Integer.MAX_VALUE; for (IngredientDistribution distrib : assignedIngredients) { @@ -275,7 +275,7 @@ public class DefaultOverlayHandler implements IOverlayHandler { return quantity; } - private Slot[][] assignIngredSlots(GuiContainer gui, List ingredients, + protected Slot[][] assignIngredSlots(GuiContainer gui, List ingredients, List assignedIngredients) { Slot[][] recipeSlots = mapIngredSlots(gui, ingredients); // setup the slot map @@ -320,7 +320,7 @@ public class DefaultOverlayHandler implements IOverlayHandler { return recipeSlots; } - private List assignIngredients(List ingredients, + protected List assignIngredients(List ingredients, List ingredStacks) { ArrayList assignedIngredients = new ArrayList<>(); for (PositionedStack posstack : ingredients) // assign what we need and have @@ -358,7 +358,7 @@ public class DefaultOverlayHandler implements IOverlayHandler { return assignedIngredients; } - private void findInventoryQuantities(GuiContainer gui, List ingredStacks) { + protected void findInventoryQuantities(GuiContainer gui, List ingredStacks) { for (Slot slot : gui.inventorySlots.inventorySlots) /* work out how much we have to go round */ { if (slot.getHasStack() && canMoveFrom(slot, gui)) { final ItemStack pstack = slot.getStack(); @@ -390,7 +390,7 @@ public class DefaultOverlayHandler implements IOverlayHandler { } } - private List getPermutationIngredients(List ingredients) { + protected List getPermutationIngredients(List ingredients) { ArrayList ingredStacks = new ArrayList<>(); for (PositionedStack posstack : ingredients) /* work out what we need */ { for (ItemStack pstack : posstack.items) { diff --git a/src/main/java/codechicken/nei/recipe/RecipeHandlerRef.java b/src/main/java/codechicken/nei/recipe/RecipeHandlerRef.java index 456821d9..d384d8a5 100644 --- a/src/main/java/codechicken/nei/recipe/RecipeHandlerRef.java +++ b/src/main/java/codechicken/nei/recipe/RecipeHandlerRef.java @@ -77,9 +77,24 @@ public class RecipeHandlerRef { return this.handler.getOverlayHandler(firstGui, this.recipeIndex); } + if (firstGui != null && canUseSmartOverlay(firstGui)) { + return SmartOverlayHandler.INSTANCE; + } + return null; } + private boolean canUseSmartOverlay(GuiContainer gui) { + if (!(this.handler instanceof TemplateRecipeHandler templateHandler) + || templateHandler.getOverlayIdentifier() == null + || templateHandler.getGuiClass() == null + || !templateHandler.getGuiClass().isAssignableFrom(gui.getClass())) { + return false; + } + + return SmartOverlayHandler.canHandle(gui, this.handler, this.recipeIndex); + } + public boolean canFillCraftingGrid(GuiContainer gui) { final IOverlayHandler overlayHandler = getOverlayHandler(gui); diff --git a/src/main/java/codechicken/nei/recipe/SmartOverlayHandler.java b/src/main/java/codechicken/nei/recipe/SmartOverlayHandler.java new file mode 100644 index 00000000..d3985641 --- /dev/null +++ b/src/main/java/codechicken/nei/recipe/SmartOverlayHandler.java @@ -0,0 +1,232 @@ +package codechicken.nei.recipe; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.entity.player.InventoryPlayer; +import net.minecraft.inventory.Slot; +import net.minecraft.inventory.SlotCrafting; +import net.minecraft.item.ItemStack; + +import codechicken.nei.FastTransferManager; +import codechicken.nei.PositionedStack; + +public class SmartOverlayHandler extends DefaultOverlayHandler { + + public static final SmartOverlayHandler INSTANCE = new SmartOverlayHandler(); + + private static final int MAX_RECIPE_SLOTS = 12; + private static final int MAX_COST_DELTA = 16 * 16; + + private SmartOverlayHandler() {} + + public static boolean canHandle(GuiContainer gui, IRecipeHandler handler, int recipeIndex) { + return INSTANCE.resolveSlots(gui, handler.getIngredientStacks(recipeIndex)) != null; + } + + @Override + public int transferRecipe(GuiContainer gui, IRecipeHandler handler, int recipeIndex, int multiplier) { + final List ingredients = handler.getIngredientStacks(recipeIndex); + final List ingredStacks = getPermutationIngredients(ingredients); + final Slot[][] recipeSlots = resolveSlots(gui, ingredients); + + if (recipeSlots == null || !clearMappedSlots(gui, recipeSlots)) return 0; + + findInventoryQuantities(gui, ingredStacks); + + final List assignedIngredients = assignIngredients(ingredients, ingredStacks); + if (assignedIngredients == null) return 0; + + assignIngredSlots(gui, ingredients, assignedIngredients); + multiplier = Math.min(multiplier == 0 ? 64 : multiplier, calculateRecipeQuantity(assignedIngredients)); + + moveIngredients(gui, assignedIngredients, Math.max(1, multiplier)); + + return assignedIngredients.stream().anyMatch(distrib -> distrib.distrib.distributed == 0) ? 0 : multiplier; + } + + @Override + public boolean canFillCraftingGrid(GuiContainer firstGui, IRecipeHandler handler, int recipeIndex) { + return canHandle(firstGui, handler, recipeIndex); + } + + @Override + protected Set getCraftMatrixSlots(GuiContainer gui, IRecipeHandler handler) { + return Collections.emptySet(); + } + + @Override + public Slot[][] mapIngredSlots(GuiContainer gui, List ingredients) { + Slot[][] exactSlots = super.mapIngredSlots(gui, ingredients); + if (!hasEmptyMappings(exactSlots)) { + return exactSlots; + } + + Slot[][] smartSlots = resolveSlots(gui, ingredients); + return smartSlots == null ? exactSlots : smartSlots; + } + + private boolean clearMappedSlots(GuiContainer gui, Slot[][] recipeSlots) { + Set cleared = new HashSet<>(); + + for (Slot[] slots : recipeSlots) { + for (Slot slot : slots) { + if (cleared.add(slot) && slot.getHasStack() && slot.canTakeStack(gui.mc.thePlayer)) { + FastTransferManager.clickSlot(gui, slot.slotNumber, 0, 1); + if (slot.getHasStack()) return false; + } + } + } + + FastTransferManager.dropHeldItem(gui); + return gui.mc.thePlayer.inventory.getItemStack() == null; + } + + private Slot[][] resolveSlots(GuiContainer gui, List ingredients) { + if (ingredients == null || ingredients.isEmpty() || ingredients.size() > MAX_RECIPE_SLOTS) { + return null; + } + + List candidates = new ArrayList<>(); + for (Slot slot : gui.inventorySlots.inventorySlots) { + if (isDestinationCandidate(slot)) { + candidates.add(slot); + } + } + + if (candidates.size() < ingredients.size()) { + return null; + } + + Assignment best = new Assignment(); + Assignment secondBest = new Assignment(); + searchAssignments( + ingredients, + candidates, + new boolean[candidates.size()], + new Slot[ingredients.size()], + 0, + 0, + best, + secondBest); + + if (best.slots == null || secondBest.cost - best.cost <= MAX_COST_DELTA) { + return null; + } + + Slot[][] resolved = new Slot[ingredients.size()][]; + for (int i = 0; i < ingredients.size(); i++) { + resolved[i] = new Slot[] { best.slots[i] }; + } + return resolved; + } + + private void searchAssignments(List ingredients, List candidates, boolean[] used, + Slot[] assigned, int ingredientIndex, int cost, Assignment best, Assignment secondBest) { + if (cost >= secondBest.cost) { + return; + } + + if (ingredientIndex == ingredients.size()) { + if (cost < best.cost) { + secondBest.copyFrom(best); + best.set(cost, assigned); + } else if (cost < secondBest.cost) { + secondBest.set(cost, assigned); + } + return; + } + + PositionedStack ingredient = ingredients.get(ingredientIndex); + for (int i = 0; i < candidates.size(); i++) { + if (used[i]) continue; + + Slot slot = candidates.get(i); + int slotCost = getSlotCost(ingredient, slot); + if (slotCost < 0) continue; + + used[i] = true; + assigned[ingredientIndex] = slot; + searchAssignments( + ingredients, + candidates, + used, + assigned, + ingredientIndex + 1, + cost + slotCost, + best, + secondBest); + assigned[ingredientIndex] = null; + used[i] = false; + } + } + + private int getSlotCost(PositionedStack ingredient, Slot slot) { + boolean acceptsAnyPermutation = false; + for (ItemStack permutation : ingredient.items) { + if (canUseSlot(slot, permutation)) { + acceptsAnyPermutation = true; + break; + } + } + + if (!acceptsAnyPermutation) { + return -1; + } + + int dx = slot.xDisplayPosition - (ingredient.relx + offsetx); + int dy = slot.yDisplayPosition - (ingredient.rely + offsety); + int cost = dx * dx + dy * dy; + + if (slot.getHasStack()) { + cost += 10_000; + } + + return cost; + } + + private boolean canUseSlot(Slot slot, ItemStack stack) { + if (stack == null || stack.stackSize <= 0 || !slot.isItemValid(stack)) { + return false; + } + + if (!slot.inventory.isItemValidForSlot(slot.getSlotIndex(), stack)) { + return false; + } + + return !slot.getHasStack() || canStack(slot.getStack(), stack); + } + + private boolean isDestinationCandidate(Slot slot) { + return slot != null && !(slot.inventory instanceof InventoryPlayer) && !(slot instanceof SlotCrafting); + } + + private boolean hasEmptyMappings(Slot[][] recipeSlots) { + for (Slot[] recipeSlot : recipeSlots) { + if (recipeSlot.length == 0) { + return true; + } + } + return false; + } + + private static class Assignment { + + int cost = Integer.MAX_VALUE; + Slot[] slots; + + void set(int cost, Slot[] slots) { + this.cost = cost; + this.slots = slots.clone(); + } + + void copyFrom(Assignment other) { + this.cost = other.cost; + this.slots = other.slots == null ? null : other.slots.clone(); + } + } +}