/* * Copyright (C) 2022 NotEnoughUpdates contributors * * This file is part of NotEnoughUpdates. * * NotEnoughUpdates is free software: you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * NotEnoughUpdates is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with NotEnoughUpdates. If not, see . */ package io.github.moulberry.notenoughupdates.recipes; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.util.Debouncer; import io.github.moulberry.notenoughupdates.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.inventory.GuiChest; import net.minecraft.init.Items; import net.minecraft.inventory.ContainerChest; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.EnumChatFormatting; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import org.lwjgl.input.Keyboard; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class RecipeGenerator { public static final String DURATION = "Duration: "; public static final String COINS_SUFFIX = " Coins"; private final NotEnoughUpdates neu; private final Map savedForgingDurations = new HashMap<>(); private final Debouncer debouncer = new Debouncer(1000 * 1000 * 50 /* 50 ms */); private final Debouncer durationDebouncer = new Debouncer(1000 * 1000 * 500); public RecipeGenerator(NotEnoughUpdates neu) { this.neu = neu; } @SubscribeEvent public void onTick(TickEvent event) { if (!neu.config.apiData.repositoryEditing) return; GuiScreen currentScreen = Minecraft.getMinecraft().currentScreen; if (currentScreen == null) return; if (!(currentScreen instanceof GuiChest)) return; analyzeUI((GuiChest) currentScreen); } private boolean shouldSaveRecipe() { return Keyboard.isKeyDown(Keyboard.KEY_O) && debouncer.trigger(); } public void analyzeUI(GuiChest gui) { ContainerChest container = (ContainerChest) gui.inventorySlots; IInventory menu = container.getLowerChestInventory(); String uiTitle = menu.getDisplayName().getUnformattedText(); EntityPlayerSP p = Minecraft.getMinecraft().thePlayer; if (uiTitle.startsWith("Item Casting") || uiTitle.startsWith("Refine")) { if (durationDebouncer.trigger()) parseAllForgeItemMetadata(menu); } boolean saveRecipe = shouldSaveRecipe(); if (uiTitle.equals("Confirm Process") && saveRecipe) { ForgeRecipe recipe = parseSingleForgeRecipe(menu); if (recipe == null) { Utils.addChatMessage( "" + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + "Could not parse recipe for this UI"); } else { Utils.addChatMessage("" + EnumChatFormatting.GREEN + EnumChatFormatting.BOLD + "Parsed recipe:"); Utils.addChatMessage("" + EnumChatFormatting.AQUA + " Inputs:"); for (Ingredient i : recipe.getInputs()) Utils.addChatMessage(" - " + EnumChatFormatting.AQUA + i.getInternalItemId() + " x " + i.getCount()); Utils.addChatMessage("" + EnumChatFormatting.AQUA + " Output: " + EnumChatFormatting.GOLD + recipe.getOutput().getInternalItemId() + " x " + recipe.getOutput().getCount()); Utils.addChatMessage( "" + EnumChatFormatting.AQUA + " Time: " + EnumChatFormatting.GRAY + recipe.getTimeInSeconds() + " seconds (no QF) ."); boolean saved = false; try { saved = saveRecipes(recipe.getOutput().getInternalItemId(), Collections.singletonList(recipe)); } catch (IOException ignored) { } if (!saved) Utils.addChatMessage( "" + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + EnumChatFormatting.OBFUSCATED + "#" + EnumChatFormatting.RESET + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + " ERROR " + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + EnumChatFormatting.OBFUSCATED + "#" + EnumChatFormatting.RESET + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + " Failed to save recipe. Does the item already exist?"); } } if (saveRecipe) attemptToSaveBestiary(menu); } private List getLore(ItemStack item) { NBTTagList loreTag = item.getTagCompound().getCompoundTag("display").getTagList("Lore", 8); List loreList = new ArrayList<>(); for (int i = 0; i < loreTag.tagCount(); i++) { loreList.add(loreTag.getStringTagAt(i)); } return loreList; } // §8[§7Lv1§8] §fZombie Villager private static final Pattern MOB_DISPLAY_NAME_PATTERN = Pattern.compile("^§8\\[§7Lv(?\\d+)§8] (?.*)$"); // §7Coins per Kill: §61 // §7Combat Exp: §3120 // §8 ■ §7§5Skeleton Grunt Helmet §8(§a5%§8) // §8 ■ §7§fRotten Flesh // §8 ■ Dragon Essence §8x3-5 private static final Pattern LORE_PATTERN = Pattern.compile("^(?:" + "§7Coins per Kill: §6(?[,\\d]+)|" + "§7Combat Exp: §3(?[,\\d]+)|" + "§7XP Orbs: §3(?[,\\d]+)|" + "§8 ■ (?:§7)?(?(?:§.)?.+?)(?: §8\\(§a(?[\\d.<]+%)§8\\)| §8(?x.*))?|" + "§7Kills: §a[,\\d]+|" + "§.[a-zA-Z]+ Loot|" + "§7Deaths: §a[,\\d]+|" + " §8■ (?§c\\?\\?\\?)|" + "" + ")$"); private void attemptToSaveBestiary(IInventory menu) { if (!menu.getDisplayName().getUnformattedText().contains("➜")) return; ItemStack backArrow = menu.getStackInSlot(48); if (backArrow == null || backArrow.getItem() != Items.arrow) return; if (!getLore(backArrow).stream().anyMatch(it -> it.startsWith("§7To Bestiary ➜"))) return; List recipes = new ArrayList<>(); String internalMobName = menu.getDisplayName().getUnformattedText().split("➜")[1].toUpperCase(Locale.ROOT).trim() + "_MONSTER"; for (int i = 9; i < 44; i++) { ItemStack mobStack = menu.getStackInSlot(i); if (mobStack == null || mobStack.getItem() != Items.skull) continue; Matcher matcher = MOB_DISPLAY_NAME_PATTERN.matcher(mobStack.getDisplayName()); if (!matcher.matches()) continue; String name = matcher.group("name"); int level = parseIntIgnoringCommas(matcher.group("level")); List mobLore = getLore(mobStack); int coins = 0, xp = 0, combatXp = 0; List drops = new ArrayList<>(); for (String loreLine : mobLore) { Matcher loreMatcher = LORE_PATTERN.matcher(loreLine); if (!loreMatcher.matches()) { Utils.addChatMessage("[WARNING] Unknown lore line: " + loreLine); continue; } if (loreMatcher.group("coins") != null) coins = parseIntIgnoringCommas(loreMatcher.group("coins")); if (loreMatcher.group("combatxp") != null) combatXp = parseIntIgnoringCommas(loreMatcher.group("combatxp")); if (loreMatcher.group("xp") != null) xp = parseIntIgnoringCommas(loreMatcher.group("xp")); if (loreMatcher.group("dropName") != null) { String dropName = loreMatcher.group("dropName"); List possibleItems = neu.manager.getItemInformation().values().stream().filter(it -> it.get( "displayname").getAsString().equals(dropName)).collect(Collectors.toList()); if (possibleItems.size() != 1) { Utils.addChatMessage("[WARNING] Could not parse drop, ambiguous or missing item information: " + loreLine); continue; } Ingredient item = new Ingredient(neu.manager, possibleItems.get(0).get("internalname").getAsString()); String chance = loreMatcher.group("dropChances") != null ? loreMatcher.group("dropChances") : loreMatcher.group("dropCount"); drops.add(new MobLootRecipe.MobDrop(item, chance, new ArrayList<>(), Collections.emptyList())); } if (loreMatcher.group("missing") != null) { Utils.addChatMessage("[WARNING] You are missing Bestiary levels for drop: " + loreLine); } } recipes.add(new MobLootRecipe( new Ingredient(neu.manager, internalMobName, 1), drops, level, coins, xp, combatXp, name, null, new ArrayList<>(), "unknown" )); } boolean saved = false; try { saved = saveRecipes(internalMobName, recipes); } catch (IOException ignored) { } if (!saved) Utils.addChatMessage( "" + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + EnumChatFormatting.OBFUSCATED + "#" + EnumChatFormatting.RESET + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + " ERROR " + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + EnumChatFormatting.OBFUSCATED + "#" + EnumChatFormatting.RESET + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + " Failed to save recipe. Does the item already exist?"); // TODO: MERGE CODE OVER } private int parseIntIgnoringCommas(String text) { return Integer.parseInt(text.replace(",", "")); } /*{ id: "minecraft:skull", Count: 1b, tag: { overrideMeta: 1b, SkullOwner: { Id: "2005daad-730b-363c-abae-e6f3830816fb", Properties: { textures: [{ Value: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTZjMGIzNmQ1M2ZmZjY5YTQ5YzdkNmYzOTMyZjJiMGZlOTQ4ZTAzMjIyNmQ1ZTgwNDVlYzU4NDA4YTM2ZTk1MSJ9fX0=" }] } }, display: { Lore: ["§7Coins per Kill: §610", "§7Combat Exp: §340", "§7XP Orbs: §312", "", "§7Kills: §a990", "§7Deaths: §a2", "", "§fCommon Loot", "§8 ■ §7§fEnder Pearl §8x1-3", "", "§9Rare Loot", "§8 ■ §7§aEnchanted Ender Pearl §8(§a1%§8)", "", "§6Legendary Loot", "§8 ■ §7§7[Lvl 1] §aEnderman §8(§a0.02%§8)", "§8 ■ §7§7[Lvl 1] §fEnderman §8(§a0.05%§8)", "§8 ■ §7§5Ender Helmet §8(§a0.1%§8)", "§8 ■ §7§5Ender Boots §8(§a0.1%§8)", "§8 ■ §7§5Ender Leggings §8(§a0.1%§8)", "§8 ■ §7§5Ender Chestplate §8(§a0.1%§8)", "", "§dRNGesus Loot", " §8■ §c???"], Name: "§8[§7Lv42§8] §fEnderman" }, AttributeModifiers: [] }, Damage: 3s }*/ public boolean saveRecipes(String relevantItem, List recipes) throws IOException { relevantItem = relevantItem.replace(" ", "_"); JsonObject outputJson = neu.manager.readJsonDefaultDir(relevantItem + ".json"); if (outputJson == null) return false; outputJson.addProperty("clickcommand", "viewrecipe"); JsonArray array = new JsonArray(); for (NeuRecipe recipe : recipes) { array.add(recipe.serialize()); } outputJson.add("recipes", array); neu.manager.writeJsonDefaultDir(outputJson, relevantItem + ".json"); neu.manager.loadItem(relevantItem); return true; } public ForgeRecipe parseSingleForgeRecipe(IInventory chest) { int durationInSeconds = -1; List inputs = new ArrayList<>(); Ingredient output = null; for (int i = 0; i < chest.getSizeInventory(); i++) { int col = i % 9; ItemStack itemStack = chest.getStackInSlot(i); if (itemStack == null) continue; String name = Utils.cleanColour(itemStack.getDisplayName()); String internalId = neu.manager.getInternalNameForItem(itemStack); Ingredient ingredient = null; if (itemStack.getDisplayName().endsWith(COINS_SUFFIX)) { int coinCost = Integer.parseInt( name.substring(0, name.length() - COINS_SUFFIX.length()) .replace(",", "")); ingredient = Ingredient.coinIngredient(neu.manager, coinCost); } else if (internalId != null) { ingredient = new Ingredient(neu.manager, internalId, itemStack.stackSize); } if (ingredient == null) continue; if (col < 4) { inputs.add(ingredient); } else { output = ingredient; } } if (output == null || inputs.isEmpty()) return null; if (savedForgingDurations.containsKey(output.getInternalItemId())) durationInSeconds = parseDuration(savedForgingDurations.get(output.getInternalItemId())); return new ForgeRecipe( neu.manager, new ArrayList<>(Ingredient.mergeIngredients(inputs)), output, durationInSeconds, -1 ); } private static final Map durationSuffixLengthMap = new HashMap() {{ put('d', 60 * 60 * 24); put('h', 60 * 60); put('m', 60); put('s', 1); }}; public int parseDuration(String durationString) { String[] parts = durationString.split(" "); int timeInSeconds = 0; for (String part : parts) { char signifier = part.charAt(part.length() - 1); int value = Integer.parseInt(part.substring(0, part.length() - 1)); if (!durationSuffixLengthMap.containsKey(signifier)) { return -1; } timeInSeconds += value * durationSuffixLengthMap.get(signifier); } return timeInSeconds; } private void parseAllForgeItemMetadata(IInventory chest) { for (int i = 0; i < chest.getSizeInventory(); i++) { ItemStack stack = chest.getStackInSlot(i); if (stack == null) continue; String internalName = neu.manager.getInternalNameForItem(stack); if (internalName == null) continue; List tooltip = stack.getTooltip(Minecraft.getMinecraft().thePlayer, false); String durationInfo = null; for (String s : tooltip) { String info = Utils.cleanColour(s); if (info.startsWith(DURATION)) { durationInfo = info.substring(DURATION.length()); } } if (durationInfo != null) savedForgingDurations.put(internalName, durationInfo); } } }