From 49271c233feb646cb58dfad185dbd0c20b1089fc Mon Sep 17 00:00:00 2001 From: Ic3Tank <61137113+IceTank@users.noreply.github.com> Date: Sun, 8 Mar 2026 21:42:45 +0100 Subject: [PATCH 1/3] Add AutoVillagerCycle module to automatically cycle librarians for specific books --- .../modules/player/AutoVillagerCycle.kt | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt diff --git a/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt b/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt new file mode 100644 index 000000000..a0495a9bb --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt @@ -0,0 +1,301 @@ +/* + * 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.config.AutomationConfig.Companion.setDefaultAutomationConfig +import com.lambda.config.settings.complex.Bind +import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress +import com.lambda.context.SafeContext +import com.lambda.event.events.PacketEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure +import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest +import com.lambda.interaction.managers.rotating.visibilty.lookAtEntity +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.sound.SoundManager.playSound +import com.lambda.task.RootTask.run +import com.lambda.task.Task +import com.lambda.task.tasks.BuildTask.Companion.build +import com.lambda.threading.runSafeAutomated +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.Communication.info +import com.lambda.util.Communication.logError +import com.lambda.util.NamedEnum +import com.lambda.util.world.closestEntity +import net.minecraft.block.Blocks +import net.minecraft.component.DataComponentTypes +import net.minecraft.component.type.ItemEnchantmentsComponent +import net.minecraft.enchantment.Enchantment +import net.minecraft.entity.passive.VillagerEntity +import net.minecraft.item.Items +import net.minecraft.network.packet.s2c.play.SetTradeOffersS2CPacket +import net.minecraft.registry.RegistryKeys +import net.minecraft.sound.SoundEvents +import net.minecraft.util.Hand +import net.minecraft.util.hit.EntityHitResult +import net.minecraft.util.math.BlockPos + + +object AutoVillagerCycle : Module( + name = "AutoVillagerCycler", + description = "Automatically cycles librarian villagers with lecterns until a desired enchanted book is found", + tag = ModuleTag.PLAYER +) { + private enum class Group(override val displayName: String) : NamedEnum { + General("General"), + Enchantments("Enchantments") + } + + private val allEnchantments = ArrayList() + + private val lecternPos by setting("Lectern Pos", BlockPos.ORIGIN, "Position where the lectern should be placed/broken").group(Group.General) + private val logFoundBooks by setting("Log Found Books", true, "Log all enchanted books found during cycling").group(Group.General) + private val interactDelay by setting("Interact Delay", 20, 1..40, 1, "Ticks to wait before interacting with the villager", " ticks").group(Group.General) + private val breakDelay by setting("Break Delay", 5, 1..20, 1, "Ticks to wait after breaking the lectern", " ticks").group(Group.General) + private val searchRange by setting("Search Range", 5.0, 1.0..10.0, 0.5, "Range to search for nearby villagers", " blocks").group(Group.General) + private val startCyclingBind by setting("Start Cycling", Bind.EMPTY, "Press to start/stop cycling").group(Group.General) + .onPress { + if (cycleState != CycleState.IDLE) { + info("Stopped villager cycling.") + switchState(CycleState.IDLE) + } else { + info("Started villager cycling.") + buildTask?.cancel() + buildTask = null + switchState(CycleState.PLACE_LECTERN) + } + } + private val desiredEnchantments by setting("Desired Enchantments", emptySet(), allEnchantments).group(Group.Enchantments) + + private var cycleState = CycleState.IDLE + private var tickCounter = 0 + + private var buildTask: Task<*>? = null + + init { + setDefaultAutomationConfig() + + onEnable { + allEnchantments.clear() + allEnchantments.addAll(getEnchantmentList()) + cycleState = CycleState.IDLE + tickCounter = 0 + } + + onDisable { + cycleState = CycleState.IDLE + tickCounter = 0 + buildTask?.cancel() + buildTask = null + } + + listen { + tickCounter++ + + if (allEnchantments.isEmpty()) { + allEnchantments.addAll(getEnchantmentList()) // Have to load enchantments after we loaded into a world + } + + when (cycleState) { + CycleState.IDLE -> {} + CycleState.PLACE_LECTERN -> handlePlaceLectern() + CycleState.WAIT_LECTERN -> {} + CycleState.OPEN_VILLAGER -> handleOpenVillager() + CycleState.BREAK_LECTERN -> handleBreakLectern() + CycleState.WAIT_BREAK -> {} + } + } + + listen { event -> + if (event.packet !is SetTradeOffersS2CPacket) return@listen + if (cycleState != CycleState.OPEN_VILLAGER) return@listen + + val tradeOfferPacket = event.packet + val trades = tradeOfferPacket.offers + if (trades.isEmpty()) { + logError("Villager has no trades!") + switchState(CycleState.IDLE) + return@listen + } + + for (offer in trades) { + if (offer.isDisabled) continue + + val sellItem = offer.sellItem + if (sellItem.item != Items.ENCHANTED_BOOK) continue + + val storedEnchantments = sellItem.get(DataComponentTypes.STORED_ENCHANTMENTS) ?: continue + + if (logFoundBooks) { + for (entry in storedEnchantments.enchantmentEntries) { + info("Found book: ${entry.key.value().description().string}") + } + } + + findDesiredEnchantment(storedEnchantments)?.let { + info("Found desired enchantment: ${it.description().string}!") + playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP) + switchState(CycleState.IDLE) + return@listen + } + } + + // No desired enchantment found, break lectern and try again + tickCounter = 0 + switchState(CycleState.BREAK_LECTERN) + } + } + + private fun SafeContext.getEnchantmentList(): MutableList { + val enchantments = ArrayList() + for (foo in world.registryManager.getOrThrow(RegistryKeys.ENCHANTMENT)) { + enchantments.add(foo.description.string) + } + return enchantments + } + + private fun SafeContext.handlePlaceLectern() { + player.closeHandledScreen() + + if (lecternPos == BlockPos.ORIGIN) { + logError("Lectern position is not set!") + switchState(CycleState.IDLE) + return + } + + val state = blockState(lecternPos) + + if (!state.isAir) { + if (state.isOf(Blocks.LECTERN)) { + switchState(CycleState.OPEN_VILLAGER) + return + } + logError("Block at lectern position is not air or a lectern!") + switchState(CycleState.IDLE) + return + } + + runSafeAutomated { + buildTask = lecternPos.toStructure(TargetState.Block(Blocks.LECTERN)) + .toBlueprint() + .build(finishOnDone = true) + .finally { + switchState(CycleState.OPEN_VILLAGER) + } + .run() + } + switchState(CycleState.WAIT_LECTERN) + } + + private fun SafeContext.handleOpenVillager() { + if (tickCounter < interactDelay) return + + if (buildTask?.state == Task.State.Running) { + return + } + + // Verify lectern is still present + val state = blockState(lecternPos) + if (state.isAir) { + tickCounter = 0 + switchState(CycleState.PLACE_LECTERN) + return + } + if (!state.isOf(Blocks.LECTERN)) { + logError("Block at lectern position is not a lectern!") + switchState(CycleState.IDLE) + return + } + + val villager = closestEntity(searchRange) + if (villager == null) { + logError("No villager found nearby!") + switchState(CycleState.IDLE) + return + } + + runSafeAutomated { + lookAtEntity(villager)?.let { + rotationRequest { + rotation(it.rotation) + }.submit() + interaction.interactEntityAtLocation(player, villager, it.hit as EntityHitResult?, Hand.MAIN_HAND) + interaction.interactEntity(player, villager, Hand.MAIN_HAND) + player.swingHand(Hand.MAIN_HAND) + } + } + + tickCounter = 0 + } + + private fun SafeContext.handleBreakLectern() { + if (player.currentScreenHandler != player.playerScreenHandler) { + player.closeHandledScreen() + } + + if (tickCounter < breakDelay) return + + val state = blockState(lecternPos) + + if (!state.isAir) { + buildTask = runSafeAutomated { + lecternPos.toStructure(TargetState.Empty) + .build(finishOnDone = true) + .finally { + switchState(CycleState.PLACE_LECTERN) + } + .run() + } + switchState(CycleState.WAIT_BREAK) + return + } + switchState(CycleState.PLACE_LECTERN) + } + + private fun findDesiredEnchantment(enchantments: ItemEnchantmentsComponent): Enchantment? { + if (desiredEnchantments.isEmpty()) return null + + for (entry in enchantments.enchantmentEntries) { + val enchantmentName = entry.key.value().description().string + if (desiredEnchantments.any { it.equals(enchantmentName, ignoreCase = true) }) { + return entry.key.value() + } + } + return null + } + + private fun switchState(newState: CycleState) { + if (cycleState != newState) { + tickCounter = 0 + } + cycleState = newState + } + + private enum class CycleState { + IDLE, + PLACE_LECTERN, + OPEN_VILLAGER, + WAIT_LECTERN, + BREAK_LECTERN, + WAIT_BREAK + } +} \ No newline at end of file From c9dc11dd058d0169e643cb2f7bf8dc72c5adb6ef Mon Sep 17 00:00:00 2001 From: Ic3Tank <61137113+IceTank@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:39:47 +0100 Subject: [PATCH 2/3] Code cleanup --- .../modules/player/AutoVillagerCycle.kt | 87 ++++++++++--------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt b/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt index a0495a9bb..fcce0a802 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt @@ -37,6 +37,7 @@ import com.lambda.task.Task import com.lambda.task.tasks.BuildTask.Companion.build import com.lambda.threading.runSafeAutomated import com.lambda.util.BlockUtils.blockState +import com.lambda.util.BlockUtils.isEmpty import com.lambda.util.Communication.info import com.lambda.util.Communication.logError import com.lambda.util.NamedEnum @@ -56,7 +57,7 @@ import net.minecraft.util.math.BlockPos object AutoVillagerCycle : Module( - name = "AutoVillagerCycler", + name = "AutoVillagerCycle", description = "Automatically cycles librarian villagers with lecterns until a desired enchanted book is found", tag = ModuleTag.PLAYER ) { @@ -74,19 +75,19 @@ object AutoVillagerCycle : Module( private val searchRange by setting("Search Range", 5.0, 1.0..10.0, 0.5, "Range to search for nearby villagers", " blocks").group(Group.General) private val startCyclingBind by setting("Start Cycling", Bind.EMPTY, "Press to start/stop cycling").group(Group.General) .onPress { - if (cycleState != CycleState.IDLE) { + if (cycleState != CycleState.Idle) { info("Stopped villager cycling.") - switchState(CycleState.IDLE) + switchState(CycleState.Idle) } else { info("Started villager cycling.") buildTask?.cancel() buildTask = null - switchState(CycleState.PLACE_LECTERN) + switchState(CycleState.PlaceLectern) } } private val desiredEnchantments by setting("Desired Enchantments", emptySet(), allEnchantments).group(Group.Enchantments) - private var cycleState = CycleState.IDLE + private var cycleState = CycleState.Idle private var tickCounter = 0 private var buildTask: Task<*>? = null @@ -97,12 +98,12 @@ object AutoVillagerCycle : Module( onEnable { allEnchantments.clear() allEnchantments.addAll(getEnchantmentList()) - cycleState = CycleState.IDLE + cycleState = CycleState.Idle tickCounter = 0 } onDisable { - cycleState = CycleState.IDLE + cycleState = CycleState.Idle tickCounter = 0 buildTask?.cancel() buildTask = null @@ -116,24 +117,24 @@ object AutoVillagerCycle : Module( } when (cycleState) { - CycleState.IDLE -> {} - CycleState.PLACE_LECTERN -> handlePlaceLectern() - CycleState.WAIT_LECTERN -> {} - CycleState.OPEN_VILLAGER -> handleOpenVillager() - CycleState.BREAK_LECTERN -> handleBreakLectern() - CycleState.WAIT_BREAK -> {} + CycleState.Idle -> {} + CycleState.PlaceLectern -> handlePlaceLectern() + CycleState.WaitLectern -> {} + CycleState.OpenVillager -> handleOpenVillager() + CycleState.BreakLectern -> handleBreakLectern() + CycleState.WaitBreak -> {} } } listen { event -> if (event.packet !is SetTradeOffersS2CPacket) return@listen - if (cycleState != CycleState.OPEN_VILLAGER) return@listen + if (cycleState != CycleState.OpenVillager) return@listen val tradeOfferPacket = event.packet val trades = tradeOfferPacket.offers if (trades.isEmpty()) { logError("Villager has no trades!") - switchState(CycleState.IDLE) + switchState(CycleState.Idle) return@listen } @@ -154,14 +155,14 @@ object AutoVillagerCycle : Module( findDesiredEnchantment(storedEnchantments)?.let { info("Found desired enchantment: ${it.description().string}!") playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP) - switchState(CycleState.IDLE) + switchState(CycleState.Idle) return@listen } } // No desired enchantment found, break lectern and try again tickCounter = 0 - switchState(CycleState.BREAK_LECTERN) + switchState(CycleState.BreakLectern) } } @@ -178,19 +179,19 @@ object AutoVillagerCycle : Module( if (lecternPos == BlockPos.ORIGIN) { logError("Lectern position is not set!") - switchState(CycleState.IDLE) + switchState(CycleState.Idle) return } val state = blockState(lecternPos) - if (!state.isAir) { + if (!state.isEmpty) { if (state.isOf(Blocks.LECTERN)) { - switchState(CycleState.OPEN_VILLAGER) + switchState(CycleState.OpenVillager) return } logError("Block at lectern position is not air or a lectern!") - switchState(CycleState.IDLE) + switchState(CycleState.Idle) return } @@ -199,11 +200,11 @@ object AutoVillagerCycle : Module( .toBlueprint() .build(finishOnDone = true) .finally { - switchState(CycleState.OPEN_VILLAGER) + switchState(CycleState.OpenVillager) } .run() } - switchState(CycleState.WAIT_LECTERN) + switchState(CycleState.WaitLectern) } private fun SafeContext.handleOpenVillager() { @@ -215,32 +216,34 @@ object AutoVillagerCycle : Module( // Verify lectern is still present val state = blockState(lecternPos) - if (state.isAir) { + if (state.isEmpty) { tickCounter = 0 - switchState(CycleState.PLACE_LECTERN) + switchState(CycleState.PlaceLectern) return } if (!state.isOf(Blocks.LECTERN)) { logError("Block at lectern position is not a lectern!") - switchState(CycleState.IDLE) + switchState(CycleState.Idle) return } val villager = closestEntity(searchRange) if (villager == null) { logError("No villager found nearby!") - switchState(CycleState.IDLE) + switchState(CycleState.Idle) return } runSafeAutomated { lookAtEntity(villager)?.let { - rotationRequest { + val done = rotationRequest { rotation(it.rotation) - }.submit() - interaction.interactEntityAtLocation(player, villager, it.hit as EntityHitResult?, Hand.MAIN_HAND) - interaction.interactEntity(player, villager, Hand.MAIN_HAND) - player.swingHand(Hand.MAIN_HAND) + }.submit().done + if (done) { + interaction.interactEntityAtLocation(player, villager, it.hit as EntityHitResult?, Hand.MAIN_HAND) + interaction.interactEntity(player, villager, Hand.MAIN_HAND) + player.swingHand(Hand.MAIN_HAND) + } } } @@ -256,19 +259,19 @@ object AutoVillagerCycle : Module( val state = blockState(lecternPos) - if (!state.isAir) { + if (!state.isEmpty) { buildTask = runSafeAutomated { lecternPos.toStructure(TargetState.Empty) .build(finishOnDone = true) .finally { - switchState(CycleState.PLACE_LECTERN) + switchState(CycleState.PlaceLectern) } .run() } - switchState(CycleState.WAIT_BREAK) + switchState(CycleState.WaitBreak) return } - switchState(CycleState.PLACE_LECTERN) + switchState(CycleState.PlaceLectern) } private fun findDesiredEnchantment(enchantments: ItemEnchantmentsComponent): Enchantment? { @@ -291,11 +294,11 @@ object AutoVillagerCycle : Module( } private enum class CycleState { - IDLE, - PLACE_LECTERN, - OPEN_VILLAGER, - WAIT_LECTERN, - BREAK_LECTERN, - WAIT_BREAK + Idle, + PlaceLectern, + OpenVillager, + WaitLectern, + BreakLectern, + WaitBreak } } \ No newline at end of file From 7bf762b42a68ec03008a8184a2cc46e5c60edb51 Mon Sep 17 00:00:00 2001 From: Ic3Tank <61137113+IceTank@users.noreply.github.com> Date: Wed, 11 Mar 2026 21:43:51 +0100 Subject: [PATCH 3/3] Hide some automation groups --- .../com/lambda/module/modules/player/AutoVillagerCycle.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt b/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt index fcce0a802..f35c809ef 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/AutoVillagerCycle.kt @@ -18,6 +18,7 @@ package com.lambda.module.modules.player import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig +import com.lambda.config.applyEdits import com.lambda.config.settings.complex.Bind import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress import com.lambda.context.SafeContext @@ -93,7 +94,11 @@ object AutoVillagerCycle : Module( private var buildTask: Task<*>? = null init { - setDefaultAutomationConfig() + setDefaultAutomationConfig() { + applyEdits { + hideAllGroupsExcept(rotationConfig, inventoryConfig, breakConfig, interactConfig, buildConfig) + } + } onEnable { allEnchantments.clear()