diff --git a/src/main/java/codechicken/nei/ItemCraftablesPanel.java b/src/main/java/codechicken/nei/ItemCraftablesPanel.java index af2627ff..ac6c4683 100644 --- a/src/main/java/codechicken/nei/ItemCraftablesPanel.java +++ b/src/main/java/codechicken/nei/ItemCraftablesPanel.java @@ -6,11 +6,14 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import net.minecraft.client.gui.inventory.GuiContainer; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidContainerRegistry; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.ItemsGrid.ItemsGridSlot; @@ -21,7 +24,6 @@ import codechicken.nei.recipe.GuiCraftingRecipe; import codechicken.nei.recipe.GuiRecipe; import codechicken.nei.recipe.ICraftingHandler; import codechicken.nei.recipe.IRecipeHandler; -import codechicken.nei.recipe.Recipe; import codechicken.nei.recipe.Recipe.RecipeId; import codechicken.nei.recipe.RecipeInfo; import codechicken.nei.recipe.StackInfo; @@ -82,6 +84,42 @@ public class ItemCraftablesPanel } + private static class CraftablesSnapshot { + + final GuiContainer guiContainer; + final ItemStackAmount inventory; + final boolean showOnlyFavorites; + final int maxCandidates; + + CraftablesSnapshot(GuiContainer guiContainer, ItemStackAmount inventory, boolean showOnlyFavorites, + int maxCandidates) { + this.guiContainer = guiContainer; + this.inventory = inventory; + this.showOnlyFavorites = showOnlyFavorites; + this.maxCandidates = maxCandidates; + } + } + + private static class RecipeCandidate { + + final IRecipeHandler handler; + final int recipeIndex; + final List ingredientStacks; + final ItemStack sortKeyItem; + + RecipeCandidate(IRecipeHandler handler, int recipeIndex, List ingredientStacks, + ItemStack sortKeyItem) { + this.handler = handler; + this.recipeIndex = recipeIndex; + this.ingredientStacks = ingredientStacks; + this.sortKeyItem = sortKeyItem; + } + } + + private static final Comparator ITEM_SORT_COMPARATOR = Comparator + .comparing(FavoriteRecipes::containsManual).thenComparing(FavoriteRecipes::contains).reversed() + .thenComparing(ItemSorter.instance); + public static List guiBlacklist = new ArrayList<>(); private final int MIN_DELAY = 600; @@ -93,6 +131,16 @@ public class ItemCraftablesPanel private long lastPlayerInventorySync = 0; private int lastFavoritesCount = 0; + private volatile CraftablesSnapshot pendingSnapshot; + private final AtomicReference> pendingResult = new AtomicReference<>(); + private final RestartableTask task = new RestartableTask("NEI Craftables Panel") { + + @Override + public void execute() { + pendingResult.set(generateCraftables(ItemCraftablesPanel.this.pendingSnapshot)); + } + }; + public ItemCraftablesPanel() { this.grid = new ItemsGrid<>() { @@ -114,12 +162,7 @@ public class ItemCraftablesPanel if (!ItemCraftablesPanel.this.availableRecipes.isEmpty()) { final List> entries = ItemCraftablesPanel.this.availableRecipes - .entrySet().stream() - .sorted( - Map.Entry.comparingByKey( - Comparator.comparing(FavoriteRecipes::containsManual) - .thenComparing(FavoriteRecipes::contains).reversed() - .thenComparing(ItemSorter.instance))) + .entrySet().stream().sorted(Map.Entry.comparingByKey(ITEM_SORT_COMPARATOR)) .limit(maxSlotIndex).collect(Collectors.toList()); int slotIndex = 0; int itemIndex = 0; @@ -195,10 +238,20 @@ public class ItemCraftablesPanel public void update() { this.splittingLineColor = NEIClientConfig.getSetting("inventory.craftables.color").getHexValue(); + applyPendingResult(); updateCraftables(); super.update(); } + private void applyPendingResult() { + final Map result = this.pendingResult.getAndSet(null); + + if (result != null && NEIClientConfig.showCraftablesPanelWidget()) { + this.availableRecipes = result; + this.grid.onGridChanged(); + } + } + private void updateCraftables() { if (!NEIClientConfig.showCraftablesPanelWidget()) { @@ -219,8 +272,14 @@ public class ItemCraftablesPanel this.lastPlayerInventory = inv; this.lastGuiContainer = firstGui; this.lastFavoritesCount = FavoriteRecipes.size(); - this.availableRecipes = generateCraftables(firstGui); - this.grid.onGridChanged(); + final int maxRows = NEIClientConfig.getIntSetting("inventory.craftables.useRows") + 2; + final int maxCandidates = Math.max(this.grid.columns, 1) * maxRows; + this.pendingSnapshot = new CraftablesSnapshot( + firstGui, + inv, + showOnlyFavorites, + maxCandidates); + this.task.restart(); } } else { @@ -244,17 +303,28 @@ public class ItemCraftablesPanel return (firstGui instanceof GuiRecipegui) ? gui.firstGui : firstGui; } - private Map generateCraftables(GuiContainer firstGui) { - final List availableHandlers = getAvailableHandlers(firstGui); + private Map generateCraftables(CraftablesSnapshot snapshot) { + final List availableHandlers = getAvailableHandlers(snapshot.guiContainer); final Map availableRecipes = new HashMap<>(); if (!availableHandlers.isEmpty()) { - final List invStacks = ItemCraftablesPanel.this.lastPlayerInventory.values(); + final Map> invByItem = indexInventoryByItem(snapshot.inventory.values()); - for (IRecipeHandler handler : availableHandlers) { - availableRecipes.putAll(filterHandlerRecipes(handler, firstGui, invStacks)); + if (snapshot.showOnlyFavorites) { + for (IRecipeHandler handler : availableHandlers) { + availableRecipes.putAll(filterHandlerRecipes(handler, invByItem, true)); + } + } else { + final List candidates = Collections.synchronizedList(new ArrayList<>()); + + for (IRecipeHandler handler : availableHandlers) { + collectHandlerCandidates(handler, invByItem, candidates); + } + + candidates.sort(Comparator.comparing(c -> c.sortKeyItem, ITEM_SORT_COMPARATOR)); + candidates.subList(0, Math.min(candidates.size(), snapshot.maxCandidates)).parallelStream() + .forEach(candidate -> buildCandidate(candidate, availableRecipes)); } - } return availableRecipes; @@ -270,19 +340,126 @@ public class ItemCraftablesPanel return availableHandlers; } - private Map filterHandlerRecipes(IRecipeHandler handler, GuiContainer firstGui, - List invStacks) { + private static Map> indexInventoryByItem(List invStacks) { + final Map> index = new HashMap<>(); + + for (ItemStack stack : invStacks) { + index.computeIfAbsent(stack.getItem(), i -> new ArrayList<>()).add(stack); + } + + return index; + } + + private void collectHandlerCandidates(IRecipeHandler handler, Map> invByItem, + List candidates) { + IntStream.range(0, handler.numRecipes()).parallel().forEach(recipeIndex -> { + final List ingredientStacks = handler.getIngredientStacks(recipeIndex); + + if (existsIngredients(ingredientStacks, invByItem)) { + final ItemStack sortKeyItem = getCheapResultItem(handler, recipeIndex); + + if (sortKeyItem != null) { + candidates.add(new RecipeCandidate(handler, recipeIndex, ingredientStacks, sortKeyItem)); + } + } + }); + } + + private void buildCandidate(RecipeCandidate candidate, Map availableRecipes) { + final ItemStack result = getCheapResult(candidate.handler, candidate.recipeIndex); + + if (result != null) { + final RecipeId recipeId = RecipeId.of(candidate.handler, candidate.recipeIndex, candidate.ingredientStacks); + + synchronized (availableRecipes) { + availableRecipes.put(result, recipeId); + } + } + } + + private static ItemStack getCheapResultItem(IRecipeHandler handler, int recipeIndex) { + final PositionedStack resultStack = handler.getResultStack(recipeIndex); + + if (resultStack != null) { + return resultStack.item; + } + + final List otherStacks = handler.getOtherStacks(recipeIndex); + + for (PositionedStack otherStack : otherStacks) { + if (!FluidContainerRegistry.isContainer(otherStack.items[0]) + || StackInfo.getFluid(otherStack.items[0]) != null) { + return otherStack.item; + } + } + + return otherStacks.isEmpty() ? null : otherStacks.get(0).item; + } + + private static ItemStack getCheapResult(IRecipeHandler handler, int recipeIndex) { + final PositionedStack resultStack = handler.getResultStack(recipeIndex); + final List results = resultStack != null ? Collections.singletonList(resultStack) + : handler.getOtherStacks(recipeIndex); + + if (results.isEmpty()) { + return null; + } + + ItemStack stack = null; + + if (results.size() > 1) { + for (PositionedStack result : results) { + if (!FluidContainerRegistry.isContainer(result.item) || StackInfo.getFluid(result.item) != null) { + stack = result.item; + break; + } + } + } + + if (stack == null) { + stack = results.get(0).item; + } + + int stackSize = 0; + + for (PositionedStack result : results) { + if (containsSameType(result, stack)) { + stackSize += StackInfo.getAmount(result.item); + } + } + + return StackInfo.withAmount(stack, stackSize); + } + + private static boolean containsSameType(PositionedStack pStack, ItemStack stack) { + final ItemStack normalized = StackInfo.withAmount(stack, 0); + + for (ItemStack permutation : pStack.items) { + if (NEIServerUtils.areStacksSameTypeWithNBT(StackInfo.withAmount(permutation, 0), normalized)) { + return true; + } + } + + return false; + } + + private Map filterHandlerRecipes(IRecipeHandler handler, Map> invByItem, + boolean showOnlyFavorites) { final Map availableRecipes = new HashMap<>(); - final boolean showOnlyFavorites = NEIClientConfig.getBooleanSetting("inventory.craftables.favoritesOnly"); IntStream.range(0, handler.numRecipes()).parallel().forEach(recipeIndex -> { - if (existsIngredients(firstGui, handler.getIngredientStacks(recipeIndex), invStacks)) { - final Recipe recipe = Recipe.of(handler, recipeIndex); + final List ingredientStacks = handler.getIngredientStacks(recipeIndex); - if (!recipe.getResults().isEmpty() - && (!showOnlyFavorites || FavoriteRecipes.getFavorite(recipe.getRecipeId()) != null)) { - synchronized (availableRecipes) { - availableRecipes.put(recipe.getResult(), recipe.getRecipeId()); + if (existsIngredients(ingredientStacks, invByItem)) { + final ItemStack result = getCheapResult(handler, recipeIndex); + + if (result != null) { + final RecipeId recipeId = RecipeId.of(handler, recipeIndex, ingredientStacks); + + if (!showOnlyFavorites || FavoriteRecipes.getFavorite(recipeId) != null) { + synchronized (availableRecipes) { + availableRecipes.put(result, recipeId); + } } } } @@ -291,14 +468,27 @@ public class ItemCraftablesPanel return availableRecipes; } - private boolean existsIngredients(GuiContainer firstGui, List ingredients, - List invStacks) { + private boolean existsIngredients(List ingredients, Map> invByItem) { final Map usedItems = new HashMap<>(); for (PositionedStack pStack : ingredients) { - final ItemStack used = invStacks.stream().filter( - is -> usedItems.getOrDefault(is, is.stackSize) >= pStack.item.stackSize && pStack.contains(is)) - .findAny().orElse(null); + ItemStack used = null; + + for (ItemStack permutation : pStack.items) { + final List candidates = invByItem.get(permutation.getItem()); + + if (candidates == null) continue; + + for (ItemStack is : candidates) { + if (usedItems.getOrDefault(is, is.stackSize) >= pStack.item.stackSize + && NEIServerUtils.areStacksSameTypeCrafting(permutation, is)) { + used = is; + break; + } + } + + if (used != null) break; + } if (used != null) { usedItems.put(used, usedItems.getOrDefault(used, used.stackSize) - pStack.item.stackSize); diff --git a/src/main/java/codechicken/nei/recipe/Recipe.java b/src/main/java/codechicken/nei/recipe/Recipe.java index c0cbc174..4b987a2a 100644 --- a/src/main/java/codechicken/nei/recipe/Recipe.java +++ b/src/main/java/codechicken/nei/recipe/Recipe.java @@ -4,7 +4,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.IdentityHashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import net.minecraft.item.Item; @@ -43,7 +45,13 @@ public class Recipe { if (!isShapedRecipe()) { // sort to ignore ingredients position in shapeless recipes - this.ingredients.sort(Comparator.comparing(StackInfo::getItemStackGUID)); + final Map guids = new IdentityHashMap<>(); + + for (ItemStack ingredient : this.ingredients) { + guids.put(ingredient, StackInfo.getItemStackGUID(ingredient)); + } + + this.ingredients.sort(Comparator.comparing(guids::get)); } } @@ -52,7 +60,10 @@ public class Recipe { } public static RecipeId of(IRecipeHandler handler, int recipeIndex) { - final List ingredients = handler.getIngredientStacks(recipeIndex); + return of(handler, recipeIndex, handler.getIngredientStacks(recipeIndex)); + } + + public static RecipeId of(IRecipeHandler handler, int recipeIndex, List ingredients) { final String handlerName = GuiRecipeTab.getHandlerInfo(handler).getHandlerName(); PositionedStack pStackResult = handler.getResultStack(recipeIndex);