aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRoman / Linnea Gräf <roman.graef@gmail.com>2023-03-13 23:43:40 +0100
committerGitHub <noreply@github.com>2023-03-13 23:43:40 +0100
commit86cd165c1ad9a72567cf5d033a8ff92779f72b30 (patch)
tree55960f96bd155aaa29ca03c5f79e61f830aa0ae0
parentd249bbdc8e99bfdab81aa6b215e70c4f21def91e (diff)
downloadNotEnoughUpdates-86cd165c1ad9a72567cf5d033a8ff92779f72b30.tar.gz
NotEnoughUpdates-86cd165c1ad9a72567cf5d033a8ff92779f72b30.tar.bz2
NotEnoughUpdates-86cd165c1ad9a72567cf5d033a8ff92779f72b30.zip
Improve NPC shop generator (#650)
-rw-r--r--build.gradle.kts3
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java14
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/listener/RenderListener.java10
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/Navigation.java3
-rw-r--r--src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/dev/RepoExporters.java140
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemSearchGui.kt152
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemShopExporter.kt139
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporter.kt28
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporters.kt99
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingContext.kt93
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingInterruptedException.kt24
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt88
-rw-r--r--src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/gson.kt22
13 files changed, 664 insertions, 151 deletions
diff --git a/build.gradle.kts b/build.gradle.kts
index aec078d3..333f75cd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -134,6 +134,9 @@ dependencies {
implementation(enforcedPlatform("org.jetbrains.kotlin:kotlin-bom:1.8.0"))
kotlinDependencies(kotlin("stdlib"))
+ ksp("dev.zacsweers.autoservice:auto-service-ksp:1.0.0")
+ implementation("com.google.auto.service:auto-service-annotations:1.0.1")
+
compileOnly(ksp(project(":annotations"))!!)
compileOnly("org.projectlombok:lombok:1.18.24")
annotationProcessor("org.projectlombok:lombok:1.18.24")
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
index 5eab77f9..0281d95b 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/NEUManager.java
@@ -1193,6 +1193,19 @@ public class NEUManager {
json.addProperty("crafttext", crafttext);
json.addProperty("clickcommand", clickcommand);
json.addProperty("damage", damage);
+ nbttag.setInteger("HideFlags", 254);
+ NBTTagCompound display = nbttag.getCompoundTag("display");
+ nbttag.setTag("display", display);
+ display.setString("Name", displayName);
+ NBTTagList loreList = new NBTTagList();
+ for (String loreLine : lore) {
+ loreList.appendTag(new NBTTagString(loreLine));
+ }
+ display.setTag("Lore", loreList);
+ NBTTagCompound extraAttributes = nbttag.getCompoundTag("ExtraAttributes");
+ nbttag.setTag("ExtraAttributes", extraAttributes);
+ extraAttributes.setString("id", internalname);
+
json.addProperty("nbttag", nbttag.toString());
json.addProperty("modver", NotEnoughUpdates.VERSION);
json.addProperty("infoType", infoType);
@@ -1265,6 +1278,7 @@ public class NEUManager {
}
public void writeJson(JsonObject json, File file) throws IOException {
+ file.getParentFile().mkdirs();
file.createNewFile();
try (
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/listener/RenderListener.java b/src/main/java/io/github/moulberry/notenoughupdates/listener/RenderListener.java
index 5e39aa45..d324184f 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/listener/RenderListener.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/listener/RenderListener.java
@@ -128,8 +128,6 @@ public class RenderListener {
private String loadedInvName = "";
//NPC parsing
- public static boolean typing;
-
private boolean inDungeonPage = false;
public RenderListener(NotEnoughUpdates neu) {
@@ -1075,9 +1073,6 @@ public class RenderListener {
event.setCanceled(true);
return;
}
- if (typing) {
- event.setCanceled(true);
- }
if (NotEnoughUpdates.INSTANCE.config.hidden.dev && Keyboard.isKeyDown(Keyboard.KEY_B) &&
Minecraft.getMinecraft().currentScreen instanceof GuiChest
) {
@@ -1092,11 +1087,6 @@ public class RenderListener {
} else if (lower.getName().contains("Draconic Altar Guide")) {
RepoExporters.getInstance().draconicAlterExporter();
}
- } else if (Keyboard.isKeyDown(Keyboard.KEY_RETURN) && NotEnoughUpdates.INSTANCE.config.hidden.dev) {
- if (RepoExporters.getInstance().npcExporter()) {
- event.setCanceled(true);
- return;
- }
} else if (NotEnoughUpdates.INSTANCE.config.hidden.dev && Keyboard.isKeyDown(Keyboard.KEY_B) &&
Minecraft.getMinecraft().currentScreen instanceof GuiChest &&
((((ContainerChest) ((GuiChest) Minecraft.getMinecraft().currentScreen).inventorySlots)
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/Navigation.java b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/Navigation.java
index 9bdc4608..86e49aec 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/Navigation.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/Navigation.java
@@ -111,7 +111,8 @@ public class Navigation {
private Teleporter nextTeleporter = null;
public boolean isValidWaypoint(JsonObject object) {
- return object.has("x")
+ return object != null
+ && object.has("x")
&& object.has("y")
&& object.has("z")
&& object.has("island")
diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/dev/RepoExporters.java b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/dev/RepoExporters.java
index ecd7a203..259aadc0 100644
--- a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/dev/RepoExporters.java
+++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/dev/RepoExporters.java
@@ -25,12 +25,9 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
-import io.github.moulberry.notenoughupdates.NEUOverlay;
import io.github.moulberry.notenoughupdates.NotEnoughUpdates;
import io.github.moulberry.notenoughupdates.itemeditor.NEUItemEditor;
-import io.github.moulberry.notenoughupdates.listener.RenderListener;
import io.github.moulberry.notenoughupdates.util.ItemUtils;
-import io.github.moulberry.notenoughupdates.util.SBInfo;
import io.github.moulberry.notenoughupdates.util.Utils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.inventory.GuiChest;
@@ -54,11 +51,9 @@ import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.TreeMap;
import java.util.stream.Collectors;
public class RepoExporters {
@@ -304,141 +299,6 @@ public class RepoExporters {
}
}
- private HashMap<String, String> cachedDefinitions;
- private String correctingItem;
-
- public boolean npcExporter() {
- if (RenderListener.typing) {
- RenderListener.typing = false;
- cachedDefinitions.put(correctingItem, NEUOverlay.getTextField().getText());
- NEUOverlay.getTextField().setText("");
- }
- if (Minecraft.getMinecraft().currentScreen instanceof GuiChest) {
- Minecraft mc = Minecraft.getMinecraft();
- GuiChest eventGui = (GuiChest) Minecraft.getMinecraft().currentScreen;
- ContainerChest cc = (ContainerChest) eventGui.inventorySlots;
- IInventory lower = cc.getLowerChestInventory();
-
- try {
- JsonObject newNPC = new JsonObject();
- String displayName = lower.getDisplayName().getUnformattedText();
- File file = new File(
- Minecraft.getMinecraft().mcDataDir.getAbsolutePath(),
- "config" + File.separator + "notenoughupdates" +
- File.separator + "repo" + File.separator + "npc" + File.separator +
- displayName.toUpperCase().replace(" ", "_") + ".json"
- );
- newNPC.add("itemid", new JsonPrimitive("minecraft:skull"));
- newNPC.add("displayname", new JsonPrimitive("§9" + displayName + " (NPC)"));
- newNPC.add("nbttag", new JsonPrimitive("TODO"));
- newNPC.add("damage", new JsonPrimitive(3));
-
- JsonArray newArray = new JsonArray();
- newArray.add(new JsonPrimitive(""));
- newNPC.add("lore", newArray);
- newNPC.add("internalname", new JsonPrimitive(displayName.toUpperCase().replace(" ", "_") + "_NPC"));
- newNPC.add("clickcommand", new JsonPrimitive("viewrecipe"));
- newNPC.add("modver", new JsonPrimitive(NotEnoughUpdates.VERSION));
- newNPC.add("infoType", new JsonPrimitive("WIKI_URL"));
- JsonArray emptyInfoArray = new JsonArray();
- emptyInfoArray.add(new JsonPrimitive("TODO"));
- newNPC.add("info", emptyInfoArray);
- newNPC.add("x", new JsonPrimitive((int) mc.thePlayer.posX));
- newNPC.add("y", new JsonPrimitive((int) mc.thePlayer.posY + 2));
- newNPC.add("z", new JsonPrimitive((int) mc.thePlayer.posZ));
- newNPC.add("island", new JsonPrimitive(SBInfo.getInstance().getLocation()));
-
- JsonArray recipesArray = new JsonArray();
-
- TreeMap<String, JsonObject> itemInformation = NotEnoughUpdates.INSTANCE.manager.getItemInformation();
- for (int i = 0; i < 45; i++) {
- ItemStack stack = lower.getStackInSlot(i);
- if (stack == null) continue;
- if (stack.getDisplayName().isEmpty() || stack.getDisplayName().equals(" ")) continue;
-
- String internalname = NotEnoughUpdates.INSTANCE.manager.getInternalnameFromNBT(stack.getTagCompound());
- if (internalname == null) continue;
- JsonObject currentRecipe = new JsonObject();
- currentRecipe.add("type", new JsonPrimitive("npc_shop"));
- JsonArray costArray = new JsonArray();
- boolean inCost = false;
- for (String s : NotEnoughUpdates.INSTANCE.manager.getLoreFromNBT(stack.getTagCompound())) {
- if (s.equals("§7Cost")) {
- inCost = true;
- continue;
- } else if (s.equals("§eClick to trade!")) {
- inCost = false;
- }
- if (!inCost) continue;
- String entry = StringUtils.stripControlCodes(s);
- if (entry.isEmpty()) continue;
- int coinIndex = entry.indexOf(" Coins");
- if (coinIndex != -1) {
- String amountString = entry.substring(0, coinIndex).replace(",", "");
- costArray.add(new JsonPrimitive("SKYBLOCK_COIN:" + amountString));
- } else {
- if (cachedDefinitions == null) {
- cachedDefinitions = new HashMap<>();
- }
-
- String item;
- int amountIndex = entry.lastIndexOf(" x");
- String amountString;
- if (amountIndex == -1) {
- amountString = "1";
- item = entry.replace(" ", "_").toUpperCase();
- } else {
- amountString = entry.substring(amountIndex);
- item = entry.substring(0, amountIndex).replace(" ", "_").toUpperCase();
- }
- amountString = amountString.replace(",", "").replace("x", "").trim();
- if (itemInformation.containsKey(item)) {
- costArray.add(new JsonPrimitive(item + ":" + amountString));
- } else if (cachedDefinitions.containsKey(item)) {
- costArray.add(new JsonPrimitive(cachedDefinitions.get(item) + ":" + amountString));
- } else {
- Utils.addChatMessage("Change the item ID of " + item + " to the correct one and press Enter.");
- NEUOverlay.getTextField().setText(item);
- RenderListener.typing = true;
- correctingItem = item;
- if (cachedDefinitions == null) {
- cachedDefinitions = new HashMap<>();
- }
- return true;
- }
- }
- }
- currentRecipe.add("cost", costArray);
- currentRecipe.add("result", new JsonPrimitive(internalname));
- recipesArray.add(currentRecipe);
- newNPC.add("recipes", recipesArray);
- }
- Gson gson = new GsonBuilder().setPrettyPrinting().create();
- System.out.println(gson.toJson(newNPC));
- try {
- //noinspection ResultOfMethodCallIgnored
- file.createNewFile();
- try (
- BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
- Files.newOutputStream(file.toPath()),
- StandardCharsets.UTF_8
- ))
- ) {
- writer.write(gson.toJson(newNPC));
- Utils.addChatMessage(
- EnumChatFormatting.AQUA + "Parsed and saved: " + EnumChatFormatting.WHITE + displayName);
- }
- } catch (IOException ignored) {
- Utils.addChatMessage(EnumChatFormatting.RED + "Error while writing file.");
- }
- } catch (Exception e) {
- e.printStackTrace();
- Utils.addChatMessage(
- EnumChatFormatting.RED + "Error while parsing inventory. Try again or check logs for details");
- }
- }
- return false;
- }
public void essenceExporter2() {
GuiChest eventGui = (GuiChest) Minecraft.getMinecraft().currentScreen;
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemSearchGui.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemSearchGui.kt
new file mode 100644
index 00000000..2af7975e
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemSearchGui.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.recipes.generators
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.util.Utils
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.gui.GuiTextField
+import net.minecraft.client.renderer.Tessellator
+import net.minecraft.init.Items
+import net.minecraft.item.ItemStack
+import net.minecraftforge.fml.client.GuiScrollingList
+
+class ItemSearchGui(initialText: String, val onSelect: (String?) -> Unit) : GuiScreen() {
+ val textField = GuiTextField(
+ 0, Minecraft.getMinecraft().fontRendererObj,
+ -1,
+ 5,
+ 200,
+ 20
+ )
+ var lastFilter = ""
+
+ class ItemScrollingList(
+ screenWidth: Int,
+ screenHeight: Int,
+ val callback: (ItemStack) -> Unit
+ ) : GuiScrollingList(
+ Minecraft.getMinecraft(),
+ 200,
+ screenHeight - 30,
+ 30,
+ screenHeight,
+ screenWidth / 2 - 100,
+ 20,
+ screenWidth,
+ screenHeight
+ ) {
+ var items: List<ItemStack> = listOf()
+ private set
+ var selected: Int? = null
+
+ fun setItems(newItems: List<ItemStack>) {
+ this.items = newItems
+ selected = null
+ }
+
+ override fun getSize(): Int {
+ return items.size
+ }
+
+ override fun elementClicked(i: Int, doubleClick: Boolean) {
+ if (doubleClick)
+ callback(items[i])
+ selected = i
+ }
+
+ override fun isSelected(i: Int): Boolean {
+ return selected == i
+ }
+
+ override fun drawBackground() {
+
+ }
+
+ override fun drawSlot(i: Int, right: Int, top: Int, height: Int, tessellator: Tessellator?) {
+ val item = items[i]
+ Utils.drawItemStack(item, this.left + 1, top + 1)
+ Utils.drawStringF(item.displayName, this.left + 18F, top + 1F, true, 0xFF00FF00.toInt())
+ }
+ }
+
+ lateinit var items: ItemScrollingList
+
+
+ init {
+ textField.text = initialText
+ }
+
+ override fun initGui() {
+ super.initGui()
+ textField.xPosition = width / 2 - 100
+ textField.setCanLoseFocus(false)
+ textField.isFocused = true
+ items = ItemScrollingList(width, height) {
+ val name = NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery()
+ .withItemStack(it)
+ .resolveInternalName() ?: return@ItemScrollingList
+ onSelect(name)
+ }
+ updateItems()
+ }
+
+ override fun drawScreen(mouseX: Int, mouseY: Int, partialTicks: Float) {
+ super.drawScreen(mouseX, mouseY, partialTicks)
+ drawDefaultBackground()
+ textField.drawTextBox()
+ items.drawScreen(mouseX, mouseY, partialTicks)
+ }
+
+ override fun onGuiClosed() {
+ onSelect(null)
+ }
+ override fun mouseClicked(mouseX: Int, mouseY: Int, mouseButton: Int) {
+ super.mouseClicked(mouseX, mouseY, mouseButton)
+ textField.mouseClicked(mouseX, mouseY, mouseButton)
+ }
+
+ override fun keyTyped(typedChar: Char, keyCode: Int) {
+ super.keyTyped(typedChar, keyCode)
+ textField.textboxKeyTyped(typedChar, keyCode)
+ }
+
+ fun updateItems() {
+ lastFilter = textField.text
+
+ val candidates =
+ NotEnoughUpdates.INSTANCE.manager.search(textField.text)
+ .map {
+ NotEnoughUpdates.INSTANCE.manager.createItemResolutionQuery()
+ .withKnownInternalName(it)
+ .resolveToItemStack() ?: ItemStack(
+ Items.painting, 1, 10
+ )
+ }
+ items.setItems(candidates)
+ }
+
+ override fun updateScreen() {
+ super.updateScreen()
+ textField.updateCursorCounter()
+ if (textField.text != lastFilter) updateItems()
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemShopExporter.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemShopExporter.kt
new file mode 100644
index 00000000..e2673125
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/ItemShopExporter.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.recipes.generators
+
+import com.google.auto.service.AutoService
+import io.github.moulberry.notenoughupdates.recipes.Ingredient
+import io.github.moulberry.notenoughupdates.recipes.ItemShopRecipe
+import io.github.moulberry.notenoughupdates.util.ItemUtils
+import io.github.moulberry.notenoughupdates.util.JsonUtils
+import io.github.moulberry.notenoughupdates.util.SBInfo
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.kotlin.set
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.gui.inventory.GuiChest
+import net.minecraft.inventory.ContainerChest
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NBTTagCompound
+import net.minecraft.util.StringUtils
+
+@AutoService(RepoExporter::class)
+class ItemShopExporter : RepoExporter {
+ override suspend fun export(context: RepoExportingContext) {
+ val chest = context.gui as GuiChest
+ val container = chest.inventorySlots as ContainerChest
+ val inventory = container.lowerChestInventory
+ val displayName = inventory.displayName.unformattedText
+ val npcInternalName = displayName.uppercase().replace(" ", "_") + "_NPC"
+ val file = context.manager.repoLocation.resolve("items/$npcInternalName.json")
+ val itemDisplayName = "§9$displayName (NPC)"
+ val baseNPCJson = context.manager.createItemJson(
+ npcInternalName,
+ "minecraft:skull",
+ itemDisplayName,
+ arrayOf(""),
+ null,
+ "WIKI_URL",
+ arrayOf("TODO"),
+ "viewrecipe",
+ 3,
+ NBTTagCompound()
+ )
+ baseNPCJson["x"] = context.mc.thePlayer.posX.toInt()
+ baseNPCJson["y"] = context.mc.thePlayer.posY.toInt()
+ baseNPCJson["z"] = context.mc.thePlayer.posZ.toInt()
+ baseNPCJson["island"] = SBInfo.getInstance().getLocation()
+
+ val recipes = mutableListOf<ItemShopRecipe>()
+ for (slotNum in 0 until inventory.sizeInventory) {
+ val slot = inventory.getStackInSlot(slotNum) ?: continue
+ if (slot.displayName.isNullOrBlank()) continue
+
+ recipes.add(findRecipe(context, slot) ?: continue)
+ }
+ baseNPCJson["recipes"] = JsonUtils.transformListToJsonArray(recipes, ItemShopRecipe::serialize)
+ context.writeFile(file, baseNPCJson)
+ }
+
+ val coinRegex = "^([0-9,]+) Coins$".toRegex()
+ val itemRegex = "^(?<name>.*?)(?: x(?<amount>[0-9,]+))?$".toRegex()
+ suspend fun findRecipe(
+ context: RepoExportingContext,
+ stack: ItemStack,
+ ): ItemShopRecipe? {
+ val resultName =
+ context.manager.createItemResolutionQuery().withItemStack(stack).resolveInternalName() ?: return null
+
+ var inCost = false
+ val costList = mutableListOf<Ingredient>()
+ for (loreLine in ItemUtils.getLore(stack)) {
+ var loreLine = loreLine
+ if (loreLine == "§7Cost") {
+ inCost = true
+ continue
+ } else if (loreLine == "§eClick to trade!") {
+ inCost = false
+ continue
+ } else if (loreLine.startsWith("§7Cost: ")) {
+ loreLine = loreLine.substringAfter("§7Cost: ")
+ inCost = true
+ } else if (!inCost) continue
+ val cleanLine = StringUtils.stripControlCodes(loreLine)
+ if (cleanLine.isBlank()) continue
+ val ingredient = findIngredientForLoreLine(context, cleanLine)
+ if (ingredient == null) {
+ Utils.addChatMessage("Warning: Could not find ingredient for $loreLine")
+ } else {
+ costList.add(ingredient)
+ }
+ }
+ return ItemShopRecipe(
+ null, costList, Ingredient(context.manager, resultName, stack.stackSize.toDouble()), null
+ )
+ }
+
+ suspend fun findIngredientForLoreLine(
+ context: RepoExportingContext,
+ loreLine: String,
+ ): Ingredient? {
+ val coinMatch = coinRegex.matchEntire(loreLine)
+ if (coinMatch != null) {
+ val amount = coinMatch.groupValues[1].replace(",", "").toInt()
+ return Ingredient.coinIngredient(context.manager, amount)
+ }
+ val itemMatch = itemRegex.matchEntire(loreLine)
+ if (itemMatch != null) {
+ val label = itemMatch.groups["name"]!!.value
+ val amount = itemMatch.groups["amount"]?.value?.replace(",", "")?.toDouble() ?: 1.0
+ if (context.manager.itemInformation.containsKey(label.uppercase())) {
+ return Ingredient(context.manager, label, amount)
+ }
+ return Ingredient(context.manager, context.findItemByName(label), amount)
+ }
+ return null
+ }
+
+ override fun canExport(gui: GuiScreen): Boolean {
+ return gui is GuiChest
+ }
+
+ override val name: String
+ get() = "Item Shop"
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporter.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporter.kt
new file mode 100644
index 00000000..c03bb498
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporter.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.recipes.generators
+
+import net.minecraft.client.gui.GuiScreen
+
+interface RepoExporter {
+ suspend fun export(context: RepoExportingContext)
+ fun canExport(gui: GuiScreen): Boolean
+ val name: String
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporters.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporters.kt
new file mode 100644
index 00000000..1ffbd696
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExporters.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.recipes.generators
+
+import io.github.moulberry.notenoughupdates.NotEnoughUpdates
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import io.github.moulberry.notenoughupdates.events.GuiContainerBackgroundDrawnEvent
+import io.github.moulberry.notenoughupdates.mixins.AccessorGuiContainer
+import io.github.moulberry.notenoughupdates.util.Utils
+import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiButton
+import net.minecraft.client.gui.inventory.GuiContainer
+import net.minecraftforge.client.event.GuiScreenEvent
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import org.lwjgl.input.Mouse
+import java.util.*
+
+@NEUAutoSubscribe
+object RepoExporters {
+ private val allRepoExporters = ServiceLoader.load(RepoExporter::class.java).let {
+ it.reload()
+ it.toList()
+ }
+ private var lastRenderedButtons = listOf<Pair<GuiButton, RepoExporter>>()
+ private var lastGui: GuiContainer? = null
+
+ @SubscribeEvent
+ fun onGuiRender(event: GuiContainerBackgroundDrawnEvent) {
+ if (!NotEnoughUpdates.INSTANCE.config.apiData.repositoryEditing) return
+ val mouseX = Utils.getMouseX()
+ val mouseY = Utils.getMouseY()
+ val gui = event.container
+ if (gui !is AccessorGuiContainer) return
+ val exporters = allRepoExporters.filter { it.canExport(gui) }
+ synchronized(this) {
+ lastGui = gui
+ lastRenderedButtons = exporters.withIndex().map { (index, exporter) ->
+ GuiButton(
+ 0,
+ 0, -30 - 20 * index,
+ gui.xSize, 20,
+ "Run Exporter: ${exporter.name}"
+ ).also {
+ it.drawButton(
+ Minecraft.getMinecraft(),
+ mouseX - gui.guiLeft,
+ mouseY - gui.guiTop
+ )
+ } to exporter
+ }
+ }
+ }
+
+ @SubscribeEvent
+ fun onGuiClick(event: GuiScreenEvent.MouseInputEvent.Pre) {
+ if (!Mouse.getEventButtonState()) return
+ val accessor = event.gui as? AccessorGuiContainer ?: return
+
+ val mouseX = Utils.getMouseX() - accessor.guiLeft
+ val mouseY = Utils.getMouseY() - accessor.guiTop
+
+ val exporter = synchronized(this) {
+ if (lastGui !== event.gui) return
+ lastRenderedButtons.forEach { (button, exporter) ->
+ if (button.mousePressed(Minecraft.getMinecraft(), mouseX, mouseY)) {
+ return@synchronized exporter
+ }
+ }
+ return
+ }
+ event.isCanceled = true
+ Coroutines.launchCoroutineOnCurrentThread {
+ try {
+ exporter.export(RepoExportingContext(NotEnoughUpdates.INSTANCE.manager, event.gui))
+ Utils.addChatMessage("Repo export completed")
+ } catch (e: RepoExportingInterruptedException) {
+ Utils.addChatMessage("Repo exporting interrupted")
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingContext.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingContext.kt
new file mode 100644
index 00000000..66381242
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingContext.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.recipes.generators
+
+import com.google.gson.JsonObject
+import io.github.moulberry.notenoughupdates.NEUManager
+import io.github.moulberry.notenoughupdates.util.MinecraftExecutor
+import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines.continueOn
+import io.github.moulberry.notenoughupdates.util.kotlin.Coroutines.waitTicks
+import net.minecraft.client.Minecraft
+import net.minecraft.client.gui.GuiScreen
+import net.minecraft.client.gui.GuiYesNo
+import java.io.File
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class RepoExportingContext(
+ val manager: NEUManager,
+ val gui: GuiScreen,
+) {
+ val mc = Minecraft.getMinecraft()
+ val nameToItemCache = mutableMapOf<String, String>()
+
+ suspend fun writeFile(file: File, json: JsonObject) {
+ if (file.exists()) {
+ if (!askYesNo("Overwrite file?", "The file $file already exists."))
+ return
+ }
+ manager.writeJson(json, file)
+ }
+
+ suspend fun askYesNo(title: String, subtitle: String): Boolean {
+ var wasResolved = false
+ continueOn(MinecraftExecutor.OnThread)
+ val result = suspendCoroutine {
+ mc.displayGuiScreen(object : GuiYesNo({ result, id ->
+ if (!wasResolved) {
+ wasResolved = true
+ it.resume(result)
+ }
+ }, title, subtitle, 0) {
+ override fun onGuiClosed() {
+ if (!wasResolved) {
+ wasResolved = true
+ it.resume(null)
+ }
+ }
+ })
+ }
+ if (result == null) {
+ waitTicks(1) // Wait one tick for gui closing animation
+ }
+ mc.displayGuiScreen(gui)
+ return result ?: false
+ }
+
+ suspend fun findItemByName(name: String): String {
+ val c = nameToItemCache[name]
+ if (c != null) return c
+ var wasResolved = false
+ continueOn(MinecraftExecutor.OnThread)
+ val result = suspendCoroutine { cont ->
+ mc.displayGuiScreen(ItemSearchGui(name) {
+ if (!wasResolved) {
+ wasResolved = true
+ cont.resume(it)
+ }
+ })
+ }
+ waitTicks(1)
+ mc.displayGuiScreen(gui)
+ result ?: throw RepoExportingInterruptedException()
+ nameToItemCache[name] = result
+ return result
+ }
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingInterruptedException.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingInterruptedException.kt
new file mode 100644
index 00000000..9839f200
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/recipes/generators/RepoExportingInterruptedException.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.recipes.generators
+
+class RepoExportingInterruptedException : Error() {
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt
new file mode 100644
index 00000000..4bcf0c61
--- /dev/null
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/Coroutines.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2023 NotEnoughUpdates contributors
+ *
+ * This file is part of NotEnoughUpdates.
+ *
+ * NotEnoughUpdates is free software: you can redistribute it
+ * and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * NotEnoughUpdates is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package io.github.moulberry.notenoughupdates.util.kotlin
+
+import io.github.moulberry.notenoughupdates.autosubscribe.NEUAutoSubscribe
+import net.minecraftforge.fml.common.eventhandler.SubscribeEvent
+import net.minecraftforge.fml.common.gameevent.TickEvent
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import kotlin.coroutines.*
+
+@NEUAutoSubscribe
+object Coroutines {
+ fun <T> launchCoroutineOnCurrentThread(block: suspend () -> T): CompletableFuture<T> {
+ val future = CompletableFuture<T>()
+ val coroutine = block.createCoroutine(object : Continuation<T> {
+ override val context: CoroutineContext
+ get() = EmptyCoroutineContext
+
+ override fun resumeWith(result: Result<T>) {
+ try {
+ future.complete(result.getOrThrow())
+ } catch (ex: Throwable) {
+ future.completeExceptionally(ex)
+ }
+ }
+ })
+ coroutine.resume(Unit)
+ return future
+ }
+
+ suspend fun continueOn(ex: Executor) {
+ suspendCoroutine {
+ ex.execute {
+ it.resume(Unit)
+ }
+ }
+ }
+
+ private data class DelayedTask(val contination: () -> Unit, var tickDelay: Int)
+
+ private val tasks = mutableListOf<DelayedTask>()
+
+ @SubscribeEvent
+ fun onTick(event: TickEvent.ClientTickEvent) {
+ if (event.phase == TickEvent.Phase.END) {
+ val toRun = mutableListOf<DelayedTask>()
+ synchronized(tasks) {
+ tasks.removeIf {
+ it.tickDelay--
+ if (it.tickDelay <= 0) {
+ toRun.add(it)
+ return@removeIf true
+ }
+ false
+ }
+ }
+ toRun.forEach { it.contination() }
+ }
+ }
+
+ suspend fun waitTicks(tickCount: Int) {
+ suspendCoroutine {
+ synchronized(tasks) {
+ tasks.add(DelayedTask({ it.resume(Unit) }, tickCount))
+ }
+ }
+ }
+
+
+}
diff --git a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/gson.kt b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/gson.kt
index 00c3d729..ec2e3c31 100644
--- a/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/gson.kt
+++ b/src/main/kotlin/io/github/moulberry/notenoughupdates/util/kotlin/gson.kt
@@ -20,6 +20,8 @@
package io.github.moulberry.notenoughupdates.util.kotlin
import com.google.gson.Gson
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
@@ -31,3 +33,23 @@ inline fun <reified T : Any> typeToken(): Type {
inline fun <reified T : Any> Gson.fromJson(string: String): T {
return fromJson(string, typeToken<T>())
}
+
+operator fun JsonObject.set(name: String, value: Number) {
+ this.addProperty(name, value)
+}
+
+operator fun JsonObject.set(name: String, value: String) {
+ this.addProperty(name, value)
+}
+
+operator fun JsonObject.set(name: String, value: Boolean) {
+ this.addProperty(name, value)
+}
+
+operator fun JsonObject.set(name: String, value: Char) {
+ this.addProperty(name, value)
+}
+
+operator fun JsonObject.set(name: String, value: JsonElement) {
+ this.add(name, value)
+}