From b0b0b7567ec0656c60f2f3e4730c0edace353fb7 Mon Sep 17 00:00:00 2001 From: Roman / Nea Date: Thu, 30 Dec 2021 14:37:02 +0100 Subject: Adding support for more recipe types and forge recipes (#40) * Foundations for support of different crafting recipe types. NeuRecipe is now a base class for a recipe which provides common concepts, such as inputs, outputs and a rendering task. GuiItemRecipe has been reworked to work with this new NeuRecipe. NeuManager now parses said recipes. This should be reworked to be a two step process (first register items, then register recipes). To keep compatibility with older repo versions, NeuRecipes are parse lenient and default to a crafting recipe. New recipes should be added in the `recipes` json field which is an array of json dictionaries, which have a `type` and other fields depending on the `type` of that recipe. This also adds support for having multiple recipes for a single item (e.g. uncrafting storage blocks). * Remove references in existing code * Recipe Generation * ring recipes * recipe generator v2 * quick forge * bugfixes and performance improvements * fix raw craft cost * reload hotm if you open the hotm tree inv * add myself to the changelog * replace quickforge formular with lookup table * do not crash anymore when opening recipes outside of skyblock * format coins differently * remove debug logs * change recipe generator so that it doesnt crash old versions --- .../notenoughupdates/ItemPriceInformation.java | 4 +- .../moulberry/notenoughupdates/NEUManager.java | 212 +++++++---------- .../notenoughupdates/NotEnoughUpdates.java | 2 + .../notenoughupdates/auction/APIManager.java | 138 ++++++----- .../notenoughupdates/commands/Commands.java | 2 +- .../notenoughupdates/miscgui/GuiItemRecipe.java | 257 +++++++++------------ .../notenoughupdates/overlays/CraftingOverlay.java | 44 ++-- .../notenoughupdates/recipes/CraftingRecipe.java | 141 +++++++++++ .../notenoughupdates/recipes/ForgeRecipe.java | 220 ++++++++++++++++++ .../notenoughupdates/recipes/Ingredient.java | 88 +++++++ .../notenoughupdates/recipes/NeuRecipe.java | 39 ++++ .../notenoughupdates/recipes/RecipeGenerator.java | 180 +++++++++++++++ .../notenoughupdates/recipes/RecipeSlot.java | 28 +++ .../moulberry/notenoughupdates/util/Debouncer.java | 34 +++ .../notenoughupdates/util/HotmInformation.java | 179 ++++++++++++++ .../notenoughupdates/util/HypixelApi.java | 36 ++- .../moulberry/notenoughupdates/util/Utils.java | 38 ++- .../notenoughupdates/textures/gui/forge_recipe.png | Bin 0 -> 889 bytes 18 files changed, 1261 insertions(+), 381 deletions(-) create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/recipes/CraftingRecipe.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/recipes/ForgeRecipe.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/recipes/Ingredient.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/recipes/NeuRecipe.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeGenerator.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeSlot.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/util/Debouncer.java create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/util/HotmInformation.java create mode 100644 src/main/resources/assets/notenoughupdates/textures/gui/forge_recipe.png (limited to 'src/main') diff --git a/src/main/java/io/github/moulberry/notenoughupdates/ItemPriceInformation.java b/src/main/java/io/github/moulberry/notenoughupdates/ItemPriceInformation.java index 5c26e3d6..e78c117b 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/ItemPriceInformation.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/ItemPriceInformation.java @@ -152,7 +152,7 @@ public class ItemPriceInformation { } break; case 4: - if (craftCost.fromRecipe) { + if (craftCost != null && craftCost.fromRecipe) { if ((int) craftCost.craftCost == 0) { continue; } @@ -224,7 +224,7 @@ public class ItemPriceInformation { } break; case 3: - if (craftCost.fromRecipe) { + if (craftCost != null && craftCost.fromRecipe) { if ((int) craftCost.craftCost == 0) { continue; } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java index 0f4e58bf..064b1fa7 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java @@ -1,14 +1,13 @@ package io.github.moulberry.notenoughupdates; -import com.google.common.collect.Lists; import com.google.gson.*; import io.github.moulberry.notenoughupdates.auction.APIManager; import io.github.moulberry.notenoughupdates.miscgui.GuiItemRecipe; import io.github.moulberry.notenoughupdates.overlays.CraftingOverlay; -import io.github.moulberry.notenoughupdates.util.Constants; -import io.github.moulberry.notenoughupdates.util.HypixelApi; -import io.github.moulberry.notenoughupdates.util.SBInfo; -import io.github.moulberry.notenoughupdates.util.Utils; +import io.github.moulberry.notenoughupdates.recipes.CraftingRecipe; +import io.github.moulberry.notenoughupdates.recipes.Ingredient; +import io.github.moulberry.notenoughupdates.recipes.NeuRecipe; +import io.github.moulberry.notenoughupdates.util.*; import net.minecraft.client.Minecraft; import net.minecraft.client.settings.KeyBinding; import net.minecraft.init.Blocks; @@ -17,8 +16,8 @@ import net.minecraft.inventory.ContainerChest; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.nbt.*; -import net.minecraft.network.play.client.C0DPacketCloseWindow; import net.minecraft.util.ResourceLocation; +import net.minecraftforge.fml.common.ProgressManager; import org.apache.commons.io.FileUtils; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.Display; @@ -68,18 +67,26 @@ public class NEUManager { private static String GIT_COMMITS_URL; - private final HashMap> usagesMap = new HashMap<>(); + // TODO: private final Map + + private final Set recipes = new HashSet<>(); + private final HashMap> recipesMap = new HashMap<>(); + private final HashMap> usagesMap = new HashMap<>(); public String latestRepoCommit = null; public File configLocation; public File repoLocation; public File configFile; + public HotmInformation hotm; public NEUManager(NotEnoughUpdates neu, File configLocation) { this.neu = neu; this.configLocation = configLocation; this.auctionManager = new APIManager(this); + this.hotm = new HotmInformation(neu); + + GIT_COMMITS_URL = neu.config.hidden.repoCommitsURL; gson = new GsonBuilder().setPrettyPrinting().create(); @@ -261,14 +268,17 @@ public class NEUManager { if (items.exists()) { File[] itemFiles = new File(repoLocation, "items").listFiles(); if (itemFiles != null) { + ProgressManager.ProgressBar bar = ProgressManager.push("Loading recipes", itemFiles.length); for (File f : itemFiles) { String internalname = f.getName().substring(0, f.getName().length() - 5); + bar.step(internalname); synchronized (itemMap) { if (!itemMap.containsKey(internalname)) { loadItem(internalname); } } } + ProgressManager.pop(bar); } } @@ -283,14 +293,17 @@ public class NEUManager { if (items.exists()) { File[] itemFiles = new File(repoLocation, "items").listFiles(); if (itemFiles != null) { + ProgressManager.ProgressBar bar = ProgressManager.push("Loading items", itemFiles.length); for (File f : itemFiles) { String internalname = f.getName().substring(0, f.getName().length() - 5); + bar.step(internalname); synchronized (itemMap) { if (!itemMap.containsKey(internalname)) { loadItem(internalname); } } } + ProgressManager.pop(bar); } } @@ -325,23 +338,17 @@ public class NEUManager { itemMap.put(internalName, json); if (json.has("recipe")) { - synchronized (usagesMap) { - JsonObject recipe = json.get("recipe").getAsJsonObject(); - - String[] x = {"1", "2", "3"}; - String[] y = {"A", "B", "C"}; - for (int i = 0; i < 9; i++) { - String name = y[i / 3] + x[i % 3]; - String itemS = recipe.get(name).getAsString(); - if (itemS != null && itemS.split(":").length == 2) { - itemS = itemS.split(":")[0]; - } - - if (!usagesMap.containsKey(itemS)) { - usagesMap.put(itemS, new HashSet<>()); - } - usagesMap.get(itemS).add(internalName); - } + JsonObject recipe = json.getAsJsonObject("recipe"); + NeuRecipe neuRecipe = NeuRecipe.parseRecipe(this, recipe, json); + if (neuRecipe != null) + registerNeuRecipe(neuRecipe); + } + if (json.has("recipes")) { + for (JsonElement element : json.getAsJsonArray("recipes")) { + JsonObject recipe = element.getAsJsonObject(); + NeuRecipe neuRecipe = NeuRecipe.parseRecipe(this, recipe, json); + if (neuRecipe != null) + registerNeuRecipe(neuRecipe); } } @@ -392,6 +399,24 @@ public class NEUManager { } } + public void registerNeuRecipe(NeuRecipe recipe) { + recipes.add(recipe); + for (Ingredient output : recipe.getOutputs()) { + recipesMap.computeIfAbsent(output.getInternalItemId(), ignored -> new HashSet<>()).add(recipe); + } + for (Ingredient input : recipe.getIngredients()) { + usagesMap.computeIfAbsent(input.getInternalItemId(), ignored -> new HashSet<>()).add(recipe); + } + } + + public Set getRecipesFor(String internalName) { + return recipesMap.getOrDefault(internalName, Collections.emptySet()); + } + + public Set getUsagesFor(String internalName) { + return usagesMap.getOrDefault(internalName, Collections.emptySet()); + } + /** * Searches a string for a query. This method is used to mimic the behaviour of the * more complex map-based search function. This method is used for the chest-item-search feature. @@ -820,27 +845,25 @@ public class NEUManager { ContainerChest container = null; if (Minecraft.getMinecraft().thePlayer.openContainer instanceof ContainerChest) container = (ContainerChest) Minecraft.getMinecraft().thePlayer.openContainer; - if (item.has("recipe") && container != null && container.getLowerChestInventory().getDisplayName().getUnformattedText().equals("Craft Item")) { - CraftingOverlay.updateItem(item); - } else if (item.has("useneucraft") && item.get("useneucraft").getAsBoolean()) { - displayGuiItemRecipe(item.get("internalname").getAsString(), ""); - } else if (item.has("clickcommand")) { - String clickcommand = item.get("clickcommand").getAsString(); - - if (clickcommand.equals("viewrecipe")) { - neu.sendChatMessage( - "/" + clickcommand + " " + - item.get("internalname").getAsString().split(";")[0]); - viewItemAttemptID = item.get("internalname").getAsString(); - viewItemAttemptTime = System.currentTimeMillis(); - } else if (clickcommand.equals("viewpotion")) { - neu.sendChatMessage( - "/" + clickcommand + " " + - item.get("internalname").getAsString().split(";")[0].toLowerCase()); - viewItemAttemptID = item.get("internalname").getAsString(); - viewItemAttemptTime = System.currentTimeMillis(); + String internalName = item.get("internalname").getAsString(); + Set recipesFor = getRecipesFor(internalName); + if (container != null && container.getLowerChestInventory().getDisplayName().getUnformattedText().equals("Craft Item")) { + Optional recipe = recipesFor.stream().filter(it -> it instanceof CraftingRecipe).findAny(); + if (recipe.isPresent()) { + CraftingOverlay.updateItem((CraftingRecipe) recipe.get()); + return; } } + if (!item.has("clickcommand")) return; + String clickcommand = item.get("clickcommand").getAsString(); + switch (clickcommand.intern()) { + case "viewrecipe": + displayGuiItemRecipe(internalName, null); + break; + case "viewoption": + neu.sendChatMessage("/viewpotion " + internalName.split(";")[0].toLowerCase(Locale.ROOT)); + } + displayGuiItemRecipe(internalName, ""); } /** @@ -920,90 +943,24 @@ public class NEUManager { loadItem(internalname); } - /** - * Constructs a GuiItemUsages from the recipe usage data (see #usagesMap) of a given item - */ public boolean displayGuiItemUsages(String internalName) { - List craftMatrices = new ArrayList<>(); - List results = new ArrayList<>(); - - if (!usagesMap.containsKey(internalName)) { - return false; - } - - for (String internalNameResult : usagesMap.get(internalName)) { - JsonObject item = getItemInformation().get(internalNameResult); - results.add(item); - - if (item != null && item.has("recipe")) { - JsonObject recipe = item.get("recipe").getAsJsonObject(); - - ItemStack[] craftMatrix = new ItemStack[9]; - - String[] x = {"1", "2", "3"}; - String[] y = {"A", "B", "C"}; - for (int i = 0; i < 9; i++) { - String name = y[i / 3] + x[i % 3]; - String itemS = recipe.get(name).getAsString(); - int count = 1; - if (itemS != null && itemS.split(":").length == 2) { - count = Integer.parseInt(itemS.split(":")[1]); - itemS = itemS.split(":")[0]; - } - JsonObject craft = getItemInformation().get(itemS); - if (craft != null) { - ItemStack stack = jsonToStack(craft); - stack.stackSize = count; - craftMatrix[i] = stack; - } - } - - craftMatrices.add(craftMatrix); - } - } - - if (craftMatrices.size() > 0) { - Minecraft.getMinecraft().displayGuiScreen(new GuiItemRecipe("Item Usages", craftMatrices, results, this)); - return true; - } - return false; + if (!usagesMap.containsKey(internalName)) return false; + Set usages = usagesMap.get(internalName); + if (usages.isEmpty()) return false; + Utils.sendCloseScreenPacket(); + Minecraft.getMinecraft().displayGuiScreen( + new GuiItemRecipe("Item Usages", new ArrayList<>(usages), this)); + return true; } - /** - * Constructs a GuiItemRecipeOld from the recipe data of a given item. - */ public boolean displayGuiItemRecipe(String internalName, String text) { - JsonObject item = getItemInformation().get(internalName); - if (item != null && item.has("recipe")) { - JsonObject recipe = item.get("recipe").getAsJsonObject(); - - ItemStack[] craftMatrix = new ItemStack[9]; - - String[] x = {"1", "2", "3"}; - String[] y = {"A", "B", "C"}; - for (int i = 0; i < 9; i++) { - String name = y[i / 3] + x[i % 3]; - String itemS = recipe.get(name).getAsString(); - int count = 1; - if (itemS != null && itemS.split(":").length == 2) { - count = Integer.parseInt(itemS.split(":")[1]); - itemS = itemS.split(":")[0]; - } - JsonObject craft = getItemInformation().get(itemS); - if (craft != null) { - ItemStack stack = jsonToStack(craft); - stack.stackSize = count; - craftMatrix[i] = stack; - } - } - - Minecraft.getMinecraft().thePlayer.sendQueue.addToSendQueue(new C0DPacketCloseWindow( - Minecraft.getMinecraft().thePlayer.openContainer.windowId)); - Minecraft.getMinecraft().displayGuiScreen(new GuiItemRecipe(text != null ? text : "Item Recipe", - Lists.newArrayList(craftMatrix), Lists.newArrayList(item), this)); - return true; - } - return false; + if (!recipesMap.containsKey(internalName)) return false; + Set recipes = recipesMap.get(internalName); + if (recipes.isEmpty()) return false; + Utils.sendCloseScreenPacket(); + Minecraft.getMinecraft().displayGuiScreen( + new GuiItemRecipe(text != null ? text : "Item Recipe", new ArrayList<>(recipes), this)); + return true; } /** @@ -1207,6 +1164,15 @@ public class NEUManager { writeJson(json, file); } + public JsonObject readJsonDefaultDir(String filename) throws IOException { + File f = new File(new File(repoLocation, "items"), filename); + if (f.exists() && f.isFile() && f.canRead()) + try (Reader reader = new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8)) { + return gson.fromJson(reader, JsonObject.class); + } // rethrow io exceptions + return null; + } + public TreeMap getItemInformation() { return itemMap; } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java b/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java index 23b89b44..3a6afef8 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java @@ -15,6 +15,7 @@ import io.github.moulberry.notenoughupdates.options.NEUConfig; import io.github.moulberry.notenoughupdates.overlays.FuelBar; import io.github.moulberry.notenoughupdates.overlays.OverlayManager; import io.github.moulberry.notenoughupdates.profileviewer.ProfileViewer; +import io.github.moulberry.notenoughupdates.recipes.RecipeGenerator; import io.github.moulberry.notenoughupdates.util.SBInfo; import io.github.moulberry.notenoughupdates.util.Utils; import io.github.moulberry.notenoughupdates.util.XPInformation; @@ -131,6 +132,7 @@ public class NotEnoughUpdates { MinecraftForge.EVENT_BUS.register(this); MinecraftForge.EVENT_BUS.register(new NEUEventListener(this)); + MinecraftForge.EVENT_BUS.register(new RecipeGenerator(this)); MinecraftForge.EVENT_BUS.register(CapeManager.getInstance()); //MinecraftForge.EVENT_BUS.register(new SBGamemodes()); MinecraftForge.EVENT_BUS.register(new EnchantingSolvers()); diff --git a/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java b/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java index 254ab5cf..d0b4a7f5 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java @@ -7,6 +7,8 @@ import io.github.moulberry.notenoughupdates.ItemPriceInformation; import io.github.moulberry.notenoughupdates.NEUManager; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.miscgui.GuiPriceGraph; +import io.github.moulberry.notenoughupdates.recipes.Ingredient; +import io.github.moulberry.notenoughupdates.recipes.NeuRecipe; import io.github.moulberry.notenoughupdates.util.Constants; import io.github.moulberry.notenoughupdates.util.Utils; import net.minecraft.client.Minecraft; @@ -760,87 +762,77 @@ public class APIManager { } public CraftInfo getCraftCost(String internalname) { - return getCraftCost(internalname, 0); + return getCraftCost(internalname, new HashSet<>()); } /** * Recursively calculates the cost of crafting an item from raw materials. */ - public CraftInfo getCraftCost(String internalname, int depth) { - if (craftCost.containsKey(internalname)) { - return craftCost.get(internalname); - } else { - CraftInfo ci = new CraftInfo(); - - ci.vanillaItem = isVanillaItem(internalname); - - JsonObject auctionInfo = getItemAuctionInfo(internalname); - float lowestBin = getLowestBin(internalname); - JsonObject bazaarInfo = getBazaarInfo(internalname); - - if (bazaarInfo != null && bazaarInfo.get("curr_buy") != null) { - float bazaarInstantBuyPrice = bazaarInfo.get("curr_buy").getAsFloat(); - ci.craftCost = bazaarInstantBuyPrice; - } - //Don't use auction prices for vanilla items cuz people like to transfer money, messing up the cost of vanilla items. - if (lowestBin > 0 && !ci.vanillaItem) { - if (ci.craftCost < 0 || lowestBin < ci.craftCost) { - ci.craftCost = lowestBin; - } - } else if (auctionInfo != null && !ci.vanillaItem) { - float auctionPrice = auctionInfo.get("price").getAsFloat() / auctionInfo.get("count").getAsFloat(); - if (ci.craftCost < 0 || auctionPrice < ci.craftCost) { - ci.craftCost = auctionPrice; - } - } - - if (depth > 16) { - craftCost.put(internalname, ci); - return ci; - } - - JsonObject item = manager.getItemInformation().get(internalname); - if (item != null && item.has("recipe")) { - float craftPrice = 0; - JsonObject recipe = item.get("recipe").getAsJsonObject(); - - String[] x = {"1", "2", "3"}; - String[] y = {"A", "B", "C"}; - for (int i = 0; i < 9; i++) { - String name = y[i / 3] + x[i % 3]; - String itemS = recipe.get(name).getAsString(); - if (itemS == null || itemS.length() == 0) continue; - - int count = 1; - if (itemS.split(":").length == 2) { - count = Integer.parseInt(itemS.split(":")[1]); - itemS = itemS.split(":")[0]; - } - if (itemS.equals(internalname)) { //if item is used a crafting component in its own recipe, return - craftCost.put(internalname, ci); - return ci; - } - - float compCost = getCraftCost(itemS, depth + 1).craftCost * count; - if (compCost < 0) { - //If it's a custom item without a cost, return - if (!getCraftCost(itemS).vanillaItem) { - craftCost.put(internalname, ci); - return ci; + private CraftInfo getCraftCost(String internalname, Set visited) { + if (craftCost.containsKey(internalname)) return craftCost.get(internalname); + if (visited.contains(internalname)) return null; + visited.add(internalname); + + + boolean vanillaItem = isVanillaItem(internalname); + float craftCost = Float.POSITIVE_INFINITY; + + JsonObject auctionInfo = getItemAuctionInfo(internalname); + float lowestBin = getLowestBin(internalname); + JsonObject bazaarInfo = getBazaarInfo(internalname); + + if (bazaarInfo != null && bazaarInfo.get("curr_buy") != null) { + craftCost = bazaarInfo.get("curr_buy").getAsFloat(); + } + //Don't use auction prices for vanilla items cuz people like to transfer money, messing up the cost of vanilla items. + if (!vanillaItem) { + if (lowestBin > 0) { + craftCost = Math.min(lowestBin, craftCost); + } else if (auctionInfo != null) { + float auctionPrice = auctionInfo.get("price").getAsFloat() / auctionInfo.get("count").getAsInt(); + craftCost = Math.min(auctionPrice, craftCost); + } + } + + Set recipes = manager.getRecipesFor(internalname); + boolean fromRecipe = false; + if (recipes != null) + RECIPE_ITER: + for (NeuRecipe recipe : recipes) { + float craftPrice = 0; + for (Ingredient i : recipe.getIngredients()) { + if (i.isCoins()) { + craftPrice += i.getCount(); + continue; + } + CraftInfo ingredientCraftCost = getCraftCost(i.getInternalItemId(), visited); + if (ingredientCraftCost == null) + continue RECIPE_ITER; // Skip recipes with items further up the chain + craftPrice += ingredientCraftCost.craftCost * i.getCount(); + } + int resultCount = 0; + for (Ingredient item : recipe.getOutputs()) + if (item.getInternalItemId().equals(internalname)) + resultCount += item.getCount(); + + if (resultCount == 0) + continue; + float craftPricePer = craftPrice / resultCount; + if (craftPricePer < craftCost) { + fromRecipe = true; + craftCost = craftPricePer; } - } else { - craftPrice += compCost; } - } - - if (ci.craftCost < 0 || craftPrice < ci.craftCost) { - ci.craftCost = craftPrice; - ci.fromRecipe = true; - } - } - craftCost.put(internalname, ci); - return ci; + visited.remove(internalname); + if (Float.isInfinite(craftCost)) { + return null; } + CraftInfo craftInfo = new CraftInfo(); + craftInfo.vanillaItem = vanillaItem; + craftInfo.craftCost = craftCost; + craftInfo.fromRecipe = fromRecipe; + this.craftCost.put(internalname, craftInfo); + return craftInfo; } /** diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/Commands.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/Commands.java index 910870d5..0e58a6bc 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/commands/Commands.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/Commands.java @@ -715,7 +715,7 @@ public class Commands { "Ok, this is actually the last message, use the command again and you'll crash I promise"}; private int devFailIndex = 0; - private static final List devTestUsers = new ArrayList<>(Arrays.asList("moulberry", "lucycoconut", "ironm00n", "ariyio", "throwpo", "dediamondpro")); + private static final List devTestUsers = new ArrayList<>(Arrays.asList("moulberry", "lucycoconut", "ironm00n", "ariyio", "throwpo", "lrg89", "dediamondpro")); SimpleCommand devTestCommand = new SimpleCommand("neudevtest", new SimpleCommand.ProcessCommandRunnable() { @Override public void processCommand(ICommandSender sender, String[] args) { diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscgui/GuiItemRecipe.java b/src/main/java/io/github/moulberry/notenoughupdates/miscgui/GuiItemRecipe.java index 210e1da7..e820378b 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/miscgui/GuiItemRecipe.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscgui/GuiItemRecipe.java @@ -1,33 +1,51 @@ package io.github.moulberry.notenoughupdates.miscgui; -import com.google.gson.JsonObject; +import com.google.common.collect.ImmutableList; import io.github.moulberry.notenoughupdates.NEUManager; +import io.github.moulberry.notenoughupdates.recipes.NeuRecipe; +import io.github.moulberry.notenoughupdates.recipes.RecipeSlot; import io.github.moulberry.notenoughupdates.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.item.ItemStack; +import net.minecraft.util.MathHelper; import net.minecraft.util.ResourceLocation; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import org.lwjgl.util.vector.Vector2f; import java.awt.*; import java.io.IOException; +import java.util.ArrayList; import java.util.List; public class GuiItemRecipe extends GuiScreen { - private static final ResourceLocation resourcePacksTexture = new ResourceLocation("textures/gui/resource_packs.png"); - private static final ResourceLocation craftingTableGuiTextures = new ResourceLocation("textures/gui/container/crafting_table.png"); + public static final ResourceLocation resourcePacksTexture = new ResourceLocation("textures/gui/resource_packs.png"); + + public static final int SLOT_SIZE = 16; + public static final int SLOT_SPACING = SLOT_SIZE + 2; + public static final int BUTTON_WIDTH = 7; + public static final int BUTTON_HEIGHT = 11; + public static final int BUTTON_POSITION_Y = 63; + public static final int BUTTON_POSITION_LEFT_X = 110; + public static final int BUTTON_POSITION_RIGHT_X = 147; + public static final int PAGE_STRING_X = 132; + public static final int PAGE_STRING_Y = 69; + public static final int TITLE_X = 28; + public static final int TITLE_Y = 6; + public static final int HOTBAR_SLOT_X = 8; + public static final int HOTBAR_SLOT_Y = 142; + public static final int PLAYER_INVENTORY_X = 8; + public static final int PLAYER_INVENTORY_Y = 84; - private final List craftMatrices; - private final List results; private int currentIndex = 0; private final String title; + private final List craftingRecipes; private final NEUManager manager; public int guiLeft = 0; @@ -35,159 +53,122 @@ public class GuiItemRecipe extends GuiScreen { public int xSize = 176; public int ySize = 166; - public GuiItemRecipe(String title, List craftMatrices, List results, NEUManager manager) { - this.craftMatrices = craftMatrices; - this.results = results; + public GuiItemRecipe(String title, List craftingRecipes, NEUManager manager) { + this.craftingRecipes = craftingRecipes; this.manager = manager; this.title = title; } - private String getCraftText() { - if(results.get(currentIndex).has("crafttext")) { - return results.get(currentIndex).get("crafttext").getAsString(); - } else { - return ""; - } + public NeuRecipe getCurrentRecipe() { + currentIndex = MathHelper.clamp_int(currentIndex, 0, craftingRecipes.size()); + return craftingRecipes.get(currentIndex); + } + + public boolean isWithinRect(int x, int y, int topLeftX, int topLeftY, int width, int height) { + return topLeftX <= x && x <= topLeftX + width + && topLeftY <= y && y <= topLeftY + height; + } + + private ImmutableList getAllRenderedSlots() { + return ImmutableList.builder() + .addAll(getPlayerInventory()) + .addAll(getCurrentRecipe().getSlots()).build(); } @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { drawDefaultBackground(); - - if(currentIndex < 0) { - currentIndex = 0; - } else if(currentIndex >= craftMatrices.size()) { - currentIndex = craftMatrices.size()-1; - } - FontRenderer fontRendererObj = Minecraft.getMinecraft().fontRendererObj; this.guiLeft = (width - this.xSize) / 2; this.guiTop = (height - this.ySize) / 2; GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - Minecraft.getMinecraft().getTextureManager().bindTexture(craftingTableGuiTextures); + + NeuRecipe currentRecipe = getCurrentRecipe(); + + Minecraft.getMinecraft().getTextureManager().bindTexture(currentRecipe.getBackground()); this.drawTexturedModalRect(guiLeft, guiTop, 0, 0, this.xSize, this.ySize); - List tooltipToRender = null; - for(int index=0; index <= 45; index++) { - Vector2f pos = getPositionForIndex(index); - Utils.drawItemStack(getStackForIndex(index), (int)pos.x, (int)pos.y); - - if(mouseX > pos.x && mouseX < pos.x+16) { - if(mouseY > pos.y && mouseY < pos.y+16) { - ItemStack stack = getStackForIndex(index); - if(stack != null) { - tooltipToRender = stack.getTooltip(Minecraft.getMinecraft().thePlayer, false); - } - } - } + currentRecipe.drawExtraBackground(this); + + List slots = getAllRenderedSlots(); + for (RecipeSlot slot : slots) { + Utils.drawItemStack(slot.getItemStack(), slot.getX(this), slot.getY(this)); } - if(craftMatrices.size() > 1) { - int guiX = mouseX - guiLeft; - int guiY = mouseY - guiTop; + if (craftingRecipes.size() > 1) drawArrows(mouseX, mouseY); - int buttonWidth = 7; - int buttonHeight = 11; + Utils.drawStringScaledMaxWidth(title, fontRendererObj, guiLeft + TITLE_X, guiTop + TITLE_Y, false, xSize - 38, 0x404040); - boolean leftSelected = false; - boolean rightSelected = false; + currentRecipe.drawExtraInfo(this); - if(guiY > + 63 && guiY < + 63 + buttonHeight) { - if(guiX > + 110 && guiX < 110 + buttonWidth) { - leftSelected = true; - } else if(guiX > 147 && guiX < 147 + buttonWidth) { - rightSelected = true; - } + for (RecipeSlot slot : slots) { + if (isWithinRect(mouseX, mouseY, slot.getX(this), slot.getY(this), SLOT_SIZE, SLOT_SIZE)) { + if (slot.getItemStack() == null) continue; + Utils.drawHoveringText(slot.getItemStack().getTooltip(Minecraft.getMinecraft().thePlayer, false), mouseX, mouseY, width, height, -1, fontRendererObj); } - - Minecraft.getMinecraft().getTextureManager().bindTexture(resourcePacksTexture); - //Left arrow - Utils.drawTexturedRect(guiLeft+110, guiTop+63, 7, 11, 34/256f, 48/256f, - 5/256f + (leftSelected ? 32/256f : 0), 27/256f + (leftSelected ? 32/256f : 0)); - //Right arrow - Utils.drawTexturedRect(guiLeft+147, guiTop+63, 7, 11, 10/256f, 24/256f, - 5/256f + (rightSelected ? 32/256f : 0), 27/256f + (rightSelected ? 32/256f : 0)); - GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); - - String str = (currentIndex+1)+"/"+craftMatrices.size(); - Utils.drawStringCenteredScaledMaxWidth(str, fontRendererObj, guiLeft+132, guiTop+69, - false, 24, Color.BLACK.getRGB()); } + currentRecipe.drawHoverInformation(this, mouseX, mouseY); + } - Utils.drawStringCenteredScaledMaxWidth(getCraftText(), fontRendererObj, guiLeft+132, guiTop+25, - false, 75, 4210752); + private void drawArrows(int mouseX, int mouseY) { + boolean leftSelected = isWithinRect(mouseX - guiLeft, mouseY - guiTop, BUTTON_POSITION_LEFT_X, BUTTON_POSITION_Y, BUTTON_WIDTH, BUTTON_HEIGHT); + boolean rightSelected = isWithinRect(mouseX - guiLeft, mouseY - guiTop, BUTTON_POSITION_RIGHT_X, BUTTON_POSITION_Y, BUTTON_WIDTH, BUTTON_HEIGHT); - Utils.drawStringScaledMaxWidth(title, fontRendererObj, guiLeft+28, guiTop+6, title.contains("\u00a7"), xSize-38, 4210752); + Minecraft.getMinecraft().getTextureManager().bindTexture(resourcePacksTexture); - if(tooltipToRender != null) { - Utils.drawHoveringText(tooltipToRender, mouseX, mouseY, width, height, -1, fontRendererObj); - } - } + Utils.drawTexturedRect(guiLeft + BUTTON_POSITION_LEFT_X, guiTop + BUTTON_POSITION_Y, BUTTON_WIDTH, BUTTON_HEIGHT, + 34 / 256f, 48 / 256f, + leftSelected ? 37 / 256f : 5 / 256f, leftSelected ? 59 / 256f : 27 / 256f + ); + Utils.drawTexturedRect(guiLeft + BUTTON_POSITION_RIGHT_X, guiTop + BUTTON_POSITION_Y, BUTTON_WIDTH, BUTTON_HEIGHT, + 10 / 256f, 24 / 256f, + rightSelected ? 37 / 256f : 5 / 256f, rightSelected ? 59 / 256f : 27 / 256f + ); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); - public ItemStack getStackForIndex(int index) { - if(index == 0) { - return manager.jsonToStack(results.get(currentIndex)); - } else if(index >= 1 && index <= 9) { - return craftMatrices.get(currentIndex)[index-1]; - } else { - return Minecraft.getMinecraft().thePlayer.inventory.getStackInSlot(index-10); - } + String selectedPage = (currentIndex + 1) + "/" + craftingRecipes.size(); + + Utils.drawStringCenteredScaledMaxWidth(selectedPage, fontRendererObj, + guiLeft + PAGE_STRING_X, guiTop + PAGE_STRING_Y, false, 24, Color.BLACK.getRGB()); } - public Vector2f getPositionForIndex(int index) { - //0 = result - //1-9 = craft matrix - //10-18 = hotbar - //19-45 = player inv - - if(index == 0) { - return new Vector2f(guiLeft+124, guiTop+35); - } else if(index >= 1 && index <= 9) { - index -= 1; - int x = index % 3; - int y = index / 3; - return new Vector2f(guiLeft+30 + x*18, guiTop+17 + y * 18); - } else if(index >= 10 && index <= 18) { - index -= 10; - return new Vector2f(guiLeft+8 + index*18, guiTop+142); - } else if(index >= 19 && index <= 45) { - index -= 19; - int x = index % 9; - int y = index / 9; - return new Vector2f(guiLeft+8 + x*18, guiTop+84 + y*18); + public List getPlayerInventory() { + List slots = new ArrayList<>(); + ItemStack[] inventory = Minecraft.getMinecraft().thePlayer.inventory.mainInventory; + int hotbarSize = InventoryPlayer.getHotbarSize(); + for (int i = 0; i < inventory.length; i++) { + ItemStack item = inventory[i]; + if (item == null || item.stackSize == 0) continue; + int row = i / hotbarSize; + int col = i % hotbarSize; + if (row == 0) + slots.add(new RecipeSlot(HOTBAR_SLOT_X + i * SLOT_SPACING, HOTBAR_SLOT_Y, item)); + else + slots.add(new RecipeSlot(PLAYER_INVENTORY_X + col * SLOT_SPACING, PLAYER_INVENTORY_Y + (row - 1) * SLOT_SPACING, item)); } - return null; + return slots; } @Override public void handleKeyboardInput() throws IOException { super.handleKeyboardInput(); - if(!Keyboard.getEventKeyState()) return; - ScaledResolution scaledResolution = new ScaledResolution(Minecraft.getMinecraft()); int width = scaledResolution.getScaledWidth(); int height = scaledResolution.getScaledHeight(); int mouseX = Mouse.getX() * width / Minecraft.getMinecraft().displayWidth; int mouseY = height - Mouse.getY() * height / Minecraft.getMinecraft().displayHeight - 1; - int keyPressed = Keyboard.getEventKey() == 0 ? Keyboard.getEventCharacter()+256 : Keyboard.getEventKey(); - for(int index=0; index <= 45; index++) { - Vector2f pos = getPositionForIndex(index); - if(mouseX > pos.x && mouseX < pos.x+16) { - if(mouseY > pos.y && mouseY < pos.y+16) { - ItemStack stack = getStackForIndex(index); - if(stack != null) { - if(keyPressed == manager.keybindViewRecipe.getKeyCode()) { - manager.displayGuiItemRecipe(manager.getInternalNameForItem(stack), ""); - } else if(keyPressed == manager.keybindViewUsages.getKeyCode()) { - manager.displayGuiItemUsages(manager.getInternalNameForItem(stack)); - } - } - return; + for (RecipeSlot slot : getAllRenderedSlots()) { + if (isWithinRect(mouseX, mouseY, slot.getX(this), slot.getY(this), SLOT_SIZE, SLOT_SIZE)) { + ItemStack itemStack = slot.getItemStack(); + if (keyPressed == manager.keybindViewRecipe.getKeyCode()) { // TODO: rework this so it doesnt skip recipe chains + manager.displayGuiItemRecipe(manager.getInternalNameForItem(itemStack), ""); + } else if (keyPressed == manager.keybindViewUsages.getKeyCode()) { + manager.displayGuiItemUsages(manager.getInternalNameForItem(itemStack)); } } } @@ -197,37 +178,25 @@ public class GuiItemRecipe extends GuiScreen { protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { super.mouseClicked(mouseX, mouseY, mouseButton); - int guiX = mouseX - guiLeft; - int guiY = mouseY - guiTop; - - int buttonWidth = 7; - int buttonHeight = 11; - - if(guiY > + 63 && guiY < + 63 + buttonHeight) { - if(guiX > + 110 && guiX < 110 + buttonWidth) { - currentIndex--; - Utils.playPressSound(); - return; - } else if(guiX > 147 && guiX < 147 + buttonWidth) { - currentIndex++; - Utils.playPressSound(); - return; - } + if (isWithinRect(mouseX - guiLeft, mouseY - guiTop, BUTTON_POSITION_LEFT_X, BUTTON_POSITION_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) { + currentIndex = currentIndex == 0 ? 0 : currentIndex - 1; + Utils.playPressSound(); + return; + } + + if (isWithinRect(mouseX - guiLeft, mouseY - guiTop, BUTTON_POSITION_RIGHT_X, BUTTON_POSITION_Y, BUTTON_WIDTH, BUTTON_HEIGHT)) { + currentIndex = currentIndex == craftingRecipes.size() - 1 ? currentIndex : currentIndex + 1; + Utils.playPressSound(); + return; } - for(int index=0; index <= 45; index++) { - Vector2f pos = getPositionForIndex(index); - if(mouseX > pos.x && mouseX < pos.x+16) { - if(mouseY > pos.y && mouseY < pos.y+16) { - ItemStack stack = getStackForIndex(index); - if(stack != null) { - if(mouseButton == 0) { - manager.displayGuiItemRecipe(manager.getInternalNameForItem(stack), ""); - } else if(mouseButton == 1) { - manager.displayGuiItemUsages(manager.getInternalNameForItem(stack)); - } - } - return; + for (RecipeSlot slot : getAllRenderedSlots()) { + if (isWithinRect(mouseX, mouseY, slot.getX(this), slot.getY(this), SLOT_SIZE, SLOT_SIZE)) { + ItemStack itemStack = slot.getItemStack(); + if (mouseButton == 0) { + manager.displayGuiItemRecipe(manager.getInternalNameForItem(itemStack), ""); + } else if (mouseButton == 1) { + manager.displayGuiItemUsages(manager.getInternalNameForItem(itemStack)); } } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/overlays/CraftingOverlay.java b/src/main/java/io/github/moulberry/notenoughupdates/overlays/CraftingOverlay.java index 532f3324..c0cbef0f 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/overlays/CraftingOverlay.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/overlays/CraftingOverlay.java @@ -3,6 +3,8 @@ package io.github.moulberry.notenoughupdates.overlays; import com.google.gson.JsonObject; import io.github.moulberry.notenoughupdates.NEUManager; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; +import io.github.moulberry.notenoughupdates.recipes.CraftingRecipe; +import io.github.moulberry.notenoughupdates.recipes.Ingredient; import io.github.moulberry.notenoughupdates.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; @@ -17,7 +19,7 @@ import org.lwjgl.input.Mouse; import java.util.List; public class CraftingOverlay { - private static ItemStack[] items = new ItemStack[9]; + private static final ItemStack[] items = new ItemStack[9]; private static final NEUManager manager = NotEnoughUpdates.INSTANCE.manager; public static boolean shouldRender = false; private static String text = null; @@ -56,34 +58,6 @@ public class CraftingOverlay { } } - public static void updateItem(JsonObject item) { - items = new ItemStack[9]; - text = null; - String[] x = {"1", "2", "3"}; - String[] y = {"A", "B", "C"}; - for (int i = 0; i < 9; i++) { - String name = y[i / 3] + x[i % 3]; - String itemS = item.getAsJsonObject("recipe").get(name).getAsString(); - if (itemS != null && !itemS.equals("")) { - int count = 1; - if (itemS.split(":").length == 2) { - count = Integer.parseInt(itemS.split(":")[1]); - itemS = itemS.split(":")[0]; - } - JsonObject craft = manager.getItemInformation().get(itemS); - if (craft != null) { - ItemStack stack = manager.jsonToStack(craft); - stack.stackSize = count; - items[i] = stack; - } - } - } - if (item.has("crafttext")) { - text = item.get("crafttext").getAsString(); - } - shouldRender = true; - } - public static void keyInput() { if (!Keyboard.getEventKeyState() || Keyboard.getEventKey() != Keyboard.KEY_U && Keyboard.getEventKey() != Keyboard.KEY_R) return; @@ -114,4 +88,16 @@ public class CraftingOverlay { } } } + + public static void updateItem(CraftingRecipe recipe) { + for (int i = 0; i < 9; i++) { + Ingredient ingredient = recipe.getInputs()[i]; + if (ingredient == null) { + items[i] = null; + } else { + items[i] = ingredient.getItemStack(); + } + } + text = recipe.getCraftText(); + } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/recipes/CraftingRecipe.java b/src/main/java/io/github/moulberry/notenoughupdates/recipes/CraftingRecipe.java new file mode 100644 index 00000000..00e70462 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/recipes/CraftingRecipe.java @@ -0,0 +1,141 @@ +package io.github.moulberry.notenoughupdates.recipes; + +import com.google.common.collect.Sets; +import com.google.gson.JsonObject; +import io.github.moulberry.notenoughupdates.NEUManager; +import io.github.moulberry.notenoughupdates.miscgui.GuiItemRecipe; +import io.github.moulberry.notenoughupdates.util.Utils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class CraftingRecipe implements NeuRecipe { + + public static final ResourceLocation BACKGROUND = new ResourceLocation("textures/gui/container/crafting_table.png"); + + private static final int EXTRA_STRING_X = 132; + private static final int EXTRA_STRING_Y = 25; + + private final NEUManager manager; + private final Ingredient[] inputs; + private final String extraText; + private final Ingredient outputIngredient; + private List slots; + + public CraftingRecipe(NEUManager manager, Ingredient[] inputs, Ingredient output, String extra) { + this.manager = manager; + this.inputs = inputs; + this.outputIngredient = output; + this.extraText = extra; + if (inputs.length != 9) + throw new IllegalArgumentException("Cannot construct crafting recipe with non standard crafting grid size"); + } + + @Override + public Set getIngredients() { + Set ingredients = Sets.newHashSet(inputs); + ingredients.remove(null); + return ingredients; + } + + @Override + public Set getOutputs() { + return Collections.singleton(getOutput()); + } + + public Ingredient getOutput() { + return outputIngredient; + } + + public Ingredient[] getInputs() { + return inputs; + } + + + @Override + public List getSlots() { + if (slots != null) return slots; + slots = new ArrayList<>(); + for (int x = 0; x < 3; x++) { + for (int y = 0; y < 3; y++) { + Ingredient input = inputs[x + y * 3]; + if (input == null) continue; + ItemStack item = input.getItemStack(); + if (item == null) continue; + slots.add(new RecipeSlot(30 + x * GuiItemRecipe.SLOT_SPACING, 17 + y * GuiItemRecipe.SLOT_SPACING, item)); + } + } + slots.add(new RecipeSlot(124, 35, outputIngredient.getItemStack())); + return slots; + } + + public String getCraftText() { + return extraText; + } + + @Override + public ResourceLocation getBackground() { + return BACKGROUND; + } + + @Override + public void drawExtraInfo(GuiItemRecipe gui) { + FontRenderer fontRenderer = Minecraft.getMinecraft().fontRendererObj; + + String craftingText = getCraftText(); + if (craftingText != null) + Utils.drawStringCenteredScaledMaxWidth(craftingText, fontRenderer, + gui.guiLeft + EXTRA_STRING_X, gui.guiTop + EXTRA_STRING_Y, false, 75, 0x404040); + } + + @Override + public JsonObject serialize() { + JsonObject object = new JsonObject(); + object.addProperty("type", "crafting"); + object.addProperty("count", outputIngredient.getCount()); + object.addProperty("overrideOutputId", outputIngredient.getInternalItemId()); + for (int i = 0; i < 9; i++) { + Ingredient ingredient = inputs[i]; + if (ingredient == null) continue; + String[] x = {"1", "2", "3"}; + String[] y = {"A", "B", "C"}; + String name = x[i / 3] + y[i % 3]; + object.addProperty(name, ingredient.serialize()); + } + if(extraText != null) + object.addProperty("crafttext", extraText); + return object; + } + + public static CraftingRecipe parseCraftingRecipe(NEUManager manager, JsonObject recipe, JsonObject outputItem) { + Ingredient[] craftMatrix = new Ingredient[9]; + + String[] x = {"1", "2", "3"}; + String[] y = {"A", "B", "C"}; + for (int i = 0; i < 9; i++) { + String name = y[i / 3] + x[i % 3]; + if (!recipe.has(name)) continue; + String item = recipe.get(name).getAsString(); + if (item == null || item.isEmpty()) continue; + craftMatrix[i] = new Ingredient(manager, item); + } + int resultCount = 1; + if (recipe.has("count")) + resultCount = recipe.get("count").getAsInt(); + String extra = null; + if (outputItem.has("crafttext")) + extra = outputItem.get("crafttext").getAsString(); + if (recipe.has("crafttext")) + extra = recipe.get("crafttext").getAsString(); + String outputItemId = outputItem.get("internalname").getAsString(); + if (recipe.has("overrideOutputId")) + outputItemId = recipe.get("overrideOutputId").getAsString(); + return new CraftingRecipe(manager, craftMatrix, new Ingredient(manager, outputItemId, resultCount), extra); + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/recipes/ForgeRecipe.java b/src/main/java/io/github/moulberry/notenoughupdates/recipes/ForgeRecipe.java new file mode 100644 index 00000000..5cbb4afe --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/recipes/ForgeRecipe.java @@ -0,0 +1,220 @@ +package io.github.moulberry.notenoughupdates.recipes; + +import com.google.common.collect.Sets; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import io.github.moulberry.notenoughupdates.NEUManager; +import io.github.moulberry.notenoughupdates.miscgui.GuiItemRecipe; +import io.github.moulberry.notenoughupdates.util.HotmInformation; +import io.github.moulberry.notenoughupdates.util.Utils; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; + +import java.util.*; + +public class ForgeRecipe implements NeuRecipe { + + private static final ResourceLocation BACKGROUND = new ResourceLocation("notenoughupdates", "textures/gui/forge_recipe.png"); + + private static final int SLOT_IMAGE_U = 176; + private static final int SLOT_IMAGE_V = 0; + private static final int SLOT_IMAGE_SIZE = 18; + private static final int SLOT_PADDING = 1; + private static final int EXTRA_INFO_MAX_WIDTH = 75; + public static final int EXTRA_INFO_X = 132; + public static final int EXTRA_INFO_Y = 25; + + public enum ForgeType { + REFINING, ITEM_FORGING + } + + private final NEUManager manager; + private final List inputs; + private final Ingredient output; + private final int hotmLevel; + private final int timeInSeconds; // TODO: quick forge + private List slots; + + public ForgeRecipe(NEUManager manager, List inputs, Ingredient output, int durationInSeconds, int hotmLevel) { + this.manager = manager; + this.inputs = inputs; + this.output = output; + this.hotmLevel = hotmLevel; + this.timeInSeconds = durationInSeconds; + } + + public List getInputs() { + return inputs; + } + + public Ingredient getOutput() { + return output; + } + + public int getHotmLevel() { + return hotmLevel; + } + + public int getTimeInSeconds() { + return timeInSeconds; + } + + @Override + public ResourceLocation getBackground() { + return BACKGROUND; + } + + @Override + public Set getIngredients() { + return Sets.newHashSet(inputs); + } + + @Override + public Set getOutputs() { + return Collections.singleton(output); + } + + @Override + public List getSlots() { + if (slots != null) return slots; + slots = new ArrayList<>(); + for (int i = 0; i < inputs.size(); i++) { + Ingredient input = inputs.get(i); + ItemStack itemStack = input.getItemStack(); + if (itemStack == null) continue; + int[] slotCoordinates = getSlotCoordinates(i, inputs.size()); + slots.add(new RecipeSlot(slotCoordinates[0], slotCoordinates[1], itemStack)); + } + slots.add(new RecipeSlot(124, 35, output.getItemStack())); + return slots; + } + + @Override + public void drawExtraBackground(GuiItemRecipe gui) { + Minecraft.getMinecraft().getTextureManager().bindTexture(BACKGROUND); + for (int i = 0; i < inputs.size(); i++) { + int[] slotCoordinates = getSlotCoordinates(i, inputs.size()); + gui.drawTexturedModalRect( + gui.guiLeft + slotCoordinates[0] - SLOT_PADDING, gui.guiTop + slotCoordinates[1] - SLOT_PADDING, + SLOT_IMAGE_U, SLOT_IMAGE_V, + SLOT_IMAGE_SIZE, SLOT_IMAGE_SIZE); + } + } + + @Override + public void drawExtraInfo(GuiItemRecipe gui) { + FontRenderer fontRenderer = Minecraft.getMinecraft().fontRendererObj; + if (timeInSeconds > 0) + Utils.drawStringCenteredScaledMaxWidth(formatDuration(timeInSeconds), fontRenderer, gui.guiLeft + EXTRA_INFO_X, gui.guiTop + EXTRA_INFO_Y, false, EXTRA_INFO_MAX_WIDTH, 0xff00ff); + } + + @Override + public void drawHoverInformation(GuiItemRecipe gui, int mouseX, int mouseY) { + manager.hotm.getInformationOnCurrentProfile().ifPresent(hotmTree -> { + if (timeInSeconds > 0 && gui.isWithinRect( + mouseX, mouseY, + gui.guiLeft + EXTRA_INFO_X - EXTRA_INFO_MAX_WIDTH / 2, + gui.guiTop + EXTRA_INFO_Y - 8, + EXTRA_INFO_MAX_WIDTH, 16 + )) { + int qf = hotmTree.getLevel("forge_time"); + int reducedTime = getReducedTime(qf); + if (qf > 0) { + + Utils.drawHoveringText(Arrays.asList(EnumChatFormatting.YELLOW + formatDuration(reducedTime) + " with Quick Forge (Level " + qf + ")"), mouseX, mouseY, gui.width, gui.height, 500, Minecraft.getMinecraft().fontRendererObj); + } + } + }); + } + + public int getReducedTime(int quickForgeUpgradeLevel) { + return HotmInformation.getQuickForgeMultiplier(quickForgeUpgradeLevel) * timeInSeconds / 1000; + } + + @Override + public JsonObject serialize() { + JsonObject object = new JsonObject(); + JsonArray ingredients = new JsonArray(); + for (Ingredient input : inputs) { + ingredients.add(new JsonPrimitive(input.serialize())); + } + object.addProperty("type", "forge"); + object.add("inputs", ingredients); + object.addProperty("count", output.getCount()); + object.addProperty("overrideOutputId", output.getInternalItemId()); + if (hotmLevel >= 0) + object.addProperty("hotmLevel", hotmLevel); + if (timeInSeconds >= 0) + object.addProperty("duration", timeInSeconds); + return object; + } + + static ForgeRecipe parseForgeRecipe(NEUManager manager, JsonObject recipe, JsonObject output) { + List ingredients = new ArrayList<>(); + for (JsonElement element : recipe.getAsJsonArray("inputs")) { + String ingredientString = element.getAsString(); + ingredients.add(new Ingredient(manager, ingredientString)); + } + String internalItemId = output.get("internalname").getAsString(); + if (recipe.has("overrideOutputId")) + internalItemId = recipe.get("overrideOutputId").getAsString(); + int resultCount = 1; + if (recipe.has("count")) { + resultCount = recipe.get("count").getAsInt(); + } + int duration = -1; + if (recipe.has("duration")) { + duration = recipe.get("duration").getAsInt(); + } + int hotmLevel = -1; + if (recipe.has("hotmLevel")) { + hotmLevel = recipe.get("hotmLevel").getAsInt(); + } + return new ForgeRecipe(manager, ingredients, new Ingredient(manager, internalItemId, resultCount), duration, hotmLevel); + } + + private static final int RECIPE_CENTER_X = 40; + private static final int RECIPE_CENTER_Y = 34; + private static final int SLOT_DISTANCE_FROM_CENTER = 22; + private static final int RECIPE_FALLBACK_X = 20; + private static final int RECIPE_FALLBACK_Y = 15; + + static int[] getSlotCoordinates(int slotNumber, int totalSlotCount) { + if (totalSlotCount > 6) { + return new int[]{ + RECIPE_FALLBACK_X + (slotNumber % 4) * GuiItemRecipe.SLOT_SPACING, + RECIPE_FALLBACK_Y + (slotNumber / 4) * GuiItemRecipe.SLOT_SPACING, + }; + } + if (totalSlotCount == 1) { + return new int[] { + RECIPE_CENTER_X - GuiItemRecipe.SLOT_SIZE / 2, + RECIPE_CENTER_Y - GuiItemRecipe.SLOT_SIZE / 2 + }; + } + double rad = Math.PI * 2 * slotNumber / totalSlotCount; + int x = (int) (Math.cos(rad) * SLOT_DISTANCE_FROM_CENTER); + int y = (int) (Math.sin(rad) * SLOT_DISTANCE_FROM_CENTER); + return new int[]{RECIPE_CENTER_X + x, RECIPE_CENTER_Y + y}; + } + + static String formatDuration(int seconds) { + int minutes = seconds / 60; + seconds %= 60; + int hours = minutes / 60; + minutes %= 60; + int days = hours / 24; + hours %= 24; + StringBuilder sB = new StringBuilder(); + if (days != 0) sB.append(days).append("d "); + if (hours != 0) sB.append(hours).append("h "); + if (minutes != 0) sB.append(minutes).append("m "); + if (seconds != 0) sB.append(seconds).append("s "); + return sB.substring(0, sB.length() - 1); + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/recipes/Ingredient.java b/src/main/java/io/github/moulberry/notenoughupdates/recipes/Ingredient.java new file mode 100644 index 00000000..d72c901f --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/recipes/Ingredient.java @@ -0,0 +1,88 @@ +package io.github.moulberry.notenoughupdates.recipes; + +import com.google.gson.JsonObject; +import io.github.moulberry.notenoughupdates.NEUManager; +import io.github.moulberry.notenoughupdates.util.Utils; +import net.minecraft.init.Items; +import net.minecraft.item.ItemStack; + +import java.text.NumberFormat; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +public class Ingredient { + + public static final String SKYBLOCK_COIN = "SKYBLOCK_COIN"; + private final int count; + private final String internalItemId; + private final NEUManager manager; + private ItemStack itemStack; + + public Ingredient(NEUManager manager, String ingredientIdentifier) { + this.manager = manager; + String[] parts = ingredientIdentifier.split(":"); + internalItemId = parts[0]; + if (parts.length == 2) { + count = Integer.parseInt(parts[1]); + } else if (parts.length == 1) { + count = 1; + } else { + throw new IllegalArgumentException("Could not parse ingredient " + ingredientIdentifier); + } + } + + public Ingredient(NEUManager manager, String internalItemId, int count) { + this.manager = manager; + this.count = count; + this.internalItemId = internalItemId; + } + + private Ingredient(NEUManager manager, int coinValue) { + this.manager = manager; + this.internalItemId = SKYBLOCK_COIN; + this.count = coinValue; + } + + public static Set mergeIngredients(Iterable ingredients) { + Map newIngredients = new HashMap<>(); + for (Ingredient i : ingredients) { + newIngredients.merge(i.getInternalItemId(), i, (a, b) -> new Ingredient(i.manager, i.internalItemId, a.count + b.count)); + } + return new HashSet<>(newIngredients.values()); + } + + public static Ingredient coinIngredient(NEUManager manager, int coins) { + return new Ingredient(manager, coins); + } + + public boolean isCoins() { + return "SKYBLOCK_COIN".equals(internalItemId); + } + + public int getCount() { + return count; + } + + public String getInternalItemId() { + return internalItemId; + } + + public ItemStack getItemStack() { + if (itemStack != null) return itemStack; + if(isCoins()) { + itemStack = new ItemStack(Items.gold_nugget); + itemStack.setStackDisplayName("\u00A7r\u00A76" + Utils.formatNumberWithDots(getCount()) + " Coins"); + return itemStack; + } + JsonObject itemInfo = manager.getItemInformation().get(internalItemId); + itemStack = manager.jsonToStack(itemInfo); + itemStack.stackSize = count; + return itemStack; + } + + public String serialize() { + return internalItemId + ":" + count; + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/recipes/NeuRecipe.java b/src/main/java/io/github/moulberry/notenoughupdates/recipes/NeuRecipe.java new file mode 100644 index 00000000..cfa091d5 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/recipes/NeuRecipe.java @@ -0,0 +1,39 @@ +package io.github.moulberry.notenoughupdates.recipes; + +import com.google.gson.JsonObject; +import io.github.moulberry.notenoughupdates.NEUManager; +import io.github.moulberry.notenoughupdates.miscgui.GuiItemRecipe; +import net.minecraft.util.ResourceLocation; + +import java.util.List; +import java.util.Set; + +public interface NeuRecipe { + Set getIngredients(); + + Set getOutputs(); + + List getSlots(); + + void drawExtraInfo(GuiItemRecipe gui); + + default void drawExtraBackground(GuiItemRecipe gui) { + } + + default void drawHoverInformation(GuiItemRecipe gui, int mouseX, int mouseY) { + } + + JsonObject serialize(); + + ResourceLocation getBackground(); + + static NeuRecipe parseRecipe(NEUManager manager, JsonObject recipe, JsonObject output) { + if (recipe.has("type")) { + switch (recipe.get("type").getAsString().intern()) { + case "forge": + return ForgeRecipe.parseForgeRecipe(manager, recipe, output); + } + } + return CraftingRecipe.parseCraftingRecipe(manager, recipe, output); + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeGenerator.java b/src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeGenerator.java new file mode 100644 index 00000000..6862de29 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeGenerator.java @@ -0,0 +1,180 @@ +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.inventory.ContainerChest; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ChatComponentText; +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.HashMap; +import java.util.List; +import java.util.Map; + +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.hidden.enableItemEditing) 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) { + p.addChatMessage(new ChatComponentText("" + EnumChatFormatting.DARK_RED + EnumChatFormatting.BOLD + "Could not parse recipe for this UI")); + } else { + p.addChatMessage(new ChatComponentText("" + EnumChatFormatting.GREEN + EnumChatFormatting.BOLD + "Parsed recipe:")); + p.addChatMessage(new ChatComponentText("" + EnumChatFormatting.AQUA + " Inputs:")); + for (Ingredient i : recipe.getInputs()) + p.addChatMessage(new ChatComponentText(" - " + EnumChatFormatting.AQUA + i.getInternalItemId() + " x " + i.getCount())); + p.addChatMessage(new ChatComponentText("" + EnumChatFormatting.AQUA + " Output: " + EnumChatFormatting.GOLD + recipe.getOutput().getInternalItemId() + " x " + recipe.getOutput().getCount())); + p.addChatMessage(new ChatComponentText("" + EnumChatFormatting.AQUA + " Time: " + EnumChatFormatting.GRAY + recipe.getTimeInSeconds() + " seconds (no QF) .")); + boolean saved = false; + try { + saved = saveRecipe(recipe); + } catch (IOException e) { + } + if (!saved) + p.addChatMessage(new ChatComponentText("" + + 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?")); + } + } + } + + public boolean saveRecipe(NeuRecipe recipe) throws IOException { + JsonObject recipeJson = recipe.serialize(); + for (Ingredient i : recipe.getOutputs()) { + if (i.isCoins()) continue; + JsonObject outputJson = neu.manager.readJsonDefaultDir(i.getInternalItemId() + ".json"); + if (outputJson == null) return false; + outputJson.addProperty("clickcommand", "viewrecipe"); + JsonArray array = new JsonArray(); + array.add(recipeJson); + outputJson.add("recipes", array); + neu.manager.writeJsonDefaultDir(outputJson, i.getInternalItemId() + ".json"); + neu.manager.loadItem(i.getInternalItemId()); + } + 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 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); + } + } + +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeSlot.java b/src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeSlot.java new file mode 100644 index 00000000..ec97e59a --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeSlot.java @@ -0,0 +1,28 @@ +package io.github.moulberry.notenoughupdates.recipes; + +import io.github.moulberry.notenoughupdates.miscgui.GuiItemRecipe; +import net.minecraft.item.ItemStack; + +public class RecipeSlot { + private final int x; + private final int y; + private final ItemStack itemStack; + + public RecipeSlot(int x, int y, ItemStack itemStack) { + this.x = x; + this.y = y; + this.itemStack = itemStack; + } + + public ItemStack getItemStack() { + return itemStack; + } + + public int getX(GuiItemRecipe recipe) { + return recipe.guiLeft + x; + } + + public int getY(GuiItemRecipe recipe) { + return recipe.guiTop + y; + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/Debouncer.java b/src/main/java/io/github/moulberry/notenoughupdates/util/Debouncer.java new file mode 100644 index 00000000..be42364a --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/Debouncer.java @@ -0,0 +1,34 @@ +package io.github.moulberry.notenoughupdates.util; + +/** + * This debouncer always triggers on the leading edge. + *

+ * Calling {@link #trigger} will only result in a truthy return value the first time it is called + * within {@link #getDelayInNanoSeconds()} nanoseconds. + */ +public class Debouncer { + private long lastPinged = 0L; + private final long delay; + + public Debouncer(long minimumDelayInNanoSeconds) { + this.delay = minimumDelayInNanoSeconds; + } + + public long getDelayInNanoSeconds() { + return delay; + } + + public synchronized long timePassed() { + // longs are technically not atomic reads since they use two 32 bit registers + // so, yes, this technically has to be synchronized + return System.nanoTime() - lastPinged; + } + + public synchronized boolean trigger() { + long newPingTime = System.nanoTime(); + long newDelay = newPingTime - lastPinged; + lastPinged = newPingTime; + return newDelay >= this.delay; + } + +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/HotmInformation.java b/src/main/java/io/github/moulberry/notenoughupdates/util/HotmInformation.java new file mode 100644 index 00000000..9ca53b5d --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/HotmInformation.java @@ -0,0 +1,179 @@ +package io.github.moulberry.notenoughupdates.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.github.moulberry.notenoughupdates.NotEnoughUpdates; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.inventory.GuiChest; +import net.minecraft.inventory.ContainerChest; +import net.minecraftforge.client.event.ClientChatReceivedEvent; +import net.minecraftforge.client.event.GuiOpenEvent; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +public class HotmInformation { + private final NotEnoughUpdates neu; + public static final int[] EXPERIENCE_FOR_HOTM_LEVEL = { + // Taken from the wiki: https://hypixel-skyblock.fandom.com/wiki/Heart_of_the_Mountain#Experience_for_Each_Tier + 0, 3000, 12000, 37000, 97000, 197000, 347000 + }; + public static final int[] QUICK_FORGE_MULTIPLIERS = { + 985, + 970, + 955, + 940, + 925, + 910, + 895, + 880, + 865, + 850, + 845, + 840, + 835, + 830, + 825, + 820, + 815, + 810, + 805, + 700 + }; + private final Map profiles = new ConcurrentHashMap<>(); + + public static class Tree { + private Map levels = new HashMap<>(); + private int totalMithrilPowder; + private int totalGemstonePowder; + private int hotmExp; + + public int getHotmExp() { + return hotmExp; + } + + public int getTotalGemstonePowder() { + return totalGemstonePowder; + } + + public int getTotalMithrilPowder() { + return totalMithrilPowder; + } + + public Set getAllUnlockedNodes() { + return levels.keySet(); + } + + public int getHotmLevel() { + for (int i = EXPERIENCE_FOR_HOTM_LEVEL.length - 1; i >= 0; i--) { + if (EXPERIENCE_FOR_HOTM_LEVEL[i] >= this.hotmExp) + return i; + } + return 0; + } + + public int getLevel(String node) { + return levels.getOrDefault(node, 0); + } + + } + + private CompletableFuture updateTask = CompletableFuture.completedFuture(null); + + private boolean shouldReloadSoon = false; + + public HotmInformation(NotEnoughUpdates neu) { + this.neu = neu; + MinecraftForge.EVENT_BUS.register(this); + } + + public Optional getInformationOn(String profile) { + if (profile == null) { + return Optional.empty(); + } + return Optional.ofNullable(this.profiles.get(profile)); + } + + public Optional getInformationOnCurrentProfile() { + return getInformationOn(neu.manager.getCurrentProfile()); + } + + + @SubscribeEvent + public synchronized void onLobbyJoin(WorldEvent.Load event) { + if (shouldReloadSoon) { + shouldReloadSoon = false; + requestUpdate(false); + } + } + + @SubscribeEvent + public synchronized void onGuiOpen(GuiOpenEvent event) { + if (event.gui instanceof GuiChest) { + String containerName = ((ContainerChest) ((GuiChest) event.gui).inventorySlots).getLowerChestInventory().getDisplayName().getUnformattedText(); + if (containerName.equals("Heart of the Mountain")) + shouldReloadSoon = true; + } + } + + @SubscribeEvent + public synchronized void onChat(ClientChatReceivedEvent event) { + if (event.message.getUnformattedText().equals("Welcome to Hypixel SkyBlock!")) + requestUpdate(false); + } + + public synchronized void requestUpdate(boolean force) { + if (updateTask.isDone() || force) { + updateTask = neu.manager.hypixelApi.getHypixelApiAsync(neu.config.apiKey.apiKey, "skyblock/profiles", new HashMap() {{ + put("uuid", Minecraft.getMinecraft().thePlayer.getUniqueID().toString().replace("-", "")); + }}).thenAccept(this::updateInformation); + } + } + + /* + * 1000 = 100% of the time left + * 700 = 70% of the time left + * */ + public static int getQuickForgeMultiplier(int level) { + if (level <= 0) return 1000; + if (level > 20) return -1; + return QUICK_FORGE_MULTIPLIERS[level - 1]; + } + + public void updateInformation(JsonObject entireApiResponse) { + if (!entireApiResponse.has("success") || !entireApiResponse.get("success").getAsBoolean()) return; + JsonArray profiles = entireApiResponse.getAsJsonArray("profiles"); + for (JsonElement element : profiles) { + JsonObject profile = element.getAsJsonObject(); + String profileName = profile.get("cute_name").getAsString(); + JsonObject player = profile.getAsJsonObject("members").getAsJsonObject(Minecraft.getMinecraft().thePlayer.getUniqueID().toString().replace("-", "")); + if (!player.has("mining_core")) + continue; + JsonObject miningCore = player.getAsJsonObject("mining_core"); + Tree tree = new Tree(); + JsonObject nodes = miningCore.getAsJsonObject("nodes"); + for (Map.Entry node : nodes.entrySet()) { + tree.levels.put(node.getKey(), node.getValue().getAsInt()); + } + if (miningCore.has("powder_mithril_total")) { + tree.totalMithrilPowder = miningCore.get("powder_mithril_total").getAsInt(); + } + if (miningCore.has("powder_gemstone_total")) { + tree.totalGemstonePowder = miningCore.get("powder_gemstone_total").getAsInt(); + } + if (miningCore.has("experience")) { + tree.hotmExp = miningCore.get("experience").getAsInt(); + } + this.profiles.put(profileName, tree); + } + } + +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/HypixelApi.java b/src/main/java/io/github/moulberry/notenoughupdates/util/HypixelApi.java index 923b962a..3d313f25 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/HypixelApi.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/HypixelApi.java @@ -6,12 +6,15 @@ import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import org.apache.commons.io.IOUtils; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.URL; import java.net.URLConnection; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; @@ -27,12 +30,17 @@ public class HypixelApi { private final String[] myApiURLs = {"https://moulberry.codes/"};//, "http://moulberry.codes/", "http://51.79.51.21/"};//, "http://51.75.78.252/" }; private final Integer[] myApiSuccesses = {0, 0, 0, 0}; + public CompletableFuture getHypixelApiAsync(String apiKey, String method, HashMap args) { + return getApiAsync(generateApiUrl(apiKey, method, args)); + } + public void getHypixelApiAsync(String apiKey, String method, HashMap args, Consumer consumer) { - getHypixelApiAsync(apiKey, method, args, consumer, () -> {}); + getHypixelApiAsync(apiKey, method, args, consumer, () -> { + }); } public void getHypixelApiAsync(String apiKey, String method, HashMap args, Consumer consumer, Runnable error) { - getApiAsync(generateApiUrl(apiKey != null ? apiKey.trim() : null, method, args), consumer, error); + getApiAsync(generateApiUrl(apiKey, method, args), consumer, error); } private String getMyApiURL() { @@ -61,6 +69,18 @@ public class HypixelApi { } } + public CompletableFuture getApiAsync(String urlS) { + CompletableFuture result = new CompletableFuture<>(); + es.submit(() -> { + try { + result.complete(getApiSync(urlS)); + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + return result; + } + public void getApiAsync(String urlS, Consumer consumer, Runnable error) { es.submit(() -> { try { @@ -134,16 +154,22 @@ public class HypixelApi { } public String generateApiUrl(String apiKey, String method, HashMap args) { - StringBuilder url = new StringBuilder("https://api.hypixel.net/" + method + (apiKey != null ? ("?key=" + apiKey.replace(" ", "")) : "")); + if (apiKey != null) + args.put("key", apiKey.trim().replace("-", "")); + StringBuilder url = new StringBuilder("https://api.hypixel.net/" + method); boolean first = true; for (Map.Entry entry : args.entrySet()) { - if (first && apiKey == null) { + if (first) { url.append("?"); first = false; } else { url.append("&"); } - url.append(entry.getKey()).append("=").append(entry.getValue()); + try { + url.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8.name())).append("=") + .append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name())); + } catch (UnsupportedEncodingException e) { + } } return url.toString(); } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/Utils.java b/src/main/java/io/github/moulberry/notenoughupdates/util/Utils.java index dc301db2..4a3e6400 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/Utils.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/Utils.java @@ -7,6 +7,7 @@ import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.miscfeatures.SlotLocking; import net.minecraft.client.Minecraft; import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.gui.inventory.GuiContainer; @@ -28,6 +29,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; +import net.minecraft.network.play.client.C0DPacketCloseWindow; import net.minecraft.util.*; import net.minecraftforge.fml.common.Loader; import org.lwjgl.BufferUtils; @@ -44,8 +46,8 @@ import java.lang.reflect.Method; import java.nio.FloatBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.util.*; import java.util.List; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -472,7 +474,6 @@ public class Utils { //EnumChatFormatting.AQUA+EnumChatFormatting.BOLD.toString()+"DIVINE", }; - public static final HashMap rarityArrMap = new HashMap() {{ put("COMMON", rarityArrC[0]); put("UNCOMMON", rarityArrC[1]); @@ -1362,7 +1363,6 @@ public class Utils { return endsIn; } - public static void drawLine(float sx, float sy, float ex, float ey, int width, int color) { float f = (float) (color >> 24 & 255) / 255.0F; float f1 = (float) (color >> 16 & 255) / 255.0F; @@ -1395,7 +1395,7 @@ public class Utils { } public static void drawTexturedQuad(float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, - float uMin, float uMax, float vMin, float vMax, int filter) { + float uMin, float uMax, float vMin, float vMax, int filter) { GlStateManager.enableTexture2D(); GlStateManager.enableBlend(); GlStateManager.tryBlendFuncSeparate(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA, GL11.GL_ONE, GL11.GL_ONE_MINUS_SRC_ALPHA); @@ -1426,4 +1426,34 @@ public class Utils { GlStateManager.disableBlend(); } + + public static boolean sendCloseScreenPacket() { + EntityPlayerSP thePlayer = Minecraft.getMinecraft().thePlayer; + if (thePlayer.openContainer == null) return false; + thePlayer.sendQueue.addToSendQueue(new C0DPacketCloseWindow( + thePlayer.openContainer.windowId)); + return true; + } + + public static String formatNumberWithDots(long number) { + if (number == 0) + return "0"; + String work = ""; + boolean isNegative = false; + if (number < 0) { + isNegative = true; + number = -number; + } + while (number != 0) { + work = String.format("%03d.%s", number % 1000, work); + number /= 1000; + } + work = work.substring(0, work.length() - 1); + while (work.startsWith("0")) + work = work.substring(1); + if (isNegative) + return "-" + work; + return work; + } + } diff --git a/src/main/resources/assets/notenoughupdates/textures/gui/forge_recipe.png b/src/main/resources/assets/notenoughupdates/textures/gui/forge_recipe.png new file mode 100644 index 00000000..2c3d2eb1 Binary files /dev/null and b/src/main/resources/assets/notenoughupdates/textures/gui/forge_recipe.png differ -- cgit