Skip to content

Commit a78e86f

Browse files
authored
AutoVillagerCycle module (#263)
1 parent 84f8f91 commit a78e86f

File tree

1 file changed

+326
-0
lines changed

1 file changed

+326
-0
lines changed
Lines changed: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
1+
/*
2+
* Copyright 2026 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.lambda.module.modules.player
19+
20+
import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig
21+
import com.lambda.config.applyEdits
22+
import com.lambda.config.settings.complex.Bind
23+
import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
24+
import com.lambda.context.SafeContext
25+
import com.lambda.event.events.PacketEvent
26+
import com.lambda.event.events.TickEvent
27+
import com.lambda.event.listener.SafeListener.Companion.listen
28+
import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
29+
import com.lambda.interaction.construction.blueprint.StaticBlueprint.Companion.toBlueprint
30+
import com.lambda.interaction.construction.verify.TargetState
31+
import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
32+
import com.lambda.interaction.managers.rotating.visibilty.lookAtEntity
33+
import com.lambda.module.Module
34+
import com.lambda.module.tag.ModuleTag
35+
import com.lambda.sound.SoundManager.playSound
36+
import com.lambda.task.RootTask.run
37+
import com.lambda.task.Task
38+
import com.lambda.task.tasks.BuildTask.Companion.build
39+
import com.lambda.threading.runSafeAutomated
40+
import com.lambda.util.BlockUtils.blockState
41+
import com.lambda.util.BlockUtils.isEmpty
42+
import com.lambda.util.Communication.info
43+
import com.lambda.util.Communication.logError
44+
import com.lambda.util.EnchantmentUtils.forEachEnchantment
45+
import com.lambda.util.NamedEnum
46+
import com.lambda.util.world.closestEntity
47+
import net.minecraft.block.Blocks
48+
import net.minecraft.component.DataComponentTypes
49+
import net.minecraft.component.type.ItemEnchantmentsComponent
50+
import net.minecraft.enchantment.Enchantment
51+
import net.minecraft.entity.passive.VillagerEntity
52+
import net.minecraft.item.ItemStack
53+
import net.minecraft.item.Items
54+
import net.minecraft.network.packet.s2c.play.SetTradeOffersS2CPacket
55+
import net.minecraft.registry.RegistryKeys
56+
import net.minecraft.sound.SoundEvents
57+
import net.minecraft.util.Hand
58+
import net.minecraft.util.hit.EntityHitResult
59+
import net.minecraft.util.math.BlockPos
60+
61+
62+
object AutoVillagerCycle : Module(
63+
name = "AutoVillagerCycle",
64+
description = "Automatically cycles librarian villagers with lecterns until a desired enchanted book is found",
65+
tag = ModuleTag.PLAYER
66+
) {
67+
private enum class Group(override val displayName: String) : NamedEnum {
68+
General("General"),
69+
Enchantments("Enchantments")
70+
}
71+
72+
private val allEnchantments = ArrayList<String>()
73+
74+
private val lecternPos by setting("Lectern Pos", BlockPos.ORIGIN, "Position where the lectern should be placed/broken").group(Group.General)
75+
private val logFoundBooks by setting("Log Found Books", true, "Log all enchanted books found during cycling").group(Group.General)
76+
private val interactDelay by setting("Interact Delay", 20, 1..40, 1, "Ticks to wait before interacting with the villager", " ticks").group(Group.General)
77+
private val breakDelay by setting("Break Delay", 5, 1..20, 1, "Ticks to wait after breaking the lectern", " ticks").group(Group.General)
78+
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)
79+
private val startCyclingBind by setting("Start Cycling", Bind.EMPTY, "Press to start/stop cycling").group(Group.General)
80+
.onPress {
81+
if (cycleState != CycleState.Idle) {
82+
info("Stopped villager cycling.")
83+
switchState(CycleState.Idle)
84+
} else {
85+
info("Started villager cycling.")
86+
buildTask?.cancel()
87+
buildTask = null
88+
switchState(CycleState.PlaceLectern)
89+
}
90+
}
91+
private val desiredEnchantments by setting("Desired Enchantments", emptySet(), allEnchantments).group(Group.Enchantments)
92+
private val minLevel by setting("Min Level", 1, 1..5, 1, "Minimum enchantment level to look for").group(Group.Enchantments)
93+
94+
private var cycleState = CycleState.Idle
95+
private var tickCounter = 0
96+
97+
private var buildTask: Task<*>? = null
98+
99+
init {
100+
setDefaultAutomationConfig() {
101+
applyEdits {
102+
hideAllGroupsExcept(rotationConfig, inventoryConfig, breakConfig, interactConfig, buildConfig)
103+
}
104+
}
105+
106+
onEnable {
107+
allEnchantments.clear()
108+
allEnchantments.addAll(getEnchantmentList())
109+
cycleState = CycleState.Idle
110+
tickCounter = 0
111+
}
112+
113+
onDisable {
114+
cycleState = CycleState.Idle
115+
tickCounter = 0
116+
buildTask?.cancel()
117+
buildTask = null
118+
}
119+
120+
listen<TickEvent.Pre> {
121+
tickCounter++
122+
123+
if (allEnchantments.isEmpty()) {
124+
allEnchantments.addAll(getEnchantmentList()) // Have to load enchantments after we loaded into a world
125+
}
126+
127+
when (cycleState) {
128+
CycleState.Idle -> {}
129+
CycleState.PlaceLectern -> handlePlaceLectern()
130+
CycleState.WaitLectern -> {}
131+
CycleState.OpenVillager -> handleOpenVillager()
132+
CycleState.BreakLectern -> handleBreakLectern()
133+
CycleState.WaitBreak -> {}
134+
}
135+
}
136+
137+
listen<PacketEvent.Receive.Pre> { event ->
138+
if (event.packet !is SetTradeOffersS2CPacket) return@listen
139+
if (cycleState != CycleState.OpenVillager) return@listen
140+
141+
val tradeOfferPacket = event.packet
142+
val trades = tradeOfferPacket.offers
143+
if (trades.isEmpty()) {
144+
logError("Villager has no trades!")
145+
switchState(CycleState.Idle)
146+
return@listen
147+
}
148+
149+
var bookFound = false
150+
for (offer in trades) {
151+
if (offer.isDisabled) continue
152+
153+
val sellItem = offer.sellItem
154+
if (sellItem.item != Items.ENCHANTED_BOOK) continue
155+
156+
if (logFoundBooks) {
157+
val storedEnchantments = sellItem.get(DataComponentTypes.STORED_ENCHANTMENTS)
158+
val foundEnchantments = mutableListOf<String>()
159+
for (entry in storedEnchantments?.enchantmentEntries ?: emptyList()) {
160+
foundEnchantments.add(entry.key.value().description().string)
161+
}
162+
if (foundEnchantments.isNotEmpty()) {
163+
bookFound = true
164+
info("Found book(s): ${foundEnchantments.joinToString(", ")}")
165+
}
166+
}
167+
168+
findDesiredEnchantment(sellItem)?.let {
169+
info("Found desired enchantment: ${it.description().string}!")
170+
playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP)
171+
switchState(CycleState.Idle)
172+
return@listen
173+
}
174+
}
175+
if (!bookFound && logFoundBooks) {
176+
info("No books found")
177+
}
178+
179+
// No desired enchantment found, break lectern and try again
180+
tickCounter = 0
181+
switchState(CycleState.BreakLectern)
182+
}
183+
}
184+
185+
private fun SafeContext.getEnchantmentList(): MutableList<String> {
186+
val enchantments = ArrayList<String>()
187+
for (foo in world.registryManager.getOrThrow(RegistryKeys.ENCHANTMENT)) {
188+
enchantments.add(foo.description.string)
189+
}
190+
return enchantments
191+
}
192+
193+
private fun SafeContext.handlePlaceLectern() {
194+
player.closeHandledScreen()
195+
196+
if (desiredEnchantments.isEmpty()) {
197+
logError("No desired enchantments set!")
198+
switchState(CycleState.Idle)
199+
return
200+
}
201+
202+
if (lecternPos == BlockPos.ORIGIN) {
203+
logError("Lectern position is not set!")
204+
switchState(CycleState.Idle)
205+
return
206+
}
207+
208+
val state = blockState(lecternPos)
209+
210+
if (!state.isEmpty) {
211+
if (state.isOf(Blocks.LECTERN)) {
212+
switchState(CycleState.OpenVillager)
213+
return
214+
}
215+
logError("Block at lectern position is not air or a lectern!")
216+
switchState(CycleState.Idle)
217+
return
218+
}
219+
220+
runSafeAutomated {
221+
buildTask = lecternPos.toStructure(TargetState.Block(Blocks.LECTERN))
222+
.toBlueprint()
223+
.build(finishOnDone = true)
224+
.finally {
225+
switchState(CycleState.OpenVillager)
226+
}
227+
.run()
228+
}
229+
switchState(CycleState.WaitLectern)
230+
}
231+
232+
private fun SafeContext.handleOpenVillager() {
233+
if (tickCounter < interactDelay) return
234+
235+
if (buildTask?.state == Task.State.Running) {
236+
return
237+
}
238+
239+
// Verify lectern is still present
240+
val state = blockState(lecternPos)
241+
if (state.isEmpty) {
242+
tickCounter = 0
243+
switchState(CycleState.PlaceLectern)
244+
return
245+
}
246+
if (!state.isOf(Blocks.LECTERN)) {
247+
logError("Block at lectern position is not a lectern!")
248+
switchState(CycleState.Idle)
249+
return
250+
}
251+
252+
val villager = closestEntity<VillagerEntity>(searchRange)
253+
if (villager == null) {
254+
logError("No villager found nearby!")
255+
switchState(CycleState.Idle)
256+
return
257+
}
258+
259+
runSafeAutomated {
260+
lookAtEntity(villager)?.let {
261+
val done = rotationRequest {
262+
rotation(it.rotation)
263+
}.submit().done
264+
if (done) {
265+
interaction.interactEntityAtLocation(player, villager, it.hit as EntityHitResult?, Hand.MAIN_HAND)
266+
interaction.interactEntity(player, villager, Hand.MAIN_HAND)
267+
player.swingHand(Hand.MAIN_HAND)
268+
tickCounter = 0
269+
}
270+
}
271+
}
272+
}
273+
274+
private fun SafeContext.handleBreakLectern() {
275+
if (player.currentScreenHandler != player.playerScreenHandler) {
276+
player.closeHandledScreen()
277+
}
278+
279+
if (tickCounter < breakDelay) return
280+
281+
val state = blockState(lecternPos)
282+
283+
if (!state.isEmpty) {
284+
buildTask = runSafeAutomated {
285+
lecternPos.toStructure(TargetState.Empty)
286+
.build(finishOnDone = true)
287+
.finally {
288+
switchState(CycleState.PlaceLectern)
289+
}
290+
.run()
291+
}
292+
switchState(CycleState.WaitBreak)
293+
return
294+
}
295+
switchState(CycleState.PlaceLectern)
296+
}
297+
298+
private fun findDesiredEnchantment(itemStack: ItemStack): Enchantment? {
299+
if (desiredEnchantments.isEmpty()) return null
300+
301+
val enchantments = itemStack.get(DataComponentTypes.STORED_ENCHANTMENTS) ?: return null
302+
enchantments.enchantmentEntries.forEach { (entry, level) ->
303+
val enchantmentName = entry.value().description().string
304+
if (desiredEnchantments.any { it.equals(enchantmentName, ignoreCase = true) && level >= minLevel }) {
305+
return entry.value()
306+
}
307+
}
308+
return null
309+
}
310+
311+
private fun switchState(newState: CycleState) {
312+
if (cycleState != newState) {
313+
tickCounter = 0
314+
}
315+
cycleState = newState
316+
}
317+
318+
private enum class CycleState {
319+
Idle,
320+
PlaceLectern,
321+
OpenVillager,
322+
WaitLectern,
323+
BreakLectern,
324+
WaitBreak
325+
}
326+
}

0 commit comments

Comments
 (0)