diff --git a/src/main/java/codechicken/nei/FavoriteRecipes.java b/src/main/java/codechicken/nei/FavoriteRecipes.java index 9e3f91957..34f292e86 100644 --- a/src/main/java/codechicken/nei/FavoriteRecipes.java +++ b/src/main/java/codechicken/nei/FavoriteRecipes.java @@ -8,39 +8,141 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.Set; +import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.fluids.FluidRegistry; -import net.minecraftforge.fluids.FluidStack; import org.apache.commons.io.IOUtils; +import com.google.common.io.Files; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import codechicken.core.CommonUtils; import codechicken.nei.api.API; +import codechicken.nei.recipe.GuiCraftingRecipe; +import codechicken.nei.recipe.GuiRecipeTab; +import codechicken.nei.recipe.ICraftingHandler; +import codechicken.nei.recipe.IRecipeHandler; import codechicken.nei.recipe.Recipe.RecipeId; +import codechicken.nei.recipe.RecipeHandlerRef; import codechicken.nei.recipe.StackInfo; +import codechicken.nei.util.FavoriteStorage; import codechicken.nei.util.NBTJson; public class FavoriteRecipes { - private static final Set tools = new HashSet<>(); - private static final Map items = new HashMap<>(); - private static final Map fluids = new HashMap<>(); + protected static final class FastKey { + + public final ItemStack stack; + private int hash; + + public FastKey(ItemStack stack) { + this.stack = stack; + + this.hash = 31 + Item.getIdFromItem(stack.getItem()); + this.hash = 31 * this.hash + stack.getItemDamage(); + } + + @Override + public int hashCode() { + return this.hash; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + + if (o instanceof FastKey k && this.stack.getItem() == k.stack.getItem() + && this.stack.getItemDamage() == k.stack.getItemDamage()) { + return this.stack == k.stack || Objects.equals(this.stack.stackTagCompound, k.stack.stackTagCompound); + } + + return false; + } + } + + private static final RestartableTask singleRecipeFavoritesTask = new RestartableTask( + "Generate Single-Recipe Favorites") { + + @Override + public void execute() { + final List handlers = GuiCraftingRecipe.getCraftingHandlers("all"); + final Map autogeneratedRecipes = new HashMap<>(); + final Set whitelistItems = new HashSet<>(ItemList.items.size()); + final FavoriteStorage storage = new FavoriteStorage(); + + for (ItemStack stack : ItemList.items) { + whitelistItems.add(new FastKey(stack)); + } + + if (interrupted()) return; + + for (ICraftingHandler handler : handlers) { + if (!GuiRecipeTab.getHandlerInfo(handler).getShowFavoritesButton()) continue; + + for (int i = 0; i < handler.numRecipes(); i++) { + RecipeHandlerRef ref = null; + + for (PositionedStack result : getOutputs(handler, i)) { + ItemStack stack = StackInfo.getItemStackWithMinimumDamage(result.items); + final FastKey key = new FastKey(stack); + + if (!whitelistItems.contains(key)) continue; + + if (autogeneratedRecipes.get(key) == null) { + + if (ref == null) { + ref = RecipeHandlerRef.of(handler, i); + } + + autogeneratedRecipes.put(key, ref); + } else { + autogeneratedRecipes.remove(key); + whitelistItems.remove(key); + } + + } + } + if (interrupted()) return; + } + + final Map recipesCache = new HashMap<>(); + + for (Map.Entry entry : autogeneratedRecipes.entrySet()) { + final RecipeId recipeId = recipesCache + .computeIfAbsent(entry.getValue(), r -> RecipeId.of(r.handler, r.recipeIndex)); + storage.add(entry.getKey().stack, recipeId); + } + + FavoriteRecipes.autoFavorites = storage; + SubsetWidget.updateHiddenItems(); + ItemList.updateFilter.restart(); + } + + protected List getOutputs(IRecipeHandler handler, int recipeIndex) { + final PositionedStack pStackResult = handler.getResultStack(recipeIndex); + return pStackResult != null ? Collections.singletonList(pStackResult) : handler.getOtherStacks(recipeIndex); + } + + }; + + private static final FavoriteStorage manualFavorites = new FavoriteStorage(); + private static FavoriteStorage autoFavorites = new FavoriteStorage(); private static File favoriteFile; static { - API.addSubset("Favorites", FavoriteRecipes::contains); + API.addSubset("Favorites.Manual", manualFavorites::contains); + API.addSubset("Favorites.Generated", stack -> autoFavorites.contains(stack)); } private FavoriteRecipes() {} @@ -66,7 +168,6 @@ public static void load() { final File defaultFavorites = configFavorites.exists() ? configFavorites : globalFavorites; if (defaultFavorites.exists()) { - try { if (favoriteFile.createNewFile()) { InputStream src = new FileInputStream(defaultFavorites); @@ -101,7 +202,9 @@ private static void loadData() { } final JsonParser parser = new JsonParser(); - items.clear(); + boolean hasErrors = false; + + manualFavorites.clear(); for (String itemStr : itemStrings) { @@ -121,70 +224,75 @@ private static void loadData() { continue; } - final String fluidKey = getFluidKey(stack); - - if (fluidKey != null) { - fluids.put(fluidKey, recipeId); + if (RecipeHandlerRef.of(recipeId) != null) { + manualFavorites.add(stack, recipeId); + } else { + hasErrors = true; } - items.put(itemStackNBT, recipeId); - - if (NEIServerUtils.isItemTool(stack)) { - tools.add(itemStackNBT); - } } } catch (Exception e) { NEIClientConfig.logger.error("Failed to load favorite from json string: {}", itemStr); + hasErrors = true; + } + + } + + if (hasErrors) { + + try { + Files.copy( + favoriteFile, + new File(favoriteFile.getAbsolutePath() + ".backup-" + System.currentTimeMillis())); + NEIClientConfig.logger.info("Backed up invalid favorite file to {}", favoriteFile); + } catch (IOException e) { + NEIClientConfig.logger.error("Failed to backup invalid favorite file to {}", favoriteFile, e); } + save(); } + loadAutoGeneratedRecipes(); + SubsetWidget.updateHiddenItems(); + ItemList.updateFilter.restart(); } public static RecipeId getFavorite(ItemStack stack) { if (NEIClientConfig.favoritesEnabled() && stack != null) { - RecipeId recipeId = items.get(StackInfo.itemStackToNBT(stack, false)); - - if (recipeId == null) { - recipeId = fluids.get(getFluidKey(stack)); - } - - if (recipeId != null) { - return recipeId; - } + final RecipeId recipeId = manualFavorites.getRecipeId(stack); + return recipeId != null ? recipeId : autoFavorites.getRecipeId(stack); + } - if (NEIServerUtils.isItemTool(stack) - && (stack.stackTagCompound == null || !stack.stackTagCompound.hasKey("GT.ToolStats"))) { - for (NBTTagCompound nbt : tools) { - if (NEIServerUtils.areStacksSameTypeCraftingWithNBT(stack, StackInfo.loadFromNBT(nbt))) { - return items.get(nbt); - } - } - } + return null; + } + public static ItemStack getManualFavorite(RecipeId recipeId) { + if (NEIClientConfig.favoritesEnabled() && recipeId != null) { + return manualFavorites.getItemStack(recipeId); } - return null; } public static ItemStack getFavorite(RecipeId recipeId) { - if (NEIClientConfig.favoritesEnabled()) { - final Optional> result = items.entrySet().stream() - .filter(entry -> entry.getValue().equals(recipeId)).findAny(); - if (result.isPresent()) { - return StackInfo.loadFromNBT(result.get().getKey()); - } + if (NEIClientConfig.favoritesEnabled() && recipeId != null) { + final ItemStack stack = manualFavorites.getItemStack(recipeId); + return stack != null ? stack : autoFavorites.getItemStack(recipeId); } return null; } public static int size() { - return items.size() + fluids.size(); + return manualFavorites.size() + autoFavorites.size(); + } + + public static void reload() { + save(); + load(); } public static boolean contains(ItemStack stack) { @@ -192,36 +300,34 @@ public static boolean contains(ItemStack stack) { } public static void setFavorite(ItemStack stack, RecipeId recipeId) { - final NBTTagCompound itemStackNBT = StackInfo.itemStackToNBT(stack, false); - final String fluidKey = getFluidKey(stack); + if (stack == null) return; - items.entrySet().removeIf(entry -> entry.getKey().equals(itemStackNBT) || entry.getValue().equals(recipeId)); - fluids.entrySet().removeIf(entry -> entry.getKey().equals(fluidKey) || entry.getValue().equals(recipeId)); + manualFavorites.removeItemStack(stack); if (recipeId != null) { - items.put(itemStackNBT, recipeId); - - if (NEIServerUtils.isItemTool(stack)) { - tools.add(itemStackNBT); - } - - if (fluidKey != null) { - fluids.put(fluidKey, recipeId); - } + manualFavorites.removeRecipeId(recipeId); + manualFavorites.add(stack, recipeId); } SubsetWidget.updateHiddenItems(); ItemList.updateFilter.restart(); } - private static String getFluidKey(ItemStack stack) { - final FluidStack fluid = StackInfo.getFluid(stack); + public static void loadAutoGeneratedRecipes() { + if (ItemList.loadFinished + && NEIClientConfig.getBooleanSetting("inventory.favorites.generateSingleRecipeFavorites")) { + singleRecipeFavoritesTask.restart(); + } else { + singleRecipeFavoritesTask.stop(); + autoFavorites.clear(); - if (fluid != null) { - return FluidRegistry.getFluidName(fluid); + SubsetWidget.updateHiddenItems(); + ItemList.updateFilter.restart(); } + } - return null; + public static boolean isAutogenerated(RecipeId recipeId) { + return autoFavorites.getItemStack(recipeId) != null; } public static void save() { @@ -232,7 +338,7 @@ public static void save() { final List strings = new ArrayList<>(); - for (Map.Entry entry : items.entrySet()) { + for (Map.Entry entry : manualFavorites.getAllFavorites()) { try { final JsonObject line = new JsonObject(); diff --git a/src/main/java/codechicken/nei/ItemList.java b/src/main/java/codechicken/nei/ItemList.java index 214b16e87..f2594d2fd 100644 --- a/src/main/java/codechicken/nei/ItemList.java +++ b/src/main/java/codechicken/nei/ItemList.java @@ -432,6 +432,7 @@ public void execute() { loadFinished = true; + FavoriteRecipes.loadAutoGeneratedRecipes(); SubsetWidget.updateHiddenItems(); ItemPanels.bookmarkPanel.load(); updateFilter.restart(); diff --git a/src/main/java/codechicken/nei/NEIClientConfig.java b/src/main/java/codechicken/nei/NEIClientConfig.java index 5363c00e4..39e8d6e8e 100644 --- a/src/main/java/codechicken/nei/NEIClientConfig.java +++ b/src/main/java/codechicken/nei/NEIClientConfig.java @@ -782,8 +782,7 @@ private static void setFavoriteDefaults(ConfigTagParent tag) { @Override public boolean onClick(int button) { super.onClick(button); - FavoriteRecipes.save(); - FavoriteRecipes.load(); + FavoriteRecipes.reload(); return true; } }); @@ -796,6 +795,18 @@ public boolean onClick(int button) { .getBooleanValue(false); API.addOption(new OptionToggleButton("inventory.favorites.showRecipeTooltipInGui", true)); + tag.getTag("inventory.favorites.generateSingleRecipeFavorites") + .setComment("Automatically generate favorites for items with only one recipe").getBooleanValue(true); + API.addOption(new OptionToggleButton("inventory.favorites.generateSingleRecipeFavorites", true) { + + @Override + public boolean onClick(int button) { + super.onClick(button); + FavoriteRecipes.loadAutoGeneratedRecipes(); + return true; + } + }); + tag.getTag("inventory.favorites.depth").setComment("Bookmark creation depth").getIntValue(3); API.addOption(new OptionIntegerField("inventory.favorites.depth", 0, 100)); } diff --git a/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java b/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java index 5806a9731..e5c060387 100644 --- a/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java +++ b/src/main/java/codechicken/nei/recipe/GuiFavoriteButton.java @@ -54,7 +54,7 @@ public GuiFavoriteButton(RecipeHandlerRef handlerRef, int x, int y) { super(handlerRef, x, y, BUTTON_ID_START + handlerRef.recipeIndex, "❤"); this.recipe = Recipe.of(this.handlerRef); - ItemStack stack = FavoriteRecipes.getFavorite(this.recipe.getRecipeId()); + ItemStack stack = FavoriteRecipes.getManualFavorite(this.recipe.getRecipeId()); this.favorite = stack != null; if (stack == null) { @@ -88,7 +88,7 @@ public void toggleFavorite() { @Override public void update() { - this.favorite = this.favorite && FavoriteRecipes.getFavorite(this.recipe.getRecipeId()) != null; + this.favorite = this.favorite && FavoriteRecipes.getManualFavorite(this.recipe.getRecipeId()) != null; } @Override @@ -146,7 +146,6 @@ public Map handleHotkeys(int mousex, int mousey, Map handleTooltip(List currenttip) { currenttip.add(translate("recipe.favorite")); - return currenttip; } @@ -178,7 +177,7 @@ public boolean mouseScrolled(int scroll) { this.selectedResult = results.get(nextIndex); this.favorite = StackInfo.equalItemAndNBT( results.get(nextIndex).getItemStack(), - FavoriteRecipes.getFavorite(this.recipe.getRecipeId()), + FavoriteRecipes.getManualFavorite(this.recipe.getRecipeId()), true); return true; diff --git a/src/main/java/codechicken/nei/recipe/Recipe.java b/src/main/java/codechicken/nei/recipe/Recipe.java index 6062b2e78..63c96e227 100644 --- a/src/main/java/codechicken/nei/recipe/Recipe.java +++ b/src/main/java/codechicken/nei/recipe/Recipe.java @@ -51,6 +51,24 @@ public static RecipeId of(Object result, String handlerName, Iterable ingredi return new RecipeId(extractItem(result), handlerName, extractIngredients(ingredients)); } + public static RecipeId of(IRecipeHandler handler, int recipeIndex) { + final List ingredients = handler.getIngredientStacks(recipeIndex); + final String handlerName = GuiRecipeTab.getHandlerInfo(handler).getHandlerName(); + PositionedStack pStackResult = handler.getResultStack(recipeIndex); + + if (pStackResult == null) { + for (PositionedStack otherStack : handler.getOtherStacks(recipeIndex)) { + if (!FluidContainerRegistry.isContainer(otherStack.items[0]) + || StackInfo.getFluid(otherStack.items[0]) != null) { + pStackResult = otherStack; + break; + } + } + } + + return new RecipeId(extractItem(pStackResult), handlerName, extractIngredients(ingredients)); + } + public static RecipeId of(JsonObject json) { String handlerName = null; List ingredients = new ArrayList<>(); diff --git a/src/main/java/codechicken/nei/util/FavoriteStorage.java b/src/main/java/codechicken/nei/util/FavoriteStorage.java new file mode 100644 index 000000000..444540886 --- /dev/null +++ b/src/main/java/codechicken/nei/util/FavoriteStorage.java @@ -0,0 +1,141 @@ +package codechicken.nei.util; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.fluids.Fluid; +import net.minecraftforge.fluids.FluidStack; + +import codechicken.nei.NEIServerUtils; +import codechicken.nei.recipe.Recipe.RecipeId; +import codechicken.nei.recipe.StackInfo; + +public class FavoriteStorage { + + protected final Map itemStackToRecipeId = new HashMap<>(); + protected final Map recipeIdToItemStack = new HashMap<>(); + protected final Map fluidToRecipeId = new IdentityHashMap<>(); + + protected final Map itemHashToRecipeId = new HashMap<>(); + protected final Map recipeIdToItemHash = new HashMap<>(); + protected final Map itemHashToItemStack = new HashMap<>(); + + public void clear() { + this.itemStackToRecipeId.clear(); + this.fluidToRecipeId.clear(); + this.recipeIdToItemStack.clear(); + this.itemHashToRecipeId.clear(); + this.recipeIdToItemHash.clear(); + this.itemHashToItemStack.clear(); + } + + public int size() { + return this.itemStackToRecipeId.size() + this.itemHashToRecipeId.size(); + } + + public void removeItemStack(ItemStack stack) { + final RecipeId recipeId = getRecipeId(stack); + + if (recipeId != null) { + removeRecipeId(recipeId); + } + } + + public void removeRecipeId(RecipeId recipeId) { + this.recipeIdToItemStack.remove(recipeId); + this.fluidToRecipeId.values().removeIf(recipeId::equals); + this.itemStackToRecipeId.values().removeIf(recipeId::equals); + this.itemHashToRecipeId.values().removeIf(recipeId::equals); + this.itemHashToItemStack.remove(this.recipeIdToItemHash.get(recipeId)); + this.recipeIdToItemHash.remove(recipeId); + } + + public void add(ItemStack stack, RecipeId recipeId) { + + if (!stack.hasTagCompound() || NEIServerUtils.isItemTool(stack)) { + long key = getItemKey(stack); + this.itemHashToRecipeId.put(key, recipeId); + this.recipeIdToItemHash.put(recipeId, key); + this.itemHashToItemStack.put(key, stack); + } else { + final NBTTagCompound itemStackNBT = StackInfo.itemStackToNBT(stack, false); + final Fluid fluidKey = getFluidKey(stack); + + if (fluidKey != null) { + this.fluidToRecipeId.put(fluidKey, recipeId); + } + + this.itemStackToRecipeId.put(itemStackNBT, recipeId); + this.recipeIdToItemStack.put(recipeId, itemStackNBT); + } + + } + + private long getItemKey(ItemStack stack) { + return ((long) Item.getIdFromItem(stack.getItem()) << 32) | (stack.getItemDamage() & 0xFFFFFFFFL); + } + + private Fluid getFluidKey(ItemStack stack) { + final FluidStack fluid = StackInfo.getFluid(stack); + + if (fluid != null) { + return fluid.getFluid(); + } + + return null; + } + + public RecipeId getRecipeId(ItemStack stack) { + RecipeId recipeId = this.itemHashToRecipeId.get(getItemKey(stack)); + + if (recipeId == null) { + recipeId = this.itemStackToRecipeId.get(StackInfo.itemStackToNBT(stack, false)); + } + + if (recipeId == null) { + recipeId = this.fluidToRecipeId.get(getFluidKey(stack)); + } + + return recipeId; + } + + public ItemStack getItemStack(RecipeId recipeId) { + NBTTagCompound itemStackNBT = this.recipeIdToItemStack.get(recipeId); + + if (itemStackNBT == null) { + final Long key = this.recipeIdToItemHash.get(recipeId); + if (key != null) { + return this.itemHashToItemStack.get(key); + } + } + + if (itemStackNBT != null) { + return StackInfo.loadFromNBT(itemStackNBT); + } + + return null; + } + + public boolean contains(ItemStack stack) { + return getRecipeId(stack) != null; + } + + public List> getAllFavorites() { + final List> entries = new ArrayList<>(this.itemStackToRecipeId.entrySet()); + + for (Map.Entry entry : this.itemHashToRecipeId.entrySet()) { + final ItemStack stack = this.itemHashToItemStack.get(entry.getKey()); + if (stack != null) { + entries.add(new HashMap.SimpleEntry<>(StackInfo.itemStackToNBT(stack, false), entry.getValue())); + } + } + + return entries; + } +} diff --git a/src/main/resources/assets/nei/lang/en_US.lang b/src/main/resources/assets/nei/lang/en_US.lang index 1cb5af276..48a87a57c 100644 --- a/src/main/resources/assets/nei/lang/en_US.lang +++ b/src/main/resources/assets/nei/lang/en_US.lang @@ -266,6 +266,9 @@ nei.options.inventory.favorites.showRecipeTooltipInGui=Show Recipe Tooltips in R nei.options.inventory.favorites.showRecipeTooltipInGui.true=Yes nei.options.inventory.favorites.showRecipeTooltipInGui.false=No nei.options.inventory.favorites.depth=Depth +nei.options.inventory.favorites.generateSingleRecipeFavorites=Generate Single-Recipe Favorites +nei.options.inventory.favorites.generateSingleRecipeFavorites.true=Yes +nei.options.inventory.favorites.generateSingleRecipeFavorites.false=No nei.options.inventory.hotkeysHelpText=Show Hotkeys Help Text nei.options.inventory.hotkeysHelpText.true=Yes nei.options.inventory.hotkeysHelpText.false=No