From 9943b441c7e29ac23269fb3b421821fa049d4a12 Mon Sep 17 00:00:00 2001
From: Ic3Tank <61137113+IceTank@users.noreply.github.com>
Date: Sun, 22 Mar 2026 19:30:49 +0100
Subject: [PATCH] Add AutoAnvil module
---
.../lambda/module/modules/player/AutoAnvil.kt | 306 ++++++++++++++++++
1 file changed, 306 insertions(+)
create mode 100644 src/main/kotlin/com/lambda/module/modules/player/AutoAnvil.kt
diff --git a/src/main/kotlin/com/lambda/module/modules/player/AutoAnvil.kt b/src/main/kotlin/com/lambda/module/modules/player/AutoAnvil.kt
new file mode 100644
index 000000000..7184dea47
--- /dev/null
+++ b/src/main/kotlin/com/lambda/module/modules/player/AutoAnvil.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2026 Lambda
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.lambda.module.modules.player
+
+import com.lambda.context.SafeContext
+import com.lambda.event.events.GuiEvent
+import com.lambda.event.events.TickEvent
+import com.lambda.event.listener.SafeListener.Companion.listen
+import com.lambda.gui.LambdaScreen
+import com.lambda.gui.dsl.ImGuiBuilder.child
+import com.lambda.gui.dsl.ImGuiBuilder.combo
+import com.lambda.gui.dsl.ImGuiBuilder.inputText
+import com.lambda.gui.dsl.ImGuiBuilder.window
+import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
+import com.lambda.interaction.material.StackSelection
+import com.lambda.interaction.material.container.containers.InventoryContainer
+import com.lambda.module.Module
+import com.lambda.module.tag.ModuleTag
+import com.lambda.threading.runSafe
+import com.lambda.util.NamedEnum
+import com.lambda.util.Timer
+import com.lambda.util.collections.LimitedDecayQueue
+import com.lambda.util.item.ItemUtils
+import imgui.ImGuiListClipper
+import imgui.callback.ImListClipperCallback
+import imgui.flag.ImGuiChildFlags
+import imgui.flag.ImGuiSelectableFlags.DontClosePopups
+import imgui.type.ImBoolean
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.gui.screen.ingame.AnvilScreen
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.enchantment.Enchantment
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.network.packet.c2s.play.RenameItemC2SPacket
+import net.minecraft.registry.Registries
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.screen.AnvilScreenHandler
+import net.minecraft.screen.slot.Slot
+import net.minecraft.screen.slot.SlotActionType
+import kotlin.time.Duration.Companion.milliseconds
+
+
+object AutoAnvil : Module(
+ name = "AutoAnvil",
+ description = "Automatically renames or combines items",
+ tag = ModuleTag.PLAYER
+) {
+ var rename by setting("Rename", false)
+ var combine by setting("Combine", false)
+
+ var renameName by setting("Rename Name", "Renamed Item").group(Group.Renaming)
+ val itemsToRename by setting("Items to Rename", ItemUtils.shulkerBoxes.toSet(), mutableSetOf()).group(Group.Renaming)
+ val combineBooksWithOnlyOneEnchantment by setting("Only one on books", true, description = "Only combine books that have one enchantment on them").group(Group.Combining)
+
+ val delayTimer = Timer()
+
+ /** This queue is used to combat item desynchronization you get on 2b by not trying to move the same items from the same slots multiple times. */
+ val lastMovedItems = LimitedDecayQueue(10, 1000)
+
+ val combineMap = mutableMapOf()
+ val enchantCombineMap = mutableMapOf()
+ val searchBox1 = SearchBox("Item 1") { Registries.ITEM.toList().map { it.name.string } }
+ val searchBox1Enchants = SearchBox("Item 1 Enchants") { getDaShit().map { it.description.string } }
+ val searchBox2 = SearchBox("Item 2") { Registries.ITEM.toSet().map { it.name.string } }
+ val searchBox2Enchants = SearchBox("Item 2 Enchants") { getDaShit().map { it.description.string } }
+
+ init {
+ listen {
+ val playerLevel = player.experienceLevel
+ if (playerLevel <= 0) return@listen
+
+ val sh = player.currentScreenHandler
+ if (sh !is AnvilScreenHandler) return@listen
+ if (rename && renameName.isNotEmpty()) {
+ if (handleRenaming(sh)) return@listen
+ }
+ if (combine) {
+ handleCombining(sh)
+ }
+ }
+
+ listen {
+ if (!combine) return@listen
+ if (mc.currentScreen !is AnvilScreen && mc.currentScreen !is LambdaScreen) return@listen
+ window("Combine Mapping", open = ImBoolean(true)) {
+ text("Combine two items")
+ separator()
+ if (combineMap.isEmpty()) {
+ text("No items")
+ }
+ val toRemove = mutableListOf()
+ combineMap.forEach { (item, item1) ->
+ val enchant1 = enchantCombineMap["item1$item"]
+ val enchant2 = enchantCombineMap["item2$item1"]
+ var i1 = if (enchant1 != null) "$item [$enchant1]" else item
+ var i2 = if (enchant2 != null) "$item1 [$enchant2]" else item1
+ text("$i1 + $i2")
+ sameLine()
+ button("X") {
+ toRemove.add(item)
+ }
+ }
+ combineMap.keys.removeAll(toRemove.toSet())
+
+ separator()
+
+ searchBox1.buildLayout()
+ if (searchBox1.selectedItem == Items.ENCHANTED_BOOK.name.string) {
+ searchBox1Enchants.buildLayout()
+ }
+ searchBox2.buildLayout()
+ if (searchBox2.selectedItem == Items.ENCHANTED_BOOK.name.string) {
+ searchBox2Enchants.buildLayout()
+ }
+ smallButton("Add Mapping") {
+ val item1 = searchBox1.selectedItem
+ val item2 = searchBox2.selectedItem
+
+ val enchant1 = searchBox1Enchants.selectedItem
+ val enchant2 = searchBox2Enchants.selectedItem
+
+ if (item1 != null && item2 != null) {
+ combineMap[item1] = item2
+ if (enchant1 != null) {
+ enchantCombineMap["item1$item1"] = enchant1
+ }
+ if (enchant2 != null) {
+ enchantCombineMap["item2$item2"] = enchant2
+ }
+ searchBox1.selectedItem = null
+ searchBox2.selectedItem = null
+ searchBox1Enchants.selectedItem = null
+ searchBox2Enchants.selectedItem = null
+ }
+ }
+ }
+ return@listen
+ }
+ }
+
+ private fun getDaShit(): List {
+ val registry = MinecraftClient.getInstance().world?.registryManager ?: return emptyList()
+ return registry.getOrThrow(RegistryKeys.ENCHANTMENT).toList()
+ }
+
+ private fun handleCombining(sh: AnvilScreenHandler) {
+ if (!delayTimer.timePassed(150.milliseconds)) return
+ for ((item1, item2) in combineMap) {
+ val input1 = sh.slots[AnvilScreenHandler.INPUT_1_ID]
+ val input2 = sh.slots[AnvilScreenHandler.INPUT_2_ID]
+ val output = sh.slots[AnvilScreenHandler.OUTPUT_ID]
+
+ if (input1.stack.item.name.string.equals(item1) && input2.stack.item.name.string.equals(item2)) {
+ val done = inventoryRequest {
+ click(output.id, 0, SlotActionType.QUICK_MOVE)
+ }.submit().done
+ if (done) delayTimer.reset()
+ return
+ }
+
+ val item1Enchant = enchantCombineMap["item1$item1"] // I am going to kill myself
+ val item2Enchant = enchantCombineMap["item2$item2"]
+
+ val foundItem1 = StackSelection.selectStack().filterSlots(sh.slots)
+ .filter { if (item1Enchant != null) hasEnchantmentByName(it.stack, item1Enchant) else true }
+ .filter { it.stack.item != Items.ENCHANTED_BOOK || !combineBooksWithOnlyOneEnchantment || hasOnlyOneEnchantment(it.stack) }
+ .firstOrNull { it.stack.item.name.string.equals(item1) }
+ val foundItem2 = StackSelection.selectStack().filterSlots(sh.slots)
+ .filter { if (item2Enchant != null) hasEnchantmentByName(it.stack, item2Enchant) else true }
+ .filter { it.stack.item != Items.ENCHANTED_BOOK || !combineBooksWithOnlyOneEnchantment || hasOnlyOneEnchantment(it.stack) }
+ .firstOrNull { it.stack.item.name.string.equals(item2) }
+
+ if (foundItem1 != null && foundItem2 != null && input1.stack.isEmpty && input2.stack.isEmpty) {
+ val done = inventoryRequest {
+ click(foundItem1.id, 0, SlotActionType.QUICK_MOVE)
+ click(foundItem2.id, 0, SlotActionType.QUICK_MOVE)
+ }.submit().done
+ if (done) delayTimer.reset()
+ return
+ }
+ }
+ }
+
+ private fun hasEnchantmentByName(itemStack: ItemStack, enchantmentName: String): Boolean {
+ return itemStack.components.get(DataComponentTypes.STORED_ENCHANTMENTS)?.enchantments?.any { it.value().description.string == enchantmentName } == true
+ }
+
+ private fun hasOnlyOneEnchantment(itemStack: ItemStack): Boolean {
+ val enchantments = itemStack.components.get(DataComponentTypes.STORED_ENCHANTMENTS)?.enchantments ?: return false
+ return enchantments.size == 1
+ }
+
+ private fun SafeContext.handleRenaming(sh: AnvilScreenHandler): Boolean {
+ if (!delayTimer.timePassed(50.milliseconds)) return false
+
+ val output = sh.slots[AnvilScreenHandler.OUTPUT_ID]
+ val input1 = sh.slots[AnvilScreenHandler.INPUT_1_ID]
+ val input2 = sh.slots[AnvilScreenHandler.INPUT_2_ID]
+ val freeInvSlot = StackSelection.selectStack().filterSlots(InventoryContainer.slots).any { it.stack.isEmpty }
+
+ if (!output.stack.isEmpty && !freeInvSlot) return false
+ if (hasName(output) && output != null) {
+ inventoryRequest {
+ click(output.id, 0, SlotActionType.QUICK_MOVE)
+ }.submit()
+ return true
+ }
+
+ if (!input1.stack.isEmpty || !input2.stack.isEmpty) return false
+
+ val slot = itemToRename(sh)
+ if (slot != null) {
+ val done = inventoryRequest {
+ click(slot.id, 0, SlotActionType.QUICK_MOVE)
+ }.submit().done
+ if (done) {
+ lastMovedItems.add(slot.id)
+ connection.sendPacket(RenameItemC2SPacket(renameName))
+ delayTimer.reset()
+ return true
+ }
+ }
+ return false
+ }
+
+ private fun hasName(slot: Slot) = slot.stack.customName?.equals(renameName) == true
+
+ private fun itemToRename(sh: AnvilScreenHandler): Slot? {
+ return StackSelection.selectStack(count = 1) {
+ isOneOfItems(itemsToRename)
+ }
+ .filterSlots(sh.slots)
+ .filter { !lastMovedItems.contains(it.id) }
+ .firstOrNull { it.stack.customName == null || it.stack.customName?.equals(renameName) == false }
+ }
+
+ enum class Group(override val displayName: String) : NamedEnum {
+ Renaming("Renaming"),
+ Combining("Combining")
+ }
+
+ /**
+ * A utility class to select none or one item from a collection, with a search filter and a scrollable list.
+ */
+ class SearchBox(val name: String, val immutableCollectionProvider: () -> Collection) {
+ var searchFilter = ""
+ var selectedItem: String? = null
+ var open = ImBoolean(false)
+
+ fun buildLayout() {
+ val text = if (selectedItem == null) "$name: None" else "$name: ${selectedItem ?: "Unknown Item"}"
+
+ combo("$name##Combo", text) {
+ inputText("##${name}-SearchBox", ::searchFilter)
+
+ child(
+ strId = "##${name}-ComboOptionsChild",
+ childFlags = ImGuiChildFlags.AutoResizeY or ImGuiChildFlags.AlwaysAutoResize
+ ) {
+ val list = immutableCollectionProvider.invoke()
+ .filter { item ->
+ val q = searchFilter.trim()
+ if (q.isEmpty()) true
+ else item.contains(q, ignoreCase = true)
+ }
+
+ val listClipperCallback = object : ImListClipperCallback() {
+ override fun accept(index: Int) {
+ val v = list.getOrNull(index) ?: return
+ val selected = v == selectedItem
+
+ selectable(
+ label = v,
+ selected = selected,
+ flags = DontClosePopups
+ ) {
+ if (selected) {
+ selectedItem = null
+ } else {
+ selectedItem = v
+ }
+ }
+ }
+ }
+ ImGuiListClipper.forEach(list.size, listClipperCallback)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file