Generate Single-Recipe Favorites (#863)

Co-authored-by: Martin Robertz <dream-master@gmx.net>
This commit is contained in:
slprimeandMartin Robertz authored and GitHub committed 2026-02-16 23:14:55 +01:00
1 parent 9c764e3682
commit f3b6be26df
7 files changed
+349 -70

No files matched your search

@@ -8,39 +8,141 @@ import java.io.InputStream;
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<NBTTagCompound> tools = new HashSet<>();
private static final Map<NBTTagCompound, RecipeId> items = new HashMap<>();
private static final Map<String, RecipeId> 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<ICraftingHandler> handlers = GuiCraftingRecipe.getCraftingHandlers("all");
final Map<FastKey, RecipeHandlerRef> autogeneratedRecipes = new HashMap<>();
final Set<FastKey> 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<RecipeHandlerRef, RecipeId> recipesCache = new HashMap<>();
for (Map.Entry<FastKey, RecipeHandlerRef> 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<PositionedStack> 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 class FavoriteRecipes {
final File defaultFavorites = configFavorites.exists() ? configFavorites : globalFavorites;
if (defaultFavorites.exists()) {
try {
if (favoriteFile.createNewFile()) {
InputStream src = new FileInputStream(defaultFavorites);
@@ -101,7 +202,9 @@ public class FavoriteRecipes {
}
final JsonParser parser = new JsonParser();
items.clear();
boolean hasErrors = false;
manualFavorites.clear();
for (String itemStr : itemStrings) {
@@ -121,70 +224,75 @@ public class FavoriteRecipes {
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;
}
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);
}
}
}
final RecipeId recipeId = manualFavorites.getRecipeId(stack);
return recipeId != null ? recipeId : autoFavorites.getRecipeId(stack);
}
return null;
}
public static ItemStack getFavorite(RecipeId recipeId) {
if (NEIClientConfig.favoritesEnabled()) {
final Optional<Map.Entry<NBTTagCompound, RecipeId>> result = items.entrySet().stream()
.filter(entry -> entry.getValue().equals(recipeId)).findAny();
public static ItemStack getManualFavorite(RecipeId recipeId) {
if (NEIClientConfig.favoritesEnabled() && recipeId != null) {
return manualFavorites.getItemStack(recipeId);
}
return null;
}
if (result.isPresent()) {
return StackInfo.loadFromNBT(result.get().getKey());
}
public static ItemStack getFavorite(RecipeId recipeId) {
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 class FavoriteRecipes {
}
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 class FavoriteRecipes {
final List<String> strings = new ArrayList<>();
for (Map.Entry<NBTTagCompound, RecipeId> entry : items.entrySet()) {
for (Map.Entry<NBTTagCompound, RecipeId> entry : manualFavorites.getAllFavorites()) {
try {
final JsonObject line = new JsonObject();
@@ -432,6 +432,7 @@ public class ItemList {
loadFinished = true;
FavoriteRecipes.loadAutoGeneratedRecipes();
SubsetWidget.updateHiddenItems();
ItemPanels.bookmarkPanel.load();
updateFilter.restart();
@@ -782,8 +782,7 @@ public class NEIClientConfig {
@Override
public boolean onClick(int button) {
super.onClick(button);
FavoriteRecipes.save();
FavoriteRecipes.load();
FavoriteRecipes.reload();
return true;
}
});
@@ -796,6 +795,18 @@ public class NEIClientConfig {
.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));
}
@@ -54,7 +54,7 @@ public class GuiFavoriteButton extends GuiRecipeButton {
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 class GuiFavoriteButton extends GuiRecipeButton {
@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 class GuiFavoriteButton extends GuiRecipeButton {
@Override
public List<String> handleTooltip(List<String> currenttip) {
currenttip.add(translate("recipe.favorite"));
return currenttip;
}
@@ -178,7 +177,7 @@ public class GuiFavoriteButton extends GuiRecipeButton {
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;
@@ -51,6 +51,24 @@ public class Recipe {
return new RecipeId(extractItem(result), handlerName, extractIngredients(ingredients));
}
public static RecipeId of(IRecipeHandler handler, int recipeIndex) {
final List<PositionedStack> 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<ItemStack> ingredients = new ArrayList<>();
@@ -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<NBTTagCompound, RecipeId> itemStackToRecipeId = new HashMap<>();
protected final Map<RecipeId, NBTTagCompound> recipeIdToItemStack = new HashMap<>();
protected final Map<Fluid, RecipeId> fluidToRecipeId = new IdentityHashMap<>();
protected final Map<Long, RecipeId> itemHashToRecipeId = new HashMap<>();
protected final Map<RecipeId, Long> recipeIdToItemHash = new HashMap<>();
protected final Map<Long, ItemStack> 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<Map.Entry<NBTTagCompound, RecipeId>> getAllFavorites() {
final List<Map.Entry<NBTTagCompound, RecipeId>> entries = new ArrayList<>(this.itemStackToRecipeId.entrySet());
for (Map.Entry<Long, RecipeId> 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;
}
}