diff options
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) +} |