package codechicken.nei; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; 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.Objects; import java.util.Set; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; 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 { 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; updateSubsetVisibility(); } 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; private static int revision = 0; static { API.addSubset("Favorites.Manual", manualFavorites::contains); API.addSubset("Favorites.Generated", stack -> FavoriteRecipes.autoFavorites.contains(stack)); } private FavoriteRecipes() {} public static void load() { String worldPath = "global"; if (NEIClientConfig.getBooleanSetting("inventory.favorites.worldSpecific")) { worldPath = NEIClientConfig.getWorldPath(); } final File dir = new File(CommonUtils.getMinecraftDir(), "saves/NEI/" + worldPath); if (!dir.exists()) { dir.mkdirs(); } favoriteFile = new File(dir, "favorites.ini"); if (!favoriteFile.exists()) { final File globalFavorites = new File(CommonUtils.getMinecraftDir(), "saves/NEI/global/favorites.ini"); final File configFavorites = new File(NEIClientConfig.configDir, "favorites.ini"); final File defaultFavorites = configFavorites.exists() ? configFavorites : globalFavorites; if (defaultFavorites.exists()) { try { if (favoriteFile.createNewFile()) { InputStream src = new FileInputStream(defaultFavorites); OutputStream dst = new FileOutputStream(favoriteFile); IOUtils.copy(src, dst); src.close(); dst.close(); } } catch (IOException e) {} } } loadData(); } private static void loadData() { if (favoriteFile == null || !favoriteFile.exists()) { return; } List itemStrings; try (FileInputStream reader = new FileInputStream(favoriteFile)) { NEIClientConfig.logger.info("Loading favorites from file {}", favoriteFile); itemStrings = IOUtils.readLines(reader, StandardCharsets.UTF_8); } catch (IOException e) { NEIClientConfig.logger.error("Failed to load favorites from file {}", favoriteFile, e); return; } final JsonParser parser = new JsonParser(); boolean hasErrors = false; manualFavorites.clear(); for (String itemStr : itemStrings) { if (itemStr.isEmpty() || itemStr.startsWith("#")) { continue; } try { JsonObject jsonObject = parser.parse(itemStr).getAsJsonObject(); if (jsonObject.get("item") != null && jsonObject.get("recipeId") != null) { final NBTTagCompound itemStackNBT = (NBTTagCompound) NBTJson.toNbt(jsonObject.get("item")); final RecipeId recipeId = RecipeId.of(jsonObject.getAsJsonObject("recipeId")); final ItemStack stack = StackInfo.loadFromNBT(itemStackNBT); if (recipeId == null || stack == null) { continue; } if (RecipeHandlerRef.of(recipeId) != null) { manualFavorites.add(stack, recipeId); } else { hasErrors = true; } } } 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(); } public static RecipeId getManualFavorite(ItemStack stack) { if (NEIClientConfig.favoritesEnabled() && stack != null) { return manualFavorites.getRecipeId(stack); } return null; } public static ItemStack getManualFavorite(RecipeId recipeId) { if (NEIClientConfig.favoritesEnabled() && recipeId != null) { return manualFavorites.getItemStack(recipeId); } return null; } public static RecipeId getFavorite(ItemStack stack) { if (NEIClientConfig.favoritesEnabled() && stack != null) { final RecipeId recipeId = manualFavorites.getRecipeId(stack); return recipeId != null ? recipeId : autoFavorites.getRecipeId(stack); } return null; } 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 manualFavorites.size() + autoFavorites.size(); } public static void reload() { save(); load(); } public static boolean containsManual(ItemStack stack) { return getManualFavorite(stack) != null; } public static boolean contains(ItemStack stack) { return getFavorite(stack) != null; } public static void setFavorite(ItemStack stack, RecipeId recipeId) { if (stack == null) return; manualFavorites.removeItemStack(stack); if (recipeId != null) { manualFavorites.removeRecipeId(recipeId); manualFavorites.add(stack, recipeId); } updateSubsetVisibility(); } public static void loadAutoGeneratedRecipes() { if (ItemList.loadFinished && NEIClientConfig.getBooleanSetting("inventory.favorites.generateSingleRecipeFavorites")) { singleRecipeFavoritesTask.restart(); } else { singleRecipeFavoritesTask.stop(); autoFavorites.clear(); updateSubsetVisibility(); } } protected static void updateSubsetVisibility() { SubsetWidget.updateHiddenItems(); FavoriteRecipes.revision++; } public static int getRevision() { return revision; } public static void save() { if (favoriteFile == null) { return; } final List strings = new ArrayList<>(); for (Map.Entry entry : manualFavorites.getAllFavorites()) { try { final JsonObject line = new JsonObject(); line.add("item", NBTJson.toJsonObject(entry.getKey())); line.add("recipeId", entry.getValue().toJsonObject()); strings.add(NBTJson.toJson(line)); } catch (JsonSyntaxException e) { NEIClientConfig.logger.error("Failed to stringify favorites ItemStack to json string"); } } try (FileOutputStream output = new FileOutputStream(favoriteFile)) { IOUtils.writeLines(strings, "\n", output, StandardCharsets.UTF_8); } catch (IOException e) { NEIClientConfig.logger.error("Failed to save favorites list to file {}", favoriteFile, e); } } }