diff --git a/src/main/java/codechicken/nei/FavoriteRecipes.java b/src/main/java/codechicken/nei/FavoriteRecipes.java index 66dbe105..7749d7d1 100644 --- a/src/main/java/codechicken/nei/FavoriteRecipes.java +++ b/src/main/java/codechicken/nei/FavoriteRecipes.java @@ -316,6 +316,10 @@ public class FavoriteRecipes { load(); } + public static boolean hasManual() { + return manualFavorites.size() > 0; + } + public static boolean containsManual(ItemStack stack) { return getManualFavorite(stack) != null; } diff --git a/src/main/java/codechicken/nei/PositionedStack.java b/src/main/java/codechicken/nei/PositionedStack.java index 50ba1aff..f72c9766 100644 --- a/src/main/java/codechicken/nei/PositionedStack.java +++ b/src/main/java/codechicken/nei/PositionedStack.java @@ -163,7 +163,7 @@ public class PositionedStack implements Cloneable { } public List getFilteredPermutations(ItemFilter additionalFilter) { - List items = Arrays.asList(this.items); + List items = new ArrayList<>(Arrays.asList(this.items)); items = filteringPermutations(items, item -> !ItemInfo.isHidden(item)); items = filteringPermutations(items, PresetsList.getItemFilter()); diff --git a/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java b/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java index 34b0d17e..b9d2b20e 100644 --- a/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java +++ b/src/main/java/codechicken/nei/recipe/GuiCraftingRecipe.java @@ -4,6 +4,7 @@ import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.function.UnaryOperator; import java.util.stream.Collectors; @@ -35,6 +36,7 @@ public class GuiCraftingRecipe extends GuiRecipe { public static GuiRecipe createRecipeGui(String outputId, boolean open, Object... results) { final Minecraft mc = NEIClientUtils.mc(); final RecipeId recipeId; + List pinnedIngredients = null; for (int i = 0; i < results.length; i++) { if (results[i] instanceof ItemStack stack) { @@ -46,6 +48,12 @@ public class GuiCraftingRecipe extends GuiRecipe { recipeId = getRecipeId(mc.currentScreen, (ItemStack) results[0]); } else if ("recipeId".equals(outputId)) { recipeId = (RecipeId) results[1]; + + // optional third argument: ingredients to pin the found recipe with + if (results.length > 2 && results[2] instanceof Listlist) { + pinnedIngredients = extractIngredients(list); + } + } else { recipeId = getCurrentRecipeId(mc.currentScreen); } @@ -65,13 +73,25 @@ public class GuiCraftingRecipe extends GuiRecipe { mc.displayGuiScreen(gui); } - gui.openTargetRecipe(recipeId); + gui.openTargetRecipe(recipeId, pinnedIngredients); return gui; } return null; } + private static List extractIngredients(List list) { + final List ingredients = new ArrayList<>(list.size()); + + for (Object item : list) { + if (item instanceof ItemStack stack) { + ingredients.add(stack); + } + } + + return ingredients; + } + public static ArrayList getCraftingHandlers(String outputId, Object... results) { ArrayList craftinghandlers = GuiCraftingRecipe.craftinghandlers; ArrayList serialCraftingHandlers = GuiCraftingRecipe.serialCraftingHandlers; diff --git a/src/main/java/codechicken/nei/recipe/GuiRecipe.java b/src/main/java/codechicken/nei/recipe/GuiRecipe.java index 2740664b..4ecea6b4 100644 --- a/src/main/java/codechicken/nei/recipe/GuiRecipe.java +++ b/src/main/java/codechicken/nei/recipe/GuiRecipe.java @@ -524,6 +524,13 @@ public abstract class GuiRecipe extends GuiContainer i } public int openTargetRecipe(RecipeId recipeId) { + return openTargetRecipe(recipeId, null); + } + + /** + * @param pinnedIngredients when not null, the found recipe is paused with these ingredients shown + */ + public int openTargetRecipe(RecipeId recipeId, List pinnedIngredients) { int recipeIndex = -1; int recipetype = 0; @@ -551,6 +558,10 @@ public abstract class GuiRecipe extends GuiContainer i setRecipePage(recipetype); this.handlerPages.gotoRecipeIndex(Math.max(0, recipeIndex)); + if (pinnedIngredients != null && recipeIndex >= 0) { + this.handlerPages.pinRecipe(recipeIndex, pinnedIngredients); + } + return recipeIndex; } diff --git a/src/main/java/codechicken/nei/recipe/RecipePageManager.java b/src/main/java/codechicken/nei/recipe/RecipePageManager.java index 654a4d41..b386ccd4 100644 --- a/src/main/java/codechicken/nei/recipe/RecipePageManager.java +++ b/src/main/java/codechicken/nei/recipe/RecipePageManager.java @@ -6,6 +6,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import net.minecraft.item.ItemStack; + import codechicken.nei.NEIClientConfig; import codechicken.nei.Widget; import codechicken.nei.recipe.widget.RecipeWidget; @@ -42,6 +44,8 @@ public class RecipePageManager { } this.cacheKey = cacheKey; + + final List pausedWidgets = getPausedWidgets(); this.pageWidgets.clear(); final boolean allowOverflowY = this.handlerInfo.isAllowOverflowY(); @@ -76,6 +80,15 @@ public class RecipePageManager { for (int groupIndex = 0; groupIndex < groupedRecipes.size(); groupIndex++) { final RecipeWidget widget = new RecipeWidget(this.handler.original, groupedRecipes.get(groupIndex)); + // keep the pause of the rebuilt widgets + for (RecipeWidget paused : pausedWidgets) { + final int pausedIndex = paused.getRecipeHandlerRef().recipeIndex; + + if (widget.containsRecipeIndex(pausedIndex)) { + widget.pinRecipe(pausedIndex, paused.getPinnedIngredients()); + } + } + widget.w = width; if (allowOverflowY) { @@ -144,6 +157,33 @@ public class RecipePageManager { return false; } + private List getPausedWidgets() { + final List paused = new ArrayList<>(); + + for (List widgets : this.pageWidgets) { + for (Widget widget : widgets) { + if (widget instanceof RecipeWidget recipeWidget && recipeWidget.isPaused()) { + paused.add(recipeWidget); + } + } + } + + return paused; + } + + public boolean pinRecipe(int recipeIndex, List ingredients) { + + for (List widgets : this.pageWidgets) { + for (Widget widget : widgets) { + if (widget instanceof RecipeWidget recipeWidget && recipeWidget.containsRecipeIndex(recipeIndex)) { + return recipeWidget.pinRecipe(recipeIndex, ingredients); + } + } + } + + return false; + } + public List getCurrentPageWidgets() { if (this.pageWidgets.isEmpty()) { return Collections.emptyList(); diff --git a/src/main/java/codechicken/nei/recipe/widget/PermutationsCycler.java b/src/main/java/codechicken/nei/recipe/widget/PermutationsCycler.java index cfc3eed1..723853c8 100644 --- a/src/main/java/codechicken/nei/recipe/widget/PermutationsCycler.java +++ b/src/main/java/codechicken/nei/recipe/widget/PermutationsCycler.java @@ -1,6 +1,7 @@ package codechicken.nei.recipe.widget; import java.util.ArrayList; +import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -50,6 +51,11 @@ public class PermutationsCycler { return false; } + public void invalidate() { + this.favoriteRevision = -1; + validate(); + } + public boolean contains(K key) { validate(); return this.permutations.containsKey(key); @@ -79,6 +85,11 @@ public class PermutationsCycler { return items; } + public int getFavoriteCount(K key) { + get(key); + return this.favoriteCounts.getOrDefault(key, 0); + } + public ItemStack getCycled(K key, int cycle) { final List items = get(key); final int favoriteCount = this.favoriteCounts.getOrDefault(key, 0); @@ -96,4 +107,10 @@ public class PermutationsCycler { return -1; } + // Hash key with the same equality as NEIServerUtils.areStacksSameType + public static Object getStackKey(ItemStack stack) { + return Arrays + .asList(stack.getItem(), stack.getHasSubtypes() ? stack.getItemDamage() : 0, stack.stackTagCompound); + } + } diff --git a/src/main/java/codechicken/nei/recipe/widget/RecipeGroup.java b/src/main/java/codechicken/nei/recipe/widget/RecipeGroup.java index 010a5298..1a7a5738 100644 --- a/src/main/java/codechicken/nei/recipe/widget/RecipeGroup.java +++ b/src/main/java/codechicken/nei/recipe/widget/RecipeGroup.java @@ -2,23 +2,109 @@ package codechicken.nei.recipe.widget; import java.awt.Point; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Set; import net.minecraft.item.ItemStack; import codechicken.nei.FavoriteRecipes; +import codechicken.nei.NEIServerUtils; import codechicken.nei.PositionedStack; import codechicken.nei.recipe.GuiRecipeTab; import codechicken.nei.recipe.HandlerInfo; import codechicken.nei.recipe.IRecipeHandler; +import codechicken.nei.recipe.StackInfo; public class RecipeGroup { + // position and items of a stack at load time, the handler may change the stack later + protected static class StackState { + + public final int relx; + public final int rely; + public final ItemStack[] items; + + public StackState(PositionedStack pStack) { + this.relx = pStack.relx; + this.rely = pStack.rely; + this.items = pStack.items; + } + } + + // filtered permutations of a slot, favorites first, with their indices in PositionedStack.items + protected static class FilteredSlot { + + public final List items; + public final int[] indices; + public final int favoriteCount; + + public FilteredSlot(PositionedStack pStack) { + this.items = pStack.getFilteredPermutations(); + this.indices = new int[this.items.size()]; + + // filtering keeps the original instances, so an identity lookup finds the source index + for (int i = 0; i < this.indices.length; i++) { + this.indices[i] = indexOfInstance(pStack.items, this.items.get(i)); + } + + int favoriteCount = 0; + + while (this.items.size() > 1 && favoriteCount < this.items.size() + && FavoriteRecipes.containsManual(this.items.get(favoriteCount))) { + favoriteCount++; + } + + this.favoriteCount = favoriteCount; + } + + private static int indexOfInstance(ItemStack[] items, ItemStack stack) { + for (int i = 0; i < items.length; i++) { + if (items[i] == stack) { + return i; + } + } + return 0; + } + } + + protected static class MemberStacks { + + public final List inputs; + public final List outputs; + public final List auxiliaries; + public final List cycling; + public final List states = new ArrayList<>(); + public final Map slots = new HashMap<>(); + public final Map filtered = new HashMap<>(); + + public MemberStacks(List inputs, List outputs, + List auxiliaries) { + this.inputs = inputs; + this.outputs = outputs; + this.auxiliaries = auxiliaries; + + if (auxiliaries.isEmpty()) { + this.cycling = inputs; + } else { + this.cycling = new ArrayList<>(inputs); + this.cycling.addAll(auxiliaries); + } + + for (PositionedStack pStack : this.cycling) { + this.slots.putIfAbsent(new Point(pStack.relx, pStack.rely), pStack); + } + + for (PositionedStack pStack : inputs) this.states.add(new StackState(pStack)); + for (PositionedStack pStack : outputs) this.states.add(new StackState(pStack)); + for (PositionedStack pStack : auxiliaries) this.states.add(new StackState(pStack)); + } + } + private static final List SINGLE_MEMBER = Collections.singletonList(0); private final IRecipeHandler handler; @@ -26,16 +112,33 @@ public class RecipeGroup { private final HandlerInfo handlerInfo; private final PermutationsCycler slotPermutations; + private final MemberStacks[] memberStacks; + private Map slotStacks = null; + private final Set sharedSlots = new HashSet<>(); private List allowedMembers = null; + private Boolean cycling = null; public RecipeGroup(IRecipeHandler handler, List recipeIndices) { this.handler = handler; this.recipeIndices = new ArrayList<>(recipeIndices); this.handlerInfo = GuiRecipeTab.getHandlerInfo(handler); + this.memberStacks = new MemberStacks[this.recipeIndices.size()]; this.slotPermutations = new PermutationsCycler( new HashMap<>(), new HashMap<>(), - this::collectSlotPermutations).onInvalidate(() -> this.allowedMembers = null); + this::collectSlotPermutations).onInvalidate(() -> { + this.allowedMembers = null; + this.cycling = null; + this.slotStacks = null; + + for (MemberStacks stacks : this.memberStacks) { + if (stacks != null) { + stacks.filtered.clear(); + } + } + + this.sharedSlots.clear(); + }); } public int size() { @@ -58,34 +161,49 @@ public class RecipeGroup { return this.recipeIndices.contains(recipeIndex); } + public int indexOf(int recipeIndex) { + return this.recipeIndices.indexOf(recipeIndex); + } + public List getPermutations(Point slotKey) { return this.slotPermutations.get(slotKey); } + public boolean hasPermutation(int member, Point slotKey, ItemStack stack) { + this.slotPermutations.validate(); + + if (stack == null) { + return false; + } + + final List permutations = getFilteredPermutations(member, slotKey); + return permutations != null && PermutationsCycler.indexOf(permutations, stack) != -1; + } + + // the given member if it has the item in the slot, otherwise the next member that has it + public int findMember(Point slotKey, ItemStack stack, int from) { + + for (int step = 0; step < size(); step++) { + final int member = (from + step) % size(); + + if (hasPermutation(member, slotKey, stack)) { + return member; + } + } + + return -1; + } + public List getInputs(int member) { - return this.handler.getIngredientStacks(getRecipeIndex(member)); + return getStacks(member).inputs; } public List getOutputs(int member) { - final int recipeIndex = getRecipeIndex(member); - final PositionedStack pStackResult = this.handler.getResultStack(recipeIndex); - return pStackResult != null ? Arrays.asList(pStackResult) : this.handler.getOtherStacks(recipeIndex); - } - - public List getCatalysts(int member) { - final int recipeIndex = getRecipeIndex(member); - - if (this.handler.getResultStack(recipeIndex) == null) { - return Collections.emptyList(); - } - - return this.handler.getOtherStacks(recipeIndex); + return getStacks(member).outputs; } public List getCyclingStacks(int member) { - final List stacks = new ArrayList<>(getInputs(member)); - stacks.addAll(getCatalysts(member)); - return stacks; + return getStacks(member).cycling; } public Point getSlotKey(PositionedStack pStack) { @@ -93,39 +211,108 @@ public class RecipeGroup { } public PositionedStack getSlot(int member, Point slotKey) { - - for (PositionedStack pStack : getCyclingStacks(member)) { - if (pStack.relx == slotKey.x && pStack.rely == slotKey.y) { - return pStack; - } - } - - return null; + return getStacks(member).slots.get(slotKey); } - public PositionedStack getOutputSlot(int member, Point slotKey) { + protected MemberStacks getStacks(int member) { - for (PositionedStack pStack : getOutputs(member)) { - if (pStack.relx == slotKey.x && pStack.rely == slotKey.y) { - return pStack; - } + if (this.memberStacks[member] == null) { + this.memberStacks[member] = loadStacks(member); } - return null; + return this.memberStacks[member]; } - public List getOutputVariants(Point slotKey) { - final List items = new ArrayList<>(); + protected MemberStacks loadStacks(int member) { + final int recipeIndex = getRecipeIndex(member); + final List inputs = new ArrayList<>(this.handler.getIngredientStacks(recipeIndex)); + final PositionedStack pStackResult = this.handler.getResultStack(recipeIndex); + final List others = new ArrayList<>(this.handler.getOtherStacks(recipeIndex)); - for (int member = 0; member < size(); member++) { - final PositionedStack slot = getOutputSlot(member, slotKey); + if (pStackResult != null) { + return new MemberStacks(inputs, Collections.singletonList(pStackResult), others); + } - if (slot != null && PermutationsCycler.indexOf(items, slot.item) == -1) { - items.add(slot.item); + return new MemberStacks(inputs, others, Collections.emptyList()); + } + + // re-reads the member stacks from the handler, keeps the cached instances when nothing changed + public void refresh(int member) { + + if (this.memberStacks[member] == null) { + return; + } + + final MemberStacks stacks = loadStacks(member); + + if (!isSameStacks(this.memberStacks[member], stacks)) { + this.memberStacks[member] = stacks; + this.slotPermutations.invalidate(); + } + } + + // index into PositionedStack.items of the permutation to render, -1 when the slot is unknown + public int getCycledPermutationIndex(int member, Point slotKey, int index) { + this.slotPermutations.validate(); + + final FilteredSlot filtered = getFilteredSlot(member, slotKey); + + if (filtered == null || filtered.indices.length == 0) { + return -1; + } + + final int size = filtered.favoriteCount > 0 ? filtered.favoriteCount : filtered.indices.length; + return filtered.indices[index % size]; + } + + protected boolean isSameStacks(MemberStacks stacksA, MemberStacks stacksB) { + + if (stacksA.inputs.size() != stacksB.inputs.size() || stacksA.outputs.size() != stacksB.outputs.size() + || stacksA.auxiliaries.size() != stacksB.auxiliaries.size()) { + return false; + } + + for (int i = 0; i < stacksA.states.size(); i++) { + final StackState stateA = stacksA.states.get(i); + final StackState stateB = stacksB.states.get(i); + + if (stateA.relx != stateB.relx || stateA.rely != stateB.rely || !isSameItems(stateA.items, stateB.items)) { + return false; } } - return items; + return true; + } + + protected boolean isSameItems(ItemStack[] itemsA, ItemStack[] itemsB) { + + if (itemsA == itemsB) { + return true; + } + + if (itemsA.length != itemsB.length) { + return false; + } + + for (int i = 0; i < itemsA.length; i++) { + if (!isSameStack(itemsA[i], itemsB[i])) { + return false; + } + } + + return true; + } + + // true when the group has something to cycle: several members or a slot with several permutations + public boolean canCycle() { + this.slotPermutations.validate(); + + if (this.cycling == null) { + this.cycling = size() > 1 || getStacks(0).slots.keySet().stream() + .anyMatch(slotKey -> getFilteredPermutations(0, slotKey).size() > 1); + } + + return this.cycling; } public List getAllowedMembers() { @@ -135,17 +322,20 @@ public class RecipeGroup { return SINGLE_MEMBER; } - if (this.allowedMembers == null) { - final Set favoriteSlots = new HashSet<>(); + if (this.allowedMembers == null && !FavoriteRecipes.hasManual()) { + this.allowedMembers = new ArrayList<>(size()); for (int member = 0; member < size(); member++) { - for (PositionedStack pStack : getCyclingStacks(member)) { - final Point slotKey = getSlotKey(pStack); - final List items = getPermutations(slotKey); + this.allowedMembers.add(member); + } - if (items.size() > 1 && items.stream().anyMatch(FavoriteRecipes::containsManual)) { - favoriteSlots.add(slotKey); - } + } else if (this.allowedMembers == null) { + final Set favoriteSlots = new HashSet<>(); + + for (Point slotKey : getSlotStacks().keySet()) { + // a shared slot gives every member the same misses, so it can't affect the filter + if (!this.sharedSlots.contains(slotKey) && this.slotPermutations.getFavoriteCount(slotKey) > 0) { + favoriteSlots.add(slotKey); } } @@ -155,6 +345,72 @@ public class RecipeGroup { return this.allowedMembers; } + protected Map getSlotStacks() { + + if (this.slotStacks == null) { + this.slotStacks = new LinkedHashMap<>(); + + for (int member = 0; member < size(); member++) { + for (PositionedStack pStack : getCyclingStacks(member)) { + final PositionedStack[] stacks = this.slotStacks + .computeIfAbsent(getSlotKey(pStack), key -> new PositionedStack[size()]); + + if (stacks[member] == null) { + stacks[member] = pStack; + } + } + } + + for (Map.Entry entry : this.slotStacks.entrySet()) { + if (isSharedSlot(entry.getValue())) { + this.sharedSlots.add(entry.getKey()); + } + } + } + + return this.slotStacks; + } + + // all members hold the same permutations in this slot + protected boolean isSharedSlot(PositionedStack[] stacks) { + ItemStack[] first = null; + + for (PositionedStack pStack : stacks) { + if (pStack == null) { + continue; + } + + if (first == null) { + first = pStack.items; + } else if (!isSameItems(first, pStack.items)) { + return false; + } + } + + return true; + } + + protected boolean isSameStack(ItemStack stackA, ItemStack stackB) { + return stackA == stackB || NEIServerUtils.areStacksSameType(stackA, stackB) + && StackInfo.getAmount(stackA) == StackInfo.getAmount(stackB); + } + + protected List getFilteredPermutations(int member, Point slotKey) { + final FilteredSlot filtered = getFilteredSlot(member, slotKey); + return filtered != null ? filtered.items : null; + } + + protected FilteredSlot getFilteredSlot(int member, Point slotKey) { + final MemberStacks stacks = getStacks(member); + final PositionedStack slot = stacks.slots.get(slotKey); + + if (slot == null) { + return null; + } + + return stacks.filtered.computeIfAbsent(slotKey, key -> new FilteredSlot(slot)); + } + protected List filterMembers(Set favoriteSlots) { final List indices = new ArrayList<>(); int minMisses = Integer.MAX_VALUE; @@ -179,9 +435,9 @@ public class RecipeGroup { int misses = 0; for (Point slotKey : favoriteSlots) { - final PositionedStack slot = getSlot(member, slotKey); + final List permutations = getFilteredPermutations(member, slotKey); - if (slot != null && slot.getFilteredPermutations().stream().noneMatch(FavoriteRecipes::containsManual)) { + if (permutations != null && permutations.stream().noneMatch(FavoriteRecipes::containsManual)) { misses++; } } @@ -190,21 +446,31 @@ public class RecipeGroup { } protected List collectSlotPermutations(Point slotKey) { - final List items = new ArrayList<>(); + + if (!getSlotStacks().containsKey(slotKey)) { + return Collections.emptyList(); + } + + final Map items = new LinkedHashMap<>(); + final boolean shared = this.sharedSlots.contains(slotKey); for (int member = 0; member < size(); member++) { - final PositionedStack slot = getSlot(member, slotKey); + final List permutations = getFilteredPermutations(member, slotKey); - if (slot != null) { - for (ItemStack stack : slot.getFilteredPermutations()) { - if (PermutationsCycler.indexOf(items, stack) == -1) { - items.add(stack); + if (permutations != null) { + for (ItemStack stack : permutations) { + if (stack != null) { + items.putIfAbsent(PermutationsCycler.getStackKey(stack), stack); } } + + if (shared) { + break; + } } } - return items; + return new ArrayList<>(items.values()); } } diff --git a/src/main/java/codechicken/nei/recipe/widget/RecipeWidget.java b/src/main/java/codechicken/nei/recipe/widget/RecipeWidget.java index ab88a10f..6099e7b9 100644 --- a/src/main/java/codechicken/nei/recipe/widget/RecipeWidget.java +++ b/src/main/java/codechicken/nei/recipe/widget/RecipeWidget.java @@ -2,6 +2,7 @@ package codechicken.nei.recipe.widget; import java.awt.Point; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -21,7 +22,6 @@ import org.lwjgl.opengl.GL12; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.FavoriteRecipes; -import codechicken.nei.ItemsTooltipLineHandler; import codechicken.nei.KeyManager; import codechicken.nei.NEIClientConfig; import codechicken.nei.NEIClientUtils; @@ -29,6 +29,8 @@ import codechicken.nei.PositionedStack; import codechicken.nei.Widget; import codechicken.nei.api.IGuiContainerOverlay; import codechicken.nei.api.ShortcutInputHandler; +import codechicken.nei.drawable.DrawableBuilder; +import codechicken.nei.drawable.DrawableResource; import codechicken.nei.guihook.GuiContainerManager; import codechicken.nei.recipe.AcceptsFollowingTooltipLineHandler; import codechicken.nei.recipe.Badge; @@ -47,28 +49,35 @@ import codechicken.nei.util.NEIMouseUtils; public class RecipeWidget extends Widget { protected static final int TICKS_PER_CYCLE = 20; + protected static final DrawableResource PAUSE_MARKER = new DrawableBuilder( + "nei:textures/pinned_pause.png", + 0, + 0, + 8, + 8).setTextureSize(8, 8).build(); + protected static final int PAUSE_MARKER_OFFSET = 1; protected final RecipeGroup group; protected final IRecipeHandler handler; protected final HandlerInfo handlerInfo; - protected final PermutationsCycler renderPermutations; - protected final Map> badgeCache = new WeakHashMap<>(); protected final Map> buttonCache = new HashMap<>(); protected AcceptsFollowingTooltipLineHandler acceptsTooltip; - protected ItemsTooltipLineHandler variantsTooltip; - protected Point variantsSlotKey; protected int activeMember = 0; - protected int memberCursor = -1; - protected int slotCycle = 0; + // single state counter: member = allowed[cycle % size], permutation index = cycle / size + protected int cycle = 0; protected int cycleticks = 0; protected int lastcycle = -1; + protected int favoriteRevision = FavoriteRecipes.getRevision(); protected boolean showAsWidget = false; protected boolean update = true; + protected boolean paused = false; + protected List pinnedIngredients = null; + protected Map pinnedSlots = Collections.emptyMap(); public RecipeWidget(RecipeHandlerRef handlerRef) { this(handlerRef.handler, Collections.singletonList(handlerRef.recipeIndex)); @@ -82,13 +91,6 @@ public class RecipeWidget extends Widget { this.group = group; this.handler = group.getHandler(); this.handlerInfo = group.getHandlerInfo(); - this.renderPermutations = new PermutationsCycler( - new WeakHashMap<>(), - new WeakHashMap<>(), - PositionedStack::getFilteredPermutations).onInvalidate(() -> { - this.badgeCache.clear(); - this.acceptsTooltip = null; - }); update(); } @@ -117,6 +119,104 @@ public class RecipeWidget extends Widget { this.showAsWidget = show; } + public boolean isPaused() { + return this.paused; + } + + public List getPinnedIngredients() { + return this.pinnedIngredients; + } + + public void setPaused(boolean paused) { + final boolean wasPaused = this.paused; + this.paused = paused && this.group.canCycle(); + + if (!this.paused) { + this.pinnedIngredients = null; + this.pinnedSlots = Collections.emptyMap(); + + if (wasPaused) { + syncCycle(); + } + } + } + + /** + * Pauses the widget on the recipe. Every given ingredient needs an input slot of its own that accepts it, otherwise + * the recipe is not pinned. + */ + public boolean pinRecipe(int recipeIndex, List ingredients) { + final int member = this.group.indexOf(recipeIndex); + + if (member < 0 || !this.group.canCycle()) { + return false; + } + + final Map slots = ingredients != null + ? matchIngredients(this.group.getInputs(member), ingredients) + : Collections.emptyMap(); + + if (slots == null) { + return false; + } + + this.pinnedIngredients = ingredients; + this.pinnedSlots = slots; + this.paused = true; + setActiveMember(member); + updatePermutations(); + + return true; + } + + // assigns each ingredient to its own slot (bipartite matching), null when some ingredient has no slot + private Map matchIngredients(List stacks, List ingredients) { + final int[] slotOwners = new int[stacks.size()]; + Arrays.fill(slotOwners, -1); + + for (int ingredient = 0; ingredient < ingredients.size(); ingredient++) { + if (!assignIngredient(ingredient, stacks, ingredients, slotOwners, new boolean[stacks.size()])) { + return null; + } + } + + final Map slots = new HashMap<>(); + + for (int slot = 0; slot < slotOwners.length; slot++) { + if (slotOwners[slot] != -1) { + slots.put(this.group.getSlotKey(stacks.get(slot)), ingredients.get(slotOwners[slot])); + } + } + + return slots; + } + + private boolean assignIngredient(int ingredient, List stacks, List ingredients, + int[] slotOwners, boolean[] visited) { + + for (int slot = 0; slot < stacks.size(); slot++) { + if (!visited[slot] && stacks.get(slot).getPermutationIndex(ingredients.get(ingredient)) != -1) { + visited[slot] = true; + + if (slotOwners[slot] == -1 + || assignIngredient(slotOwners[slot], stacks, ingredients, slotOwners, visited)) { + slotOwners[slot] = ingredient; + return true; + } + } + } + + return false; + } + + private void applyPinnedIngredient(PositionedStack pStack) { + final ItemStack ingredient = this.pinnedSlots.get(this.group.getSlotKey(pStack)); + + if (ingredient != null) { + pStack.setPermutationToRender(ingredient); + } + } + @Override public void update() { @@ -142,11 +242,11 @@ public class RecipeWidget extends Widget { return this.buttonCache.computeIfAbsent(this.activeMember, this::createButtons); } - protected List getRecipeButtonsIfInit() { + private List getRecipeButtonsIfInit() { return this.buttonCache.getOrDefault(this.activeMember, Collections.emptyList()); } - protected List createButtons(int member) { + private List createButtons(int member) { if (this.group.getOutputs(member).isEmpty()) { return Collections.emptyList(); @@ -156,7 +256,7 @@ public class RecipeWidget extends Widget { .unmodifiableList(initButtons(RecipeHandlerRef.of(this.handler, this.group.getRecipeIndex(member)))); } - protected List initButtons(RecipeHandlerRef ref) { + private List initButtons(RecipeHandlerRef ref) { final GuiRecipe guiRecipe = getGuiRecipe(); if (guiRecipe != null) { @@ -186,7 +286,7 @@ public class RecipeWidget extends Widget { } - protected List getDefaultButtons(RecipeHandlerRef ref) { + private List getDefaultButtons(RecipeHandlerRef ref) { GuiContainer guiContainer = NEIClientUtils.getGuiContainer(); final List buttons = new ArrayList<>(); final boolean showFavorites = NEIClientConfig.favoritesEnabled() && handlerInfo.getShowFavoritesButton(); @@ -217,13 +317,17 @@ public class RecipeWidget extends Widget { if (this.update) { this.update = false; - if (tickCycle()) { - - if (this.showAsWidget || !contains(mouseX, mouseY)) { - nextMember(); - } - - updatePermutations(this.lastcycle); + if (this.lastcycle == -1) { + this.lastcycle = 0; + applyCycle(); + } else if (tickCycle() && !isCycleFrozen(mouseX, mouseY)) { + this.cycle++; + applyCycle(); + } else if (this.favoriteRevision != FavoriteRecipes.getRevision()) { + this.favoriteRevision = FavoriteRecipes.getRevision(); + this.badgeCache.clear(); + this.acceptsTooltip = null; + updatePermutations(); } for (GuiRecipeButton button : getRecipeButtons()) { @@ -244,21 +348,7 @@ public class RecipeWidget extends Widget { GuiContainerManager.enableMatrixStackLogging(); - for (PositionedStack pStack : getInputs()) { - - if (!this.renderPermutations.contains(pStack)) { - updatePermutationsFor(pStack); - } - - drawItem(pStack, mouseX, mouseY, yShift, true); - } - - for (PositionedStack pStack : getCatalysts()) { - - if (!this.renderPermutations.contains(pStack)) { - updatePermutationsFor(pStack); - } - + for (PositionedStack pStack : getCyclingStacks()) { drawItem(pStack, mouseX, mouseY, yShift, true); } @@ -283,6 +373,10 @@ public class RecipeWidget extends Widget { GL11.glTranslatef(-this.x, -this.y - yShift, 0); GL11.glPopAttrib(); + if (this.paused) { + NEIClientUtils.gl2DRenderContext(this::drawPauseMarker); + } + if (!this.showAsWidget) { final Minecraft mc = NEIClientUtils.mc(); for (GuiRecipeButton button : getRecipeButtons()) { @@ -297,7 +391,22 @@ public class RecipeWidget extends Widget { DebugHandlerWidget.instance.drawGuiPlaceholder(this); } - protected void drawItem(PositionedStack pStack, int mouseX, int mouseY, int yShift, boolean input) { + private void drawPauseMarker() { + GL11.glColor4f(1, 1, 1, 1); + PAUSE_MARKER.draw(this.x + PAUSE_MARKER_OFFSET, this.y + PAUSE_MARKER_OFFSET); + } + + private boolean isPauseMarkerMouseOver(int mouseX, int mouseY) { + final int left = this.x + PAUSE_MARKER_OFFSET; + final int top = this.y + PAUSE_MARKER_OFFSET; + + return this.paused && mouseX >= left + && mouseX < left + PAUSE_MARKER.getWidth() + && mouseY >= top + && mouseY < top + PAUSE_MARKER.getHeight(); + } + + private void drawItem(PositionedStack pStack, int mouseX, int mouseY, int yShift, boolean input) { pStack.draw(mouseX - this.x, mouseY - this.y - yShift); if (this.handlerInfo.getShowBadge()) { @@ -310,7 +419,7 @@ public class RecipeWidget extends Widget { } } - protected List getBadges(PositionedStack pStack, boolean input) { + private List getBadges(PositionedStack pStack, boolean input) { return this.badgeCache.computeIfAbsent(pStack, k -> { final List badges = k.getBadges(); @@ -339,7 +448,7 @@ public class RecipeWidget extends Widget { } - protected void drawBadge(PositionedStack pStack, boolean input) { + private void drawBadge(PositionedStack pStack, boolean input) { final List badges = getBadges(pStack, input); for (Badge badge : badges) { @@ -357,6 +466,7 @@ public class RecipeWidget extends Widget { if (hovered != null && getPermutations(hovered).size() > 1) { FavoriteRecipes.toggleFavoriteItem(hovered.item); + setPaused(false); return true; } @@ -383,6 +493,12 @@ public class RecipeWidget extends Widget { @Override public boolean handleClick(int mouseX, int mouseY, int button) { + if (button == 0 && isPauseMarkerMouseOver(mouseX, mouseY)) { + NEIClientUtils.playClickSound(); + setPaused(false); + return true; + } + if (ShortcutInputHandler.handleMouseClick(getStackMouseOver(mouseX, mouseY))) { return true; } @@ -421,6 +537,11 @@ public class RecipeWidget extends Widget { return tooltip; } + if (isPauseMarkerMouseOver(mouseX, mouseY)) { + tooltip.add(NEIClientUtils.translate("recipe.pinned")); + return tooltip; + } + final List buttonTooltip = forEachButtons( button -> button.contains(mouseX, mouseY) ? button.handleTooltip(tooltip) : null, null); @@ -481,30 +602,6 @@ public class RecipeWidget extends Widget { tooltip.add(GuiDraw.TOOLTIP_HANDLER + GuiDraw.getTipLineId(this.acceptsTooltip)); } - final Point outputKey = cycling == null && hovered != null && this.group.size() > 1 - ? this.group.getSlotKey(hovered) - : null; - - if (outputKey == null || !NEIClientConfig.showCycledIngredientsTooltip()) { - this.variantsTooltip = null; - this.variantsSlotKey = null; - } else if (!outputKey.equals(this.variantsSlotKey)) { - final List variants = this.group.getOutputVariants(outputKey); - - this.variantsSlotKey = outputKey; - this.variantsTooltip = variants.size() > 1 - ? new ItemsTooltipLineHandler(NEIClientUtils.translate("recipe.group"), variants, false, 4) - : null; - - if (this.variantsTooltip != null) { - this.variantsTooltip.setActiveStack(hovered.item); - } - } - - if (this.variantsTooltip != null) { - tooltip.add(GuiDraw.TOOLTIP_HANDLER + GuiDraw.getTipLineId(this.variantsTooltip)); - } - return tooltip; } @@ -563,7 +660,7 @@ public class RecipeWidget extends Widget { return guiRecipe != null && this.handler.mouseScrolled(guiRecipe, scroll, getRecipeIndex()); } - protected boolean scrollPermutations(int scroll, PositionedStack hovered) { + private boolean scrollPermutations(int scroll, PositionedStack hovered) { final Point slotKey = this.group.getSlotKey(hovered); final List items = this.group.getPermutations(slotKey); @@ -571,46 +668,23 @@ public class RecipeWidget extends Widget { return false; } - final List slots = new ArrayList<>(this.group.size()); + final int current = Math.max(0, PermutationsCycler.indexOf(items, hovered.item)); + final int direction = scroll > 0 ? -1 : 1; - for (int member = 0; member < this.group.size(); member++) { - slots.add(this.group.getSlot(member, slotKey)); - } - - final int anchor = getCursorMember(); - final int currentItem = PermutationsCycler.indexOf(items, hovered.item); - final List variants = new ArrayList<>(); - int current = 0; - - for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) { - final ItemStack stack = items.get(itemIndex); - int member = containsStack(slots.get(this.activeMember), stack) ? this.activeMember : -1; - - for (int offset = 0; member == -1 && offset < slots.size(); offset++) { - if (containsStack(slots.get((anchor + offset) % slots.size()), stack)) { - member = (anchor + offset) % slots.size(); - } - } + for (int step = 1; step <= items.size(); step++) { + final ItemStack stack = items.get(Math.floorMod(current + direction * step, items.size())); + final int member = this.group.findMember(slotKey, stack, this.activeMember); if (member != -1) { - if (itemIndex == currentItem) { - current = variants.size(); - } - variants.add(new int[] { itemIndex, member }); + switchMember(member, stack); + return true; } } - if (variants.isEmpty()) { - return false; - } - - final int[] next = variants.get(((current - scroll) % variants.size() + variants.size()) % variants.size()); - switchMember(next[1], items.get(next[0])); - - return true; + return false; } - protected boolean scrollMembers(int scroll) { + private boolean scrollMembers(int scroll) { final int size = this.group.size(); if (size <= 1) { @@ -622,14 +696,19 @@ public class RecipeWidget extends Widget { return true; } - protected void switchMember(int member, ItemStack stack) { + private void switchMember(int member, ItemStack stack) { final Map rendered = new HashMap<>(); + setPaused(false); + for (PositionedStack pStack : getCyclingStacks()) { rendered.put(this.group.getSlotKey(pStack), pStack.item); } setActiveMember(member); + syncCycle(); + this.group.refresh(member); + this.badgeCache.clear(); for (PositionedStack pStack : getCyclingStacks()) { final ItemStack renderStack = containsStack(pStack, stack) ? stack @@ -637,6 +716,8 @@ public class RecipeWidget extends Widget { if (containsStack(pStack, renderStack)) { pStack.setPermutationToRender(renderStack); + } else { + updatePermutationsFor(pStack); } } @@ -644,10 +725,9 @@ public class RecipeWidget extends Widget { notifyPermutationsChanged(); } - protected boolean tickCycle() { + private boolean tickCycle() { - if (!NEIClientUtils.shiftKey() && (this.cycleticks++ / TICKS_PER_CYCLE) != this.lastcycle - || this.lastcycle == -1) { + if ((this.cycleticks++ / TICKS_PER_CYCLE) != this.lastcycle) { this.lastcycle = this.cycleticks / TICKS_PER_CYCLE; return true; } @@ -655,20 +735,45 @@ public class RecipeWidget extends Widget { return false; } - protected void nextMember() { - final List allowed = this.group.getAllowedMembers(); + private boolean isCycleFrozen(int mouseX, int mouseY) { - this.memberCursor = (this.memberCursor + 1) % allowed.size(); - setActiveMember(allowed.get(this.memberCursor)); + if (this.paused || NEIClientUtils.shiftKey()) { + return true; + } + + return !this.showAsWidget && contains(mouseX, mouseY) + && (getPositionedStackMouseOver(mouseX, mouseY) != null + || forEachButtons(button -> button.contains(mouseX, mouseY) ? true : null, false)); } - protected int getCursorMember() { - final List allowed = this.group.getAllowedMembers(); - return allowed.get(Math.max(0, this.memberCursor) % allowed.size()); + private void applyCycle() { + + if (!this.paused) { + final List allowed = this.group.getAllowedMembers(); + setActiveMember(allowed.get(this.cycle % allowed.size())); + } + + this.group.refresh(this.activeMember); + updatePermutations(); } - protected void updatePermutations(int cycle) { - this.slotCycle = cycle; + private int getPermutationIndex() { + return this.cycle / this.group.getAllowedMembers().size(); + } + + // keep the permutation index, move the member part of the counter to the active member + private void syncCycle() { + final List allowed = this.group.getAllowedMembers(); + final int allowedIndex = allowed.indexOf(this.activeMember); + + if (allowedIndex >= 0) { + this.cycle = this.cycle / allowed.size() * allowed.size() + allowedIndex; + } + } + + private void updatePermutations() { + // badges depend on the rendered item + this.badgeCache.clear(); for (PositionedStack pStack : getCyclingStacks()) { updatePermutationsFor(pStack); @@ -678,7 +783,7 @@ public class RecipeWidget extends Widget { notifyPermutationsChanged(); } - protected void updateTooltipActiveStack() { + private void updateTooltipActiveStack() { if (this.acceptsTooltip != null && this.acceptsTooltip.tooltipGUID instanceof Point slotKey) { final PositionedStack pStack = this.group.getSlot(this.activeMember, slotKey); @@ -688,29 +793,20 @@ public class RecipeWidget extends Widget { } } - if (this.variantsTooltip != null) { - final PositionedStack pStack = this.group.getOutputSlot(this.activeMember, this.variantsSlotKey); - - if (pStack != null) { - this.variantsTooltip.setActiveStack(pStack.item); - } - } - } - protected void updatePermutationsFor(PositionedStack pStack) { + private void updatePermutationsFor(PositionedStack pStack) { + final int index = this.group + .getCycledPermutationIndex(this.activeMember, this.group.getSlotKey(pStack), getPermutationIndex()); - if (!this.renderPermutations.contains(pStack) || this.renderPermutations.get(pStack).size() > 1) { - final ItemStack stack = this.renderPermutations.getCycled(pStack, this.slotCycle); - - if (stack != null) { - pStack.setPermutationToRender(stack); - } + if (index >= 0) { + pStack.setPermutationToRender(index); } + applyPinnedIngredient(pStack); } - protected void setActiveMember(int member) { + private void setActiveMember(int member) { if (member != this.activeMember) { this.activeMember = member; @@ -722,22 +818,21 @@ public class RecipeWidget extends Widget { } - protected boolean containsStack(PositionedStack pStack, ItemStack stack) { - return pStack != null && stack != null - && PermutationsCycler.indexOf(pStack.getFilteredPermutations(), stack) != -1; + private boolean containsStack(PositionedStack pStack, ItemStack stack) { + return pStack != null && this.group.hasPermutation(this.activeMember, this.group.getSlotKey(pStack), stack); } - protected List getPermutations(PositionedStack pStack) { + private List getPermutations(PositionedStack pStack) { return this.group.getPermutations(this.group.getSlotKey(pStack)); } - protected void notifyPermutationsChanged() { + private void notifyPermutationsChanged() { for (GuiRecipeButton button : getRecipeButtonsIfInit()) { button.onPermutationsChanged(); } } - protected R forEachButtons(Function callback, R defaultValue) { + private R forEachButtons(Function callback, R defaultValue) { for (GuiRecipeButton button : getRecipeButtonsIfInit()) { button.xPosition += this.x; @@ -773,7 +868,7 @@ public class RecipeWidget extends Widget { return cycling != null ? cycling : getOutputStackMouseOver(mx, my); } - protected PositionedStack getOutputStackMouseOver(int mx, int my) { + private PositionedStack getOutputStackMouseOver(int mx, int my) { final int yShift = this.handlerInfo.getYShift(); for (PositionedStack pStack : getOutputs()) { @@ -785,16 +880,10 @@ public class RecipeWidget extends Widget { return null; } - protected PositionedStack getCyclingStackMouseOver(int mx, int my) { + private PositionedStack getCyclingStackMouseOver(int mx, int my) { final int yShift = this.handlerInfo.getYShift(); - for (PositionedStack pStack : getInputs()) { - if (pStack.contains(mx - this.x, my - this.y - yShift)) { - return pStack; - } - } - - for (PositionedStack pStack : getCatalysts()) { + for (PositionedStack pStack : getCyclingStacks()) { if (pStack.contains(mx - this.x, my - this.y - yShift)) { return pStack; } @@ -809,7 +898,7 @@ public class RecipeWidget extends Widget { return pStack != null ? pStack.item : null; } - protected GuiRecipe getGuiRecipe() { + private GuiRecipe getGuiRecipe() { final GuiContainer guiContainer = NEIClientUtils.getGuiContainer(); if (guiContainer instanceof GuiRecipeguiRecipe) { @@ -819,18 +908,10 @@ public class RecipeWidget extends Widget { return null; } - protected List getInputs() { - return this.group.getInputs(this.activeMember); - } - protected List getOutputs() { return this.group.getOutputs(this.activeMember); } - protected List getCatalysts() { - return this.group.getCatalysts(this.activeMember); - } - protected List getCyclingStacks() { return this.group.getCyclingStacks(this.activeMember); } diff --git a/src/main/resources/assets/nei/lang/en_US.lang b/src/main/resources/assets/nei/lang/en_US.lang index 887dcc49..d9a176c6 100644 --- a/src/main/resources/assets/nei/lang/en_US.lang +++ b/src/main/resources/assets/nei/lang/en_US.lang @@ -685,8 +685,8 @@ nei.recipe.concurrent.usage=Concurrent Usage - Total nei.recipe.concurrent.crafting=Concurrent Crafting - Total nei.recipe.accepts=Accepts following nei.recipe.accepts.scroll=Change Item -nei.recipe.group=Variants nei.recipe.group.scroll=Change Variant (%s/%s) +nei.recipe.pinned=Click to resume cycling nei.recipe.overlay=Recipe Overlay nei.recipe.overlay.missing=Missing following diff --git a/src/main/resources/assets/nei/lang/ru_RU.lang b/src/main/resources/assets/nei/lang/ru_RU.lang index e3098a8f..076ec044 100644 --- a/src/main/resources/assets/nei/lang/ru_RU.lang +++ b/src/main/resources/assets/nei/lang/ru_RU.lang @@ -422,6 +422,7 @@ nei.recipe.profiler.usage=Профилирование использовани nei.recipe.concurrent.usage=Одновременное использование - всего nei.recipe.concurrent.crafting=Одновременный крафт - всего nei.recipe.accepts=Принимает следующее +nei.recipe.pinned=Нажмите, чтобы продолжить перебор nei.recipe.overlay=отображение рецепта nei.recipe.overlay.missing=Отсутствует следующее diff --git a/src/main/resources/assets/nei/textures/pinned_pause.png b/src/main/resources/assets/nei/textures/pinned_pause.png new file mode 100644 index 00000000..6a2c26f7 Binary files /dev/null and b/src/main/resources/assets/nei/textures/pinned_pause.png differ