Skip to content

Commit 7e329e8

Browse files
authored
AutoPortal module (#272)
1 parent 83338a7 commit 7e329e8

1 file changed

Lines changed: 358 additions & 0 deletions

File tree

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
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

Comments
 (0)