Recipe Group Optimization (#1054)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
slprimeandgithub-actions[bot] authored and GitHub committed 2026-09-28 00:19:29 +03:00
1 parent 9f3697bb67
commit a685efb3b7
11 files changed
+654 -214

No files matched your search

@@ -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;
}
@@ -163,7 +163,7 @@ public class PositionedStack implements Cloneable {
}
public List<ItemStack> getFilteredPermutations(ItemFilter additionalFilter) {
List<ItemStack> items = Arrays.asList(this.items);
List<ItemStack> items = new ArrayList<>(Arrays.asList(this.items));
items = filteringPermutations(items, item -> !ItemInfo.isHidden(item));
items = filteringPermutations(items, PresetsList.getItemFilter());
@@ -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<ICraftingHandler> {
public static GuiRecipe<?> createRecipeGui(String outputId, boolean open, Object... results) {
final Minecraft mc = NEIClientUtils.mc();
final RecipeId recipeId;
List<ItemStack> 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<ICraftingHandler> {
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 List<?>list) {
pinnedIngredients = extractIngredients(list);
}
} else {
recipeId = getCurrentRecipeId(mc.currentScreen);
}
@@ -65,13 +73,25 @@ public class GuiCraftingRecipe extends GuiRecipe<ICraftingHandler> {
mc.displayGuiScreen(gui);
}
gui.openTargetRecipe(recipeId);
gui.openTargetRecipe(recipeId, pinnedIngredients);
return gui;
}
return null;
}
private static List<ItemStack> extractIngredients(List<?> list) {
final List<ItemStack> ingredients = new ArrayList<>(list.size());
for (Object item : list) {
if (item instanceof ItemStack stack) {
ingredients.add(stack);
}
}
return ingredients;
}
public static ArrayList<ICraftingHandler> getCraftingHandlers(String outputId, Object... results) {
ArrayList<ICraftingHandler> craftinghandlers = GuiCraftingRecipe.craftinghandlers;
ArrayList<ICraftingHandler> serialCraftingHandlers = GuiCraftingRecipe.serialCraftingHandlers;
@@ -524,6 +524,13 @@ public abstract class GuiRecipe<H extends IRecipeHandler> 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<ItemStack> pinnedIngredients) {
int recipeIndex = -1;
int recipetype = 0;
@@ -551,6 +558,10 @@ public abstract class GuiRecipe<H extends IRecipeHandler> extends GuiContainer i
setRecipePage(recipetype);
this.handlerPages.gotoRecipeIndex(Math.max(0, recipeIndex));
if (pinnedIngredients != null && recipeIndex >= 0) {
this.handlerPages.pinRecipe(recipeIndex, pinnedIngredients);
}
return recipeIndex;
}
@@ -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<RecipeWidget> 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<RecipeWidget> getPausedWidgets() {
final List<RecipeWidget> paused = new ArrayList<>();
for (List<Widget> 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<ItemStack> ingredients) {
for (List<Widget> widgets : this.pageWidgets) {
for (Widget widget : widgets) {
if (widget instanceof RecipeWidget recipeWidget && recipeWidget.containsRecipeIndex(recipeIndex)) {
return recipeWidget.pinRecipe(recipeIndex, ingredients);
}
}
}
return false;
}
public List<Widget> getCurrentPageWidgets() {
if (this.pageWidgets.isEmpty()) {
return Collections.emptyList();
@@ -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<K> {
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<K> {
return items;
}
public int getFavoriteCount(K key) {
get(key);
return this.favoriteCounts.getOrDefault(key, 0);
}
public ItemStack getCycled(K key, int cycle) {
final List<ItemStack> items = get(key);
final int favoriteCount = this.favoriteCounts.getOrDefault(key, 0);
@@ -96,4 +107,10 @@ public class PermutationsCycler<K> {
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);
}
}
@@ -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<ItemStack> 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<PositionedStack> inputs;
public final List<PositionedStack> outputs;
public final List<PositionedStack> auxiliaries;
public final List<PositionedStack> cycling;
public final List<StackState> states = new ArrayList<>();
public final Map<Point, PositionedStack> slots = new HashMap<>();
public final Map<Point, FilteredSlot> filtered = new HashMap<>();
public MemberStacks(List<PositionedStack> inputs, List<PositionedStack> outputs,
List<PositionedStack> 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<Integer> SINGLE_MEMBER = Collections.singletonList(0);
private final IRecipeHandler handler;
@@ -26,16 +112,33 @@ public class RecipeGroup {
private final HandlerInfo handlerInfo;
private final PermutationsCycler<Point> slotPermutations;
private final MemberStacks[] memberStacks;
private Map<Point, PositionedStack[]> slotStacks = null;
private final Set<Point> sharedSlots = new HashSet<>();
private List<Integer> allowedMembers = null;
private Boolean cycling = null;
public RecipeGroup(IRecipeHandler handler, List<Integer> 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<Point>(
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<ItemStack> 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<ItemStack> 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<PositionedStack> getInputs(int member) {
return this.handler.getIngredientStacks(getRecipeIndex(member));
return getStacks(member).inputs;
}
public List<PositionedStack> 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<PositionedStack> 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<PositionedStack> getCyclingStacks(int member) {
final List<PositionedStack> 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<ItemStack> getOutputVariants(Point slotKey) {
final List<ItemStack> items = new ArrayList<>();
protected MemberStacks loadStacks(int member) {
final int recipeIndex = getRecipeIndex(member);
final List<PositionedStack> inputs = new ArrayList<>(this.handler.getIngredientStacks(recipeIndex));
final PositionedStack pStackResult = this.handler.getResultStack(recipeIndex);
final List<PositionedStack> 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<Integer> getAllowedMembers() {
@@ -135,17 +322,20 @@ public class RecipeGroup {
return SINGLE_MEMBER;
}
if (this.allowedMembers == null) {
final Set<Point> 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<ItemStack> 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<Point> 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<Point, PositionedStack[]> 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<Point, PositionedStack[]> 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<ItemStack> 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<Integer> filterMembers(Set<Point> favoriteSlots) {
final List<Integer> 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<ItemStack> 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<ItemStack> collectSlotPermutations(Point slotKey) {
final List<ItemStack> items = new ArrayList<>();
if (!getSlotStacks().containsKey(slotKey)) {
return Collections.emptyList();
}
final Map<Object, ItemStack> 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<ItemStack> 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());
}
}
@@ -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<PositionedStack> renderPermutations;
protected final Map<PositionedStack, List<Badge>> badgeCache = new WeakHashMap<>();
protected final Map<Integer, List<GuiRecipeButton>> 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<ItemStack> pinnedIngredients = null;
protected Map<Point, ItemStack> 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<PositionedStack>(
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<ItemStack> 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<ItemStack> ingredients) {
final int member = this.group.indexOf(recipeIndex);
if (member < 0 || !this.group.canCycle()) {
return false;
}
final Map<Point, ItemStack> 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<Point, ItemStack> matchIngredients(List<PositionedStack> stacks, List<ItemStack> 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<Point, ItemStack> 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<PositionedStack> stacks, List<ItemStack> 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<GuiRecipeButton> getRecipeButtonsIfInit() {
private List<GuiRecipeButton> getRecipeButtonsIfInit() {
return this.buttonCache.getOrDefault(this.activeMember, Collections.emptyList());
}
protected List<GuiRecipeButton> createButtons(int member) {
private List<GuiRecipeButton> 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<GuiRecipeButton> initButtons(RecipeHandlerRef ref) {
private List<GuiRecipeButton> initButtons(RecipeHandlerRef ref) {
final GuiRecipe<?> guiRecipe = getGuiRecipe();
if (guiRecipe != null) {
@@ -186,7 +286,7 @@ public class RecipeWidget extends Widget {
}
protected List<GuiRecipeButton> getDefaultButtons(RecipeHandlerRef ref) {
private List<GuiRecipeButton> getDefaultButtons(RecipeHandlerRef ref) {
GuiContainer guiContainer = NEIClientUtils.getGuiContainer();
final List<GuiRecipeButton> 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<Badge> getBadges(PositionedStack pStack, boolean input) {
private List<Badge> getBadges(PositionedStack pStack, boolean input) {
return this.badgeCache.computeIfAbsent(pStack, k -> {
final List<Badge> 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<Badge> 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<String> 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<ItemStack> 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<ItemStack> items = this.group.getPermutations(slotKey);
@@ -571,46 +668,23 @@ public class RecipeWidget extends Widget {
return false;
}
final List<PositionedStack> 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<int[]> 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<Point, ItemStack> 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<Integer> 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<Integer> allowed = this.group.getAllowedMembers();
return allowed.get(Math.max(0, this.memberCursor) % allowed.size());
private void applyCycle() {
if (!this.paused) {
final List<Integer> 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<Integer> 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<ItemStack> getPermutations(PositionedStack pStack) {
private List<ItemStack> 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> R forEachButtons(Function<GuiRecipeButton, R> callback, R defaultValue) {
private <R> R forEachButtons(Function<GuiRecipeButton, R> 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 GuiRecipe<?>guiRecipe) {
@@ -819,18 +908,10 @@ public class RecipeWidget extends Widget {
return null;
}
protected List<PositionedStack> getInputs() {
return this.group.getInputs(this.activeMember);
}
protected List<PositionedStack> getOutputs() {
return this.group.getOutputs(this.activeMember);
}
protected List<PositionedStack> getCatalysts() {
return this.group.getCatalysts(this.activeMember);
}
protected List<PositionedStack> getCyclingStacks() {
return this.group.getCyclingStacks(this.activeMember);
}
@@ -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
@@ -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=Отсутствует следующее
Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B