Skip to content

Commit fae1aad

Browse files
authored
Feature: EnderChestFarmer (#346)
1 parent 327a206 commit fae1aad

4 files changed

Lines changed: 225 additions & 1 deletion

File tree

‎src/main/kotlin/com/lambda/config/blocks/BreakConfig.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ interface BreakConfig : ActionConfig {
5858
val efficientOnly: Boolean
5959
val suitableToolsOnly: Boolean
6060
val forceSilkTouch: Boolean
61+
val avoidSilkTouch: Boolean
6162
val forceFortunePickaxe: Boolean
6263
val minFortuneLevel: Int
6364

‎src/main/kotlin/com/lambda/config/blocks/BreakSettings.kt‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.lambda.config.blocks.BreakConfig.SwapMode
2828
import com.lambda.config.blocks.BreakConfig.SwingMode
2929
import com.lambda.config.blocks.BreakConfig.WhitelistMode
3030
import com.lambda.config.blocks.BuildConfig.SwingType
31+
import com.lambda.config.entries.Setting.Companion.onValueChangeUnsafe
3132
import com.lambda.config.hide
3233
import com.lambda.config.withEdits
3334
import com.lambda.event.events.TickEvent
@@ -72,7 +73,10 @@ class BreakSettings(override val c: Config) : BreakConfig, ConfigBlock {
7273
// Tool
7374
override val efficientOnly by c.setting("Efficient Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { swapMode.isEnabled() }
7475
override val suitableToolsOnly by c.setting("Suitable Tools Only", true, "Only use tools suitable for the given block (will get the item drop)") { swapMode.isEnabled() }
75-
override val forceSilkTouch by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { swapMode.isEnabled() }
76+
override var forceSilkTouch: Boolean by c.setting("Force Silk Touch", false, "Force silk touch when breaking blocks") { swapMode.isEnabled() }
77+
.onValueChangeUnsafe { _, to -> if (to) avoidSilkTouch = false }
78+
override var avoidSilkTouch: Boolean by c.setting("Avoid Silk Touch", false, "Never use silk touch tools when breaking blocks") { swapMode.isEnabled() }
79+
.onValueChangeUnsafe { _, to -> if (to) forceSilkTouch = false }
7680
override val forceFortunePickaxe by c.setting("Force Fortune Pickaxe", false, "Force fortune pickaxe when breaking blocks") { swapMode.isEnabled() }
7781
override val minFortuneLevel by c.setting("Min Fortune Level", 1, 1..3, 1, "The minimum fortune level to use") { swapMode.isEnabled() && forceFortunePickaxe }
7882

‎src/main/kotlin/com/lambda/interaction/construction/simulation/sims/BreakSim.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,8 @@ class BreakSim internal constructor(simInfo: SimInfo)
156156
isSuitableForBreaking(state)
157157
}.andIf(breakConfig.forceSilkTouch) {
158158
hasEnchantment(Enchantments.SILK_TOUCH)
159+
}.andIf(breakConfig.avoidSilkTouch) {
160+
hasEnchantment(Enchantments.SILK_TOUCH).not()
159161
}.andIf(breakConfig.forceFortunePickaxe) {
160162
hasEnchantment(Enchantments.FORTUNE)
161163
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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.world
19+
20+
import com.lambda.config.Group
21+
import com.lambda.config.automation.AutomationConfig.Companion.setDefaultAutomationConfig
22+
import com.lambda.config.editSetting
23+
import com.lambda.config.editTypedSettings
24+
import com.lambda.config.hideAllExcept
25+
import com.lambda.config.withEdits
26+
import com.lambda.context.AutomatedSafeContext
27+
import com.lambda.context.SafeContext
28+
import com.lambda.event.events.TickEvent
29+
import com.lambda.event.listener.SafeListener.Companion.listen
30+
import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer
31+
import com.lambda.interaction.construction.blueprint.Blueprint.Companion.toStructure
32+
import com.lambda.interaction.construction.simulation.context.BuildContext
33+
import com.lambda.interaction.construction.simulation.result.BuildResult
34+
import com.lambda.interaction.construction.simulation.result.Dependent
35+
import com.lambda.interaction.construction.simulation.result.results.GenericResult
36+
import com.lambda.interaction.construction.simulation.sim
37+
import com.lambda.interaction.construction.verify.TargetState
38+
import com.lambda.interaction.handlers.ContainerHandler.transfer
39+
import com.lambda.interaction.handlers.breaking.RebreakHandler
40+
import com.lambda.interaction.managers.breaking.BreakRequest.Companion.breakRequest
41+
import com.lambda.interaction.managers.interacting.InteractRequest.Companion.interactRequest
42+
import com.lambda.interaction.material.StackSelection.Companion.select
43+
import com.lambda.interaction.material.container.containers.HotbarContainer
44+
import com.lambda.module.Module
45+
import com.lambda.module.tag.ModuleTag
46+
import com.lambda.threading.runSafeAutomated
47+
import com.lambda.util.BlockUtils.blockState
48+
import com.lambda.util.CommunicationUtils.info
49+
import com.lambda.util.CommunicationUtils.logError
50+
import com.lambda.util.EnchantmentUtils.getEnchantment
51+
import com.lambda.util.item.ItemStackUtils.empty
52+
import com.lambda.util.item.ItemStackUtils.hasSpace
53+
import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks
54+
import net.minecraft.block.BlockState
55+
import net.minecraft.block.Blocks
56+
import net.minecraft.enchantment.Enchantments
57+
import net.minecraft.item.Items
58+
import net.minecraft.util.math.BlockPos
59+
import java.awt.Color
60+
import java.util.concurrent.ConcurrentLinkedQueue
61+
62+
@Suppress("unused")
63+
object EnderChestFarmer : Module(
64+
name = "EnderChestFarmer",
65+
description = "Farms obsidian using ender chests",
66+
tag = ModuleTag.WORLD
67+
) {
68+
private const val RENDER_GROUP = "Render"
69+
70+
@Group(RENDER_GROUP) private val renderFarmPos by setting("Render Farm Pos", true, "Outlines the position ender chests are placed and broken at")
71+
@Group(RENDER_GROUP) private val farmPosColor by setting("Farm Pos Color", Color(155, 0, 255), "The outline color of the farm position") { renderFarmPos }
72+
73+
private val pendingActions = ConcurrentLinkedQueue<BuildContext>()
74+
75+
private var farmPos: BlockPos? = null
76+
private var startObsidian = 0
77+
78+
private val SafeContext.obsidianCount
79+
get() = player.hotbarAndInventoryStacks.filter { it.item == Items.OBSIDIAN }.sumOf { it.count }
80+
81+
private val SafeContext.inventoryFull
82+
get() = player.hotbarAndInventoryStacks.let { stacks ->
83+
stacks.empty == 0 && stacks.none { it.item == Items.OBSIDIAN && it.hasSpace }
84+
}
85+
86+
private val SafeContext.hasEnderChests
87+
get() = Items.ENDER_CHEST.select().filterStacks(player.hotbarAndInventoryStacks).isNotEmpty()
88+
89+
private val SafeContext.missingPickaxe: String?
90+
get() {
91+
val silkTouch = when {
92+
breakConfig.avoidSilkTouch -> false
93+
breakConfig.forceSilkTouch -> true
94+
else -> null
95+
}
96+
val found = player.hotbarAndInventoryStacks.any { stack ->
97+
stack.isSuitableFor(Blocks.ENDER_CHEST.defaultState) &&
98+
(silkTouch == null || (stack.getEnchantment(Enchantments.SILK_TOUCH) > 0) == silkTouch)
99+
}
100+
return if (found) null
101+
else when (silkTouch) {
102+
true -> "You need a pickaxe with Silk Touch"
103+
false -> "You need a pickaxe without Silk Touch"
104+
null -> "You need a pickaxe"
105+
}
106+
}
107+
108+
private val BlockState.farmable
109+
get() = isOf(Blocks.ENDER_CHEST) || isReplaceable
110+
111+
init {
112+
setDefaultAutomationConfig()
113+
.withEdits {
114+
hideAllExcept(::buildConfig, ::breakConfig, ::interactConfig, ::rotationConfig, ::hotbarConfig, ::inventoryConfig)
115+
buildConfig.apply {
116+
editTypedSettings(::pathing, ::collectDrops, ::spleefEntities) {
117+
defaultValue(false)
118+
hide()
119+
}
120+
}
121+
breakConfig.apply {
122+
::rebreak.editSetting {
123+
defaultValue(true)
124+
hide()
125+
}
126+
::avoidSilkTouch.editSetting { defaultValue(true) }
127+
editTypedSettings(::doubleBreak, ::avoidFluids, ::avoidSupporting) { defaultValue(false) }
128+
}
129+
hotbarConfig::keepTicks.editSetting { defaultValue(0) }
130+
}
131+
132+
onEnable {
133+
val pos = player.blockPos.offset(player.horizontalFacing)
134+
if (!blockState(pos).farmable) {
135+
this@EnderChestFarmer.logError("The block in front of you must be air or an ender chest")
136+
disable()
137+
return@onEnable
138+
}
139+
missingPickaxe?.let {
140+
this@EnderChestFarmer.logError(it)
141+
disable()
142+
return@onEnable
143+
}
144+
farmPos = pos
145+
startObsidian = obsidianCount
146+
}
147+
148+
onDisable {
149+
farmPos?.let { pos ->
150+
this@EnderChestFarmer.info("Mined ${obsidianCount - startObsidian} obsidian")
151+
if (RebreakHandler.reBreak?.context?.blockPos == pos) RebreakHandler.clearRebreak()
152+
}
153+
farmPos = null
154+
pendingActions.clear()
155+
}
156+
157+
listen<TickEvent.Input.Post> {
158+
val pos = farmPos ?: return@listen
159+
if (inventoryFull) {
160+
this@EnderChestFarmer.info("Inventory is full")
161+
disable()
162+
return@listen
163+
}
164+
missingPickaxe?.let {
165+
this@EnderChestFarmer.logError(it)
166+
disable()
167+
return@listen
168+
}
169+
val state = blockState(pos)
170+
if (!state.farmable) {
171+
this@EnderChestFarmer.logError("The farm position is blocked")
172+
disable()
173+
return@listen
174+
}
175+
runSafeAutomated {
176+
if (state.isOf(Blocks.ENDER_CHEST)) breakEnderChest(pos)
177+
else placeEnderChest(pos)
178+
}
179+
}
180+
181+
tickedRenderer("EnderChestFarmer Ticked Renderer") {
182+
if (!renderFarmPos) return@tickedRenderer
183+
farmPos?.let { pos ->
184+
box(pos) {
185+
hideFill()
186+
outlineColor(farmPosColor)
187+
}
188+
}
189+
}
190+
}
191+
192+
private fun AutomatedSafeContext.breakEnderChest(pos: BlockPos) {
193+
val results = pos.toStructure(TargetState.Empty).sim()
194+
if (transferMissingItem(results)) return
195+
results.breakRequest(pendingActions)?.submit()
196+
}
197+
198+
private fun AutomatedSafeContext.placeEnderChest(pos: BlockPos) {
199+
if (!hasEnderChests) {
200+
this@EnderChestFarmer.info("Out of ender chests")
201+
disable()
202+
return
203+
}
204+
val results = pos.toStructure(TargetState.Block(Blocks.ENDER_CHEST)).sim()
205+
if (transferMissingItem(results)) return
206+
results.interactRequest(pendingActions)?.submit()
207+
}
208+
209+
private fun AutomatedSafeContext.transferMissingItem(results: Set<BuildResult>): Boolean {
210+
val wrongItem = results
211+
.map { if (it is Dependent) it.lastDependency else it }
212+
.filterIsInstance<GenericResult.WrongItemSelection>()
213+
.firstOrNull() ?: return false
214+
wrongItem.neededSelection.transfer(HotbarContainer)
215+
return true
216+
}
217+
}

0 commit comments

Comments
 (0)