aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoman / Nea <roman.graef@gmail.com>2021-12-30 14:37:02 +0100
committerGitHub <noreply@github.com>2021-12-30 14:37:02 +0100
commitb0b0b7567ec0656c60f2f3e4730c0edace353fb7 (patch)
tree0e2b23fe4c52b0242cefc8caee9ed887797542b2
parentfc642887639d1918d68f4706e27ea59605a16fcb (diff)
downloadNotEnoughUpdates-b0b0b7567ec0656c60f2f3e4730c0edace353fb7.tar.gz
NotEnoughUpdates-b0b0b7567ec0656c60f2f3e4730c0edace353fb7.tar.bz2
NotEnoughUpdates-b0b0b7567ec0656c60f2f3e4730c0edace353fb7.zip
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
-rw-r--r--Update Notes/2.1.md2
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/ItemPriceInformation.java4
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java212
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NotEnoughUpdates.java2
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/auction/APIManager.java138
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/commands/Commands.java2
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/miscgui/GuiItemRecipe.java257
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/overlays/CraftingOverlay.java44
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/recipes/CraftingRecipe.java141
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/recipes/ForgeRecipe.java220
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/recipes/Ingredient.java88
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/recipes/NeuRecipe.java39
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeGenerator.java180
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/recipes/RecipeSlot.java28
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/Debouncer.java34
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/HotmInformation.java179
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/HypixelApi.java36
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/util/Utils.java38
-rw-r--r--src/main/resources/assets/notenoughupdates/textures/gui/forge_recipe.pngbin0 -> 889 bytes
19 files changed, 1263 insertions, 381 deletions
diff --git a/Update Notes/2.1.md b/Update Notes/2.1.md
index 325ed5ae..6c5fb48b 100644
--- a/Update Notes/2.1.md
+++ b/Update Notes/2.1.md
@@ -11,6 +11,7 @@
- [Donpireso replied to an sba dev's email about some of sba features and it seems to imply that blocking clicks in guis arent bannable](https://cdn.discordapp.com/attachments/823769568933576764/906101631861526559/unknown.png)
- Fixed pet overlay not updating when going into /pets
### **Minor Changes:**
+- Add built-in recipes for forge crafts - nea89
- Make cata xp in /pv be calculated on how many runs you have and shows master mode xp rates
- Hide mine waypoints when at location setting - Lulonaut
- Added some info panels to some settings in /neu
@@ -42,6 +43,7 @@
- Added a warning in the tooltip when price info couldn't be found/is outdated - Lulonaut
- Added custom runes and crab hat system - jani
### **Bug Fixes**
+- Fix wiki pages freezing the entire game - nea89
- Made titanium overlay and waypoints work with dwarven overlay off
- "fixed" divan rarity in NEUAH (scuffed)
- Made etherwarp block overlay config option
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<String, Set<String>> usagesMap = new HashMap<>();
+ // TODO: private final Map<String, NeuItem>
+
+ private final Set<NeuRecipe> recipes = new HashSet<>();
+ private final HashMap<String, Set<NeuRecipe>> recipesMap = new HashMap<>();
+ private final HashMap<String, Set<NeuRecipe>> 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<NeuRecipe> getRecipesFor(String internalName) {
+ return recipesMap.getOrDefault(internalName, Collections.emptySet());
+ }
+
+ public Set<NeuRecipe> 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<NeuRecipe> recipesFor = getRecipesFor(internalName);
+ if (container != null && container.getLowerChestInventory().getDisplayName().getUnformattedText().equals("Craft Item")) {
+ Optional<NeuRecipe> 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<ItemStack[]> craftMatrices = new ArrayList<>();
- List<JsonObject> 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<NeuRecipe> 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.<ItemStack[]>newArrayList(craftMatrix), Lists.newArrayList(item), this));
- return true;
- }
- return false;
+ if (!recipesMap.containsKey(internalName)) return false;
+ Set<NeuRecipe> 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<String, JsonObject> 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<String> 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<NeuRecipe> 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<String> devTestUsers = new ArrayList<>(Arrays.asList("moulberry", "lucycoconut", "ironm00n", "ariyio", "throwpo", "dediamondpro"));
+ private static final List<String> 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<ItemStack[]> craftMatrices;
- private final List<JsonObject> results;
private int currentIndex = 0;
private final String title;
+ private final List<NeuRecipe> 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<ItemStack[]> craftMatrices, List<JsonObject> results, NEUManager manager) {
- this.craftMatrices = craftMatrices;
- this.results = results;
+ public GuiItemRecipe(String title, List<NeuRecipe> 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<RecipeSlot> getAllRenderedSlots() {
+ return ImmutableList.<RecipeSlot>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<String> 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<RecipeSlot> 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<RecipeSlot> getPlayerInventory() {
+ List<RecipeSlot> 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<RecipeSlot> 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<Ingredient> getIngredients() {
+ Set<Ingredient> ingredients = Sets.newHashSet(inputs);
+ ingredients.remove(null);
+ return ingredients;
+ }
+
+ @Override
+ public Set<Ingredient> getOutputs() {
+ return Collections.singleton(getOutput());
+ }
+
+ public Ingredient getOutput() {
+ return outputIngredient;
+ }
+
+ public Ingredient[] getInputs() {
+ return inputs;
+ }
+
+
+ @Override
+ public List<RecipeSlot> 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<Ingredient> inputs;
+ private final Ingredient output;
+ private final int hotmLevel;
+ private final int timeInSeconds; // TODO: quick forge
+ private List<RecipeSlot> slots;
+
+ public ForgeRecipe(NEUManager manager, List<Ingredient> inputs, Ingredient output, int durationInSeconds, int hotmLevel) {
+ this.manager = manager;
+ this.inputs = inputs;
+ this.output = output;
+ this.hotmLevel = hotmLevel;
+ this.timeInSeconds = durationInSeconds;
+ }
+
+ public List<Ingredient> 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<Ingredient> getIngredients() {
+ return Sets.newHashSet(inputs);
+ }
+
+ @Override
+ public Set<Ingredient> getOutputs() {
+ return Collections.singleton(output);
+ }
+
+ @Override
+ public List<RecipeSlot> 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<Ingredient> 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<Ingredient> mergeIngredients(Iterable<Ingredient> ingredients) {
+ Map<String, Ingredient> 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<Ingredient> getIngredients();
+
+ Set<Ingredient> getOutputs();
+
+ List<RecipeSlot> 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<String, String> 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<Ingredient> 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<Character, Integer> durationSuffixLengthMap = new HashMap<Character, Integer>() {{
+ 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<String> 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.
+ * <p>
+ * 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<String, Tree> profiles = new ConcurrentHashMap<>();
+
+ public static class Tree {
+ private Map<String, Integer> 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<String> 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<Void> updateTask = CompletableFuture.completedFuture(null);
+
+ private boolean shouldReloadSoon = false;
+
+ public HotmInformation(NotEnoughUpdates neu) {
+ this.neu = neu;
+ MinecraftForge.EVENT_BUS.register(this);
+ }
+
+ public Optional<Tree> getInformationOn(String profile) {
+ if (profile == null) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(this.profiles.get(profile));
+ }
+
+ public Optional<Tree> 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<String, String>() {{
+ 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<String, JsonElement> 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<JsonObject> getHypixelApiAsync(String apiKey, String method, HashMap<String, String> args) {
+ return getApiAsync(generateApiUrl(apiKey, method, args));
+ }
+
public void getHypixelApiAsync(String apiKey, String method, HashMap<String, String> args, Consumer<JsonObject> consumer) {
- getHypixelApiAsync(apiKey, method, args, consumer, () -> {});
+ getHypixelApiAsync(apiKey, method, args, consumer, () -> {
+ });
}
public void getHypixelApiAsync(String apiKey, String method, HashMap<String, String> args, Consumer<JsonObject> 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<JsonObject> getApiAsync(String urlS) {
+ CompletableFuture<JsonObject> result = new CompletableFuture<>();
+ es.submit(() -> {
+ try {
+ result.complete(getApiSync(urlS));
+ } catch (Exception e) {
+ result.completeExceptionally(e);
+ }
+ });
+ return result;
+ }
+
public void getApiAsync(String urlS, Consumer<JsonObject> consumer, Runnable error) {
es.submit(() -> {
try {
@@ -134,16 +154,22 @@ public class HypixelApi {
}
public String generateApiUrl(String apiKey, String method, HashMap<String, String> 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<String, String> 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<String, String> rarityArrMap = new HashMap<String, String>() {{
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
--- /dev/null
+++ b/src/main/resources/assets/notenoughupdates/textures/gui/forge_recipe.png
Binary files differ