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 baritone.api.pathing.goals.GoalBlock
21+ import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig
22+ import com.lambda.config.applyEdits
23+ import com.lambda.config.groups.WorldLineSettings
24+ import com.lambda.config.settings.complex.Bind
25+ import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress
26+ import com.lambda.config.settings.complex.KeybindSetting.Companion.onRelease
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.ImmediateRenderer.Companion.immediateRenderer
31+ import com.lambda.graphics.util.DirectionMask.buildSideMesh
32+ import com.lambda.interaction.BaritoneManager
33+ import com.lambda.interaction.construction.verify.TargetState
34+ import com.lambda.interaction.managers.hotbar.HotbarRequest
35+ import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest
36+ import com.lambda.interaction.material.StackSelection.Companion.selectStack
37+ import com.lambda.module.Module
38+ import com.lambda.module.modules.player.AutoPortal.PosHandler.currAnchorPos
39+ import com.lambda.module.modules.player.AutoPortal.PosHandler.obiPositions
40+ import com.lambda.module.modules.player.AutoPortal.PosHandler.portalPositions
41+ import com.lambda.module.modules.player.AutoPortal.PosHandler.prevAnchorPos
42+ import com.lambda.module.tag.ModuleTag
43+ import com.lambda.task.RootTask.run
44+ import com.lambda.task.Task
45+ import com.lambda.task.tasks.BuildTask.Companion.build
46+ import com.lambda.util.BlockUtils.blockState
47+ import com.lambda.util.BlockUtils.isEmpty
48+ import com.lambda.util.BlockUtils.isNotEmpty
49+ import com.lambda.util.InputUtils.isSatisfied
50+ import com.lambda.util.NamedEnum
51+ import com.lambda.util.extension.blockColor
52+ import com.lambda.util.extension.tickDelta
53+ import com.lambda.util.math.lerp
54+ import com.lambda.util.math.setAlpha
55+ import com.lambda.util.math.vec3d
56+ import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots
57+ import com.lambda.util.player.SlotUtils.hotbarSlots
58+ import net.minecraft.block.Blocks
59+ import net.minecraft.item.FlintAndSteelItem
60+ import net.minecraft.item.Items
61+ import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket
62+ import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket
63+ import net.minecraft.util.Hand
64+ import net.minecraft.util.hit.BlockHitResult
65+ import net.minecraft.util.math.BlockPos
66+ import net.minecraft.util.math.Box
67+ import net.minecraft.util.math.Direction
68+ import net.minecraft.util.math.Vec3d
69+
70+ object AutoPortal : Module(
71+ name = " AutoPortal" ,
72+ description = " Automatically places and lights a nether portal" ,
73+ tag = ModuleTag .PLAYER
74+ ) {
75+ private enum class Group (override val displayName : String ) : NamedEnum {
76+ General (" General" ),
77+ Render (" Render" )
78+ }
79+
80+ private val previewPlace by setting(" Preview Place" , Bind .EMPTY , " The keybind to preview the portal placement and subsequentially place the portal" ).group(Group .General )
81+ .onPress { preview = true }
82+ .onRelease {
83+ preview = false
84+ buildTask?.cancel()
85+ val posStateMap =
86+ obiPositions.associateWith {
87+ TargetState .Block (Blocks .OBSIDIAN )
88+ } + portalPositions.associateWith {
89+ TargetState .Air
90+ }
91+ // ToDo: implement non placement interactions like flint and steel in the build sim, in turn, simulating portal lighting too.
92+ // + portalPositions.associateWith {
93+ // TargetState.Block(Blocks.NETHER_PORTAL)
94+ // }
95+ buildTask = posStateMap
96+ .build()
97+ .thenOrNull {
98+ if (light) LightTask (currAnchorPos.up(), walkIn)
99+ else null
100+ }
101+ .finally {
102+ buildTask = null
103+ }
104+ .run ()
105+ }
106+ private val corners by setting(" Corners" , false ).group(Group .General )
107+ private val light by setting(" Light" , true , " Attempts to automatically light the portal after building" ).group(Group .General )
108+ private val walkIn by setting(" Walk In" , true , " Automatically paths into the portal with baritone" ) { light }.group(Group .General )
109+ private val inventory by setting(" Inventory" , true , " Allows access to the players inventory when retrieving a flint and steel for lighting the portal" ).group(Group .General )
110+ private val forwardOffset by setting(" Forward Offset" , 3 , 0 .. 10 ).group(Group .General )
111+ private val sidewaysOffset by setting(" Sideways Offset" , 0 , - 5 .. 5 ).group(Group .General )
112+ private val yOffset by setting(" Y Offset" , 0 , - 5 .. 5 ).group(Group .General )
113+ private val lockToGround by setting(" Lock To Ground" , true ).group(Group .General )
114+ private val allowUpwardShift by setting(" Allow Upward Shift" , true , " Allows shifting the portal up to find ground when it would be placed inside blocks" ) { lockToGround }.group(Group .General )
115+
116+ private val renders by setting(" Renders" , true ).group(Group .Render )
117+ private val interpolate by setting(" Interpolate" , true , " Interpolates the portal renders from position to position" ) { renders }.group(Group .Render )
118+ private val fillAlpha by setting(" Fill Alpha" , 0.3 , 0.0 .. 1.0 , 0.01 ) { renders }.group(Group .Render )
119+ private val depthTest by setting(" Depth Test" , false ) { renders }.group(Group .Render )
120+ private val outlineConfig = WorldLineSettings (c = this , baseGroup = arrayOf(Group .Render )) { renders }.apply {
121+ applyEdits {
122+ hide(::startColor, ::endColor)
123+ }
124+ }
125+
126+ private var preview = false
127+ private var buildTask: Task <* >? = null
128+
129+ init {
130+ setDefaultAutomationConfig {
131+ applyEdits {
132+ hideGroup(eatConfig)
133+ hotbarConfig::tickStageMask.edit {
134+ defaultValue(mutableSetOf (TickEvent .Pre , TickEvent .Input .Post ))
135+ }
136+ }
137+ }
138+
139+ listen<TickEvent .Pre > {
140+ PosHandler .tick()
141+ }
142+
143+ immediateRenderer(" AutoPortal Immediate Renderer" , { depthTest }) { safeContext ->
144+ if (! renders || ! preview) return @immediateRenderer
145+ with (safeContext) {
146+ val obiColor = blockColor(Blocks .OBSIDIAN .defaultState, BlockPos .ORIGIN )
147+ obiPositions
148+ .map {
149+ val box = Box (it).let { box ->
150+ if (interpolate) {
151+ val offset = lerp(
152+ 1.0 - mc.tickDelta,
153+ Vec3d .ZERO ,
154+ prevAnchorPos.subtract(currAnchorPos).vec3d
155+ )
156+ box.offset(offset)
157+ } else box
158+ }
159+ Pair (it, box)
160+ }
161+ .forEach { posAndBox ->
162+ box(posAndBox.second, outlineConfig) {
163+ colors(obiColor.setAlpha(fillAlpha), obiColor)
164+ hideSides(buildSideMesh(posAndBox.first) { it in obiPositions }.inv ())
165+ }
166+ }
167+ }
168+ }
169+ }
170+
171+ private object PosHandler {
172+ var currAnchorPos: BlockPos = BlockPos .ORIGIN
173+ private set
174+ var prevAnchorPos = currAnchorPos
175+ private set
176+
177+ var obiPositions = emptyList<BlockPos >()
178+ private set
179+ var portalPositions = emptyList<BlockPos >()
180+ private set
181+
182+ private val originObiPositions = getOriginObiPositions()
183+ private val originObiPositionsWithCorners = getOriginObiPositions(true )
184+ private val originPortalPositions = getOriginPortalPositions()
185+
186+ context(safeContext: SafeContext )
187+ fun tick () =
188+ with (safeContext) {
189+ if (! previewPlace.isSatisfied()) return @with
190+ val offsetDir = player.horizontalFacing
191+
192+ val baseAnchorPos = player.blockPos
193+ .offset(offsetDir, forwardOffset)
194+ .offset(offsetDir.rotateYClockwise(), sidewaysOffset)
195+
196+ val lockedAnchorPos =
197+ if (lockToGround) lockToGround(baseAnchorPos)
198+ else baseAnchorPos
199+
200+ val yOffsetAnchorPos = lockedAnchorPos?.offset(Direction .UP , yOffset)
201+
202+ if (yOffsetAnchorPos == currAnchorPos || yOffsetAnchorPos == null ) {
203+ prevAnchorPos = currAnchorPos
204+ return @with
205+ }
206+
207+ prevAnchorPos = currAnchorPos
208+ currAnchorPos = yOffsetAnchorPos
209+ val originObi =
210+ if (corners) originObiPositionsWithCorners
211+ else originObiPositions
212+ obiPositions = originObi
213+ .rotatedTo(offsetDir)
214+ .map { it.add(yOffsetAnchorPos) }
215+ portalPositions = originPortalPositions
216+ .rotatedTo(offsetDir)
217+ .map { it.add(yOffsetAnchorPos) }
218+ }
219+
220+ private fun SafeContext.lockToGround (pos : BlockPos ): BlockPos ? {
221+ var scanPos = pos
222+ val upShifting = blockState(scanPos).isNotEmpty && allowUpwardShift
223+ if (upShifting) {
224+ while (blockState(scanPos).isNotEmpty && scanPos.y < 320 ) {
225+ scanPos = scanPos.up()
226+ }
227+ }
228+ if (! upShifting || scanPos.y >= 320 ) {
229+ scanPos = pos
230+ while (blockState(scanPos.down()).isEmpty && scanPos.y > - 64 ) {
231+ scanPos = scanPos.down()
232+ }
233+ if (scanPos.y <= - 64 ) return null
234+ }
235+ return scanPos
236+ }
237+
238+ private fun List<BlockPos>.rotatedTo (direction : Direction ): List <BlockPos > =
239+ map { pos ->
240+ when (direction) {
241+ Direction .EAST -> pos
242+ Direction .SOUTH -> BlockPos (pos.z - 1 , pos.y, - pos.x)
243+ Direction .WEST -> BlockPos (- pos.x, pos.y, - pos.z)
244+ else -> BlockPos (- pos.z + 1 , pos.y, pos.x)
245+ }
246+ }
247+
248+ private fun getOriginObiPositions (corners : Boolean = false) =
249+ buildList {
250+ (- 1 .. 2 ).forEach { x ->
251+ (0 .. 4 ).forEach { y ->
252+ if (x > - 1 && x < 2 && y > 0 && y < 4 ) return @forEach
253+ if (! corners && (x == - 1 || x == 2 ) && (y == 0 || y == 4 )) return @forEach
254+ add(BlockPos (0 , y, x))
255+ }
256+ }
257+ }
258+
259+ private fun getOriginPortalPositions () =
260+ buildList {
261+ (0 .. 1 ).forEach { x ->
262+ (1 .. 3 ).forEach { y ->
263+ add(BlockPos (0 , y, x))
264+ }
265+ }
266+ }
267+ }
268+
269+ private class LightTask (
270+ private val pos : BlockPos ,
271+ private val walkIn : Boolean
272+ ) : Task<Unit>() {
273+ override val name = " Lighting portal at $pos "
274+
275+ init {
276+ listen<TickEvent .Pre > {
277+ withFlintAndSteel {
278+ swapPacket()
279+ interaction.sendSequencedPacket(world) { sequence ->
280+ PlayerInteractBlockC2SPacket (
281+ Hand .OFF_HAND ,
282+ BlockHitResult (
283+ pos.down().toCenterPos(),
284+ Direction .UP ,
285+ pos,
286+ false ,
287+ false
288+ ),
289+ sequence
290+ )
291+ }
292+ swapPacket()
293+ if (walkIn) {
294+ BaritoneManager .setGoalAndPath(GoalBlock (currAnchorPos.up()))
295+ }
296+ success()
297+ }
298+ }
299+ }
300+
301+ private fun SafeContext.swapPacket () =
302+ connection.sendPacket(
303+ PlayerActionC2SPacket (
304+ PlayerActionC2SPacket .Action .SWAP_ITEM_WITH_OFFHAND ,
305+ BlockPos .ORIGIN ,
306+ Direction .DOWN
307+ )
308+ )
309+
310+ private fun SafeContext.withFlintAndSteel (block : SafeContext .() -> Unit ) {
311+ if (player.mainHandStack.item == Items .FLINT_AND_STEEL ) {
312+ block()
313+ return
314+ }
315+
316+ val sel = selectStack(1 ) { isItem<FlintAndSteelItem >() }
317+
318+ val hotbarStack = sel.filterSlots(player.hotbarSlots).firstOrNull()
319+ if (hotbarStack != null ) {
320+ val request = HotbarRequest (
321+ hotbarStack.index,
322+ this @AutoPortal,
323+ keepTicks = 0
324+ ).submit(queueIfMismatchedStage = false )
325+ if (request.done) block()
326+ return
327+ }
328+
329+ val invSlot =
330+ if (inventory) sel.filterSlots(player.hotbarAndInventorySlots).firstOrNull()
331+ else null
332+ if (invSlot == null ) {
333+ failure(" No Flint and Steel!" )
334+ return
335+ }
336+ val hotbarSlotToSwapWith =
337+ player.hotbarSlots.find { slot ->
338+ slot.stack.isEmpty
339+ }?.index ? : 8
340+
341+ inventoryRequest {
342+ swap(invSlot.id, hotbarSlotToSwapWith)
343+ action {
344+ val request = HotbarRequest (
345+ hotbarSlotToSwapWith,
346+ this @AutoPortal,
347+ keepTicks = 0 ,
348+ nowOrNothing = true
349+ ).submit(queueIfMismatchedStage = false )
350+ if (request.done) {
351+ block()
352+ }
353+ }
354+ swap(invSlot.id, hotbarSlotToSwapWith)
355+ }.submit()
356+ }
357+ }
358+ }
0 commit comments