diff --git a/src/main/kotlin/com/lambda/config/Configurable.kt b/src/main/kotlin/com/lambda/config/Configurable.kt index e7e5932d2..52fbf56c7 100644 --- a/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/src/main/kotlin/com/lambda/config/Configurable.kt @@ -24,6 +24,7 @@ import com.lambda.Lambda.LOG import com.lambda.config.Configuration.Companion.configurables import com.lambda.config.settings.CharSetting import com.lambda.config.settings.FunctionSetting +import com.lambda.config.settings.GuiButton import com.lambda.config.settings.StringSetting import com.lambda.config.settings.collections.BlockCollectionSetting import com.lambda.config.settings.collections.ClassCollectionSetting @@ -66,6 +67,7 @@ abstract class Configurable( val configuration: Configuration, ) : Jsonable, Nameable { val settings = mutableListOf>() + val otherElements = mutableListOf() val settingGroups = mutableListOf() init { @@ -84,6 +86,10 @@ abstract class Configurable( settings.add(this) } + fun LayoutBuildable.register() = apply { + otherElements.add(this) + } + override fun toJson() = JsonObject().apply { settings.forEach { setting -> @@ -102,6 +108,12 @@ abstract class Configurable( } } + fun button( + name: String, + description: String = "", + onPress: () -> Unit + ) = GuiButton(name, description, onPress).register() + fun setting( name: String, defaultValue: Boolean, diff --git a/src/main/kotlin/com/lambda/config/LayoutBuildable.kt b/src/main/kotlin/com/lambda/config/LayoutBuildable.kt new file mode 100644 index 000000000..a0132bb44 --- /dev/null +++ b/src/main/kotlin/com/lambda/config/LayoutBuildable.kt @@ -0,0 +1,23 @@ +/* + * 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.config + + +interface LayoutBuildable { + fun buildLayout() +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/settings/GuiButton.kt b/src/main/kotlin/com/lambda/config/settings/GuiButton.kt new file mode 100644 index 000000000..cba263798 --- /dev/null +++ b/src/main/kotlin/com/lambda/config/settings/GuiButton.kt @@ -0,0 +1,35 @@ +/* + * 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.config.settings + +import com.lambda.config.LayoutBuildable +import com.lambda.gui.dsl.ImGuiBuilder.button +import com.lambda.util.Describable +import com.lambda.util.Nameable + +class GuiButton( + override val name: String, + override val description: String, + val clickConsumer: () -> Unit? +) : Nameable, Describable, LayoutBuildable { + override fun buildLayout() { + button(name) { + clickConsumer() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/gui/components/SettingsWidget.kt b/src/main/kotlin/com/lambda/gui/components/SettingsWidget.kt index dabb904d7..ac949ad53 100644 --- a/src/main/kotlin/com/lambda/gui/components/SettingsWidget.kt +++ b/src/main/kotlin/com/lambda/gui/components/SettingsWidget.kt @@ -31,104 +31,107 @@ import imgui.flag.ImGuiPopupFlags import imgui.flag.ImGuiTabBarFlags object SettingsWidget { - /** - * Builds the settings context popup content for the given configurable. - */ - fun ImGuiBuilder.buildConfigSettingsContext(config: Configurable) { - group { - if (config is Module) { - with(config.keybindSetting) { buildLayout() } - with(config.disableOnReleaseSetting) { buildLayout() } - with(config.drawSetting) { buildLayout() } - } - smallButton("Reset") { - config.settings.forEach { it.reset(silent = true) } - } - lambdaTooltip("Resets all settings for this module to their default values") - if (config is MutableAutomationConfig && config.automationConfig !== AutomationConfig.Companion.DEFAULT) { - button("Automation Config") { - ImGui.openPopup("##automation-config-popup-${config.name}") - } - if (config.backingAutomationConfig !== config.defaultAutomationConfig) { - sameLine() - text("(${config.backingAutomationConfig.name})") - } - ImGui.setNextWindowSizeConstraints(0f, 0f, Float.MAX_VALUE, io.displaySize.y * 0.5f) - popupContextItem("##automation-config-popup-${config.name}", ImGuiPopupFlags.None) { - combo("##LinkedConfig", preview = "Linked Config: ${config.backingAutomationConfig.name}") { - val addItem: (Configurable) -> Unit = { item -> - val selected = item === config.backingAutomationConfig + /** + * Builds the settings context popup content for the given configurable. + */ + fun ImGuiBuilder.buildConfigSettingsContext(config: Configurable) { + group { + if (config is Module) { + with(config.keybindSetting) { buildLayout() } + with(config.disableOnReleaseSetting) { buildLayout() } + with(config.drawSetting) { buildLayout() } + } + smallButton("Reset") { + config.settings.forEach { it.reset(silent = true) } + } + lambdaTooltip("Resets all settings for this module to their default values") + if (config is MutableAutomationConfig && config.automationConfig !== AutomationConfig.Companion.DEFAULT) { + button("Automation Config") { + ImGui.openPopup("##automation-config-popup-${config.name}") + } + if (config.backingAutomationConfig !== config.defaultAutomationConfig) { + sameLine() + text("(${config.backingAutomationConfig.name})") + } + ImGui.setNextWindowSizeConstraints(0f, 0f, Float.MAX_VALUE, io.displaySize.y * 0.5f) + popupContextItem("##automation-config-popup-${config.name}", ImGuiPopupFlags.None) { + combo("##LinkedConfig", preview = "Linked Config: ${config.backingAutomationConfig.name}") { + val addItem: (Configurable) -> Unit = { item -> + val selected = item === config.backingAutomationConfig - selectable(item.name, selected) { - if (!selected) { - (config.backingAutomationConfig as? UserAutomationConfig)?.linkedModules?.value?.remove(config.name) - (item as? UserAutomationConfig)?.linkedModules?.value?.add(config.name) - config.automationConfig = item as? AutomationConfig ?: return@selectable - } - } - } - addItem(config.defaultAutomationConfig) + selectable(item.name, selected) { + if (!selected) { + (config.backingAutomationConfig as? UserAutomationConfig)?.linkedModules?.value?.remove(config.name) + (item as? UserAutomationConfig)?.linkedModules?.value?.add(config.name) + config.automationConfig = item as? AutomationConfig ?: return@selectable + } + } + } + addItem(config.defaultAutomationConfig) UserAutomationConfigs.configurables.forEach { addItem(it) } - } - buildConfigSettingsContext(config.automationConfig) - } - } - } - val toIgnoreSettings = - when (config) { - is Module -> setOf(config.keybindSetting, config.disableOnReleaseSetting, config.drawSetting) - is UserAutomationConfig -> setOf(config.linkedModules) - else -> emptySet() - } - val visibleSettings = config.settings.filter { it.visibility() } - toIgnoreSettings - if (visibleSettings.isEmpty()) return - else separator() - val (grouped, ungrouped) = visibleSettings.partition { it.groups.isNotEmpty() } - ungrouped.forEach { - it.withDisabled { buildLayout() } - } - renderGroup(grouped, emptyList(), config) - } + } + buildConfigSettingsContext(config.automationConfig) + } + } + } + val toIgnoreSettings = + when (config) { + is Module -> setOf(config.keybindSetting, config.disableOnReleaseSetting, config.drawSetting) + is UserAutomationConfig -> setOf(config.linkedModules) + else -> emptySet() + } + val visibleSettings = config.settings.filter { it.visibility() } - toIgnoreSettings + if (visibleSettings.isEmpty() && config.otherElements.isEmpty()) return + else separator() + val (grouped, ungrouped) = visibleSettings.partition { it.groups.isNotEmpty() } + ungrouped.forEach { + it.withDisabled { buildLayout() } + } + config.otherElements.forEach { + it.buildLayout() + } + renderGroup(grouped, emptyList(), config) + } - private fun Setting<*, *>.withDisabled(block: Setting<*, *>.() -> Unit) { - if (disabled()) ImGui.beginDisabled() - block() - if (disabled()) ImGui.endDisabled() - } + private fun Setting<*, *>.withDisabled(block: Setting<*, *>.() -> Unit) { + if (disabled()) ImGui.beginDisabled() + block() + if (disabled()) ImGui.endDisabled() + } - private fun ImGuiBuilder.renderGroup( - settings: List>, - parentPath: List, - config: Configurable - ) { - settings.filter { it.groups.contains(parentPath) }.forEach { - it.withDisabled { buildLayout() } - } + private fun ImGuiBuilder.renderGroup( + settings: List>, + parentPath: List, + config: Configurable + ) { + settings.filter { it.groups.contains(parentPath) }.forEach { + it.withDisabled { buildLayout() } + } - val subGroupSettings = settings.filter { s -> - s.groups.any { it.size > parentPath.size && it.subList(0, parentPath.size) == parentPath } - } - val subTabs = subGroupSettings - .flatMap { s -> - s.groups.mapNotNull { path -> - if (path.size > parentPath.size && path.subList(0, parentPath.size) == parentPath) - path[parentPath.size] else null - } - }.distinct() + val subGroupSettings = settings.filter { s -> + s.groups.any { it.size > parentPath.size && it.subList(0, parentPath.size) == parentPath } + } + val subTabs = subGroupSettings + .flatMap { s -> + s.groups.mapNotNull { path -> + if (path.size > parentPath.size && path.subList(0, parentPath.size) == parentPath) + path[parentPath.size] else null + } + }.distinct() - if (subTabs.isNotEmpty()) { - val id = "##${config.name}-tabs-${parentPath.joinToString("-") { it.displayName }}" - tabBar(id, ImGuiTabBarFlags.FittingPolicyResizeDown) { - subTabs.forEach { tab -> - tabItem(tab.displayName) { - val newParentPath = parentPath + tab - val settingsForSubGroup = subGroupSettings.filter { s -> - s.groups.any { it.size >= newParentPath.size && it.subList(0, newParentPath.size) == newParentPath } - } - renderGroup(settingsForSubGroup, newParentPath, config) - } - } - } - } - } + if (subTabs.isNotEmpty()) { + val id = "##${config.name}-tabs-${parentPath.joinToString("-") { it.displayName }}" + tabBar(id, ImGuiTabBarFlags.FittingPolicyResizeDown) { + subTabs.forEach { tab -> + tabItem(tab.displayName) { + val newParentPath = parentPath + tab + val settingsForSubGroup = subGroupSettings.filter { s -> + s.groups.any { it.size >= newParentPath.size && it.subList(0, newParentPath.size) == newParentPath } + } + renderGroup(settingsForSubGroup, newParentPath, config) + } + } + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt b/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt index 0efcda8b6..8cb93f7ee 100644 --- a/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt +++ b/src/main/kotlin/com/lambda/interaction/BaritoneManager.kt @@ -355,7 +355,8 @@ object BaritoneManager : Configurable(LambdaConfig), Automated by AutomationConf get() = isBaritoneLoaded && (primary?.customGoalProcess?.isActive == true || primary?.pathingBehavior?.isPathing == true || - primary?.pathingControlManager?.mostRecentInControl()?.orElse(null)?.isActive == true) + primary?.pathingControlManager?.mostRecentInControl()?.orElse(null)?.isActive == true || + primary?.elytraProcess?.isActive == true) /** * Sets the current Baritone goal and starts pathing @@ -365,11 +366,25 @@ object BaritoneManager : Configurable(LambdaConfig), Automated by AutomationConf primary?.customGoalProcess?.setGoalAndPath(goal) } + /** + * Sets the current Baritone goal without starting pathing + */ + fun setGoal(goal: Goal) { + if (!isBaritoneLoaded || primary?.elytraProcess?.isLoaded == false) return + primary?.customGoalProcess?.goal = goal + } + + fun setGoalAndElytraPath(goal: Goal) { + if (!isBaritoneLoaded || primary?.elytraProcess?.isLoaded == false) return + primary?.elytraProcess?.pathTo(goal) + } + /** * Force cancel Baritone */ fun cancel() { if (!isBaritoneLoaded) return primary?.pathingBehavior?.cancelEverything() + primary?.elytraProcess?.resetState() } } diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt index 64cf50573..ce81503c1 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationRequest.kt @@ -145,6 +145,9 @@ interface IRotationRequest : Automated { @RotationRequestDsl fun yaw(yaw: Float) { yawBuilder = { yaw.toDouble() } } + @RotationRequestDsl + fun yaw(rotation: Rotation) { yawBuilder = { rotation.yaw } } + @JvmName("pitchBuilder1") @RotationRequestDsl fun pitch(builder: SafeContext.() -> Double) { pitchBuilder = builder } @@ -159,6 +162,9 @@ interface IRotationRequest : Automated { @RotationRequestDsl fun pitch(pitch: Float) { pitchBuilder = { pitch.toDouble() } } + @RotationRequestDsl + fun pitch(rotation: Rotation) { pitchBuilder = { rotation.pitch } } + @RotationRequestDsl fun rotation(builder: SafeContext.() -> Rotation) { rotationBuilder = builder } diff --git a/src/main/kotlin/com/lambda/module/modules/movement/autospiral/AutoSpiral.kt b/src/main/kotlin/com/lambda/module/modules/movement/autospiral/AutoSpiral.kt new file mode 100644 index 000000000..d7bf97ae2 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/movement/autospiral/AutoSpiral.kt @@ -0,0 +1,114 @@ +/* + * 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.movement.autospiral + +import baritone.api.pathing.goals.GoalXZ +import com.lambda.context.SafeContext +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.BaritoneManager +import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest +import com.lambda.interaction.managers.rotating.visibilty.lookAt +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.threading.runSafe +import com.lambda.util.extension.isNether +import net.minecraft.util.math.BlockPos +import kotlin.math.sqrt + +object AutoSpiral : Module( + name = "AutoSpiral", + description = "Automatically flies in a spiral pattern. Uses Baritone elytra pathing in the Nether.", + tag = ModuleTag.MOVEMENT, +) { + var iterator: SpiralIterator2d? = null + var currentWaypoint: BlockPos? = null + var center: BlockPos = BlockPos.ORIGIN + + var spiralSpacing by setting("Spiral Spacing", 128, 16..1024, description = "The distance between each loop of the spiral") + var waypointTriggerDistance by setting("Waypoint Trigger Distance", 4, 2..64, description = "The distance to the waypoint at which a new waypoint is generated. Put in 50-60 range when in the Nether.") + var setCenterOnEnable by setting("Set Center On Enable", true, description = "Whether to set the center of the spiral to your current position when enabling the module.") + var setBaritoneGoal by setting("Set Baritone Goal", true, description = "Whether to set Baritone's goal to the current waypoint. Mostly so you can see where the next waypoint is.") + + init { + button("Reset Center") { + runSafe { + center = player.blockPos + currentWaypoint = null + } + } + button("Next Waypoint") { + runSafe { + currentWaypoint = null + } + } + + onEnable { + if (iterator == null) { + iterator = SpiralIterator2d(10000); + if (setCenterOnEnable) center = player.blockPos + } + } + + onDisable { + iterator = null + currentWaypoint = null + BaritoneManager.cancel() + } + + listen { + if (currentWaypoint == null || waypointReached()) { + nextWaypoint() + } + + currentWaypoint?.let { waypoint -> + if (!world.isNether) { + rotationRequest { + yaw(lookAt(waypoint.toCenterPos())) + }.submit(true) + } + } + } + } + + private fun SafeContext.waypointReached(): Boolean { + return currentWaypoint?.let { + val distance = distanceXZ(player.blockPos, it) + return distance <= waypointTriggerDistance + } ?: false + } + + private fun distanceXZ(a: BlockPos, b: BlockPos): Double { + val dx = (a.x - b.x).toDouble() + val dz = (a.z - b.z).toDouble() + return sqrt(dx * dx + dz * dz) + } + + private fun SafeContext.nextWaypoint() { + iterator?.next()?.let { pos -> + val scaled = pos.multiply(spiralSpacing) + val w = scaled.add(center) + if (world.isNether) { + BaritoneManager.setGoalAndElytraPath(GoalXZ(w.x, w.z)) + } else { + if (setBaritoneGoal) BaritoneManager.setGoal(GoalXZ(w.x, w.z)) + } + currentWaypoint = w + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/module/modules/movement/autospiral/SpiralIterator2d.kt b/src/main/kotlin/com/lambda/module/modules/movement/autospiral/SpiralIterator2d.kt new file mode 100644 index 000000000..9fcf9bf6d --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/movement/autospiral/SpiralIterator2d.kt @@ -0,0 +1,89 @@ +/* + * 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.movement.autospiral + +import net.minecraft.util.math.BlockPos +import kotlin.math.floor +import kotlin.math.pow + + +/** + * Spiral outwards from a central position in growing squares. + * Every point has a constant distance to its previous and following position of 1. First point returned is the starting position. + * Generates positions like this: + * ```text + * 16 15 14 13 12 + * 17 4 3 2 11 + * 18 5 0 1 10 + * 19 6 7 8 9 + * 20 21 22 23 24 + * (maxDistance = 2; points returned = 25) + * ``` + * Copy and paste source: https://stackoverflow.com/questions/3706219/algorithm-for-iterating-over-an-outward-spiral-on-a-discrete-2d-grid-from-the-or + * + */ +class SpiralIterator2d(maxDistance: Int) : MutableIterator { + val maxDistance: Int + val totalPoints: Int + private var deltaX: Int + private var deltaZ: Int + private var segmentLength: Int + private var currentX: Int + private var currentZ: Int + private var stepsInCurrentSegment: Int + var pointsGenerated: Int + + init { + this.maxDistance = maxDistance + this.totalPoints = floor(((floor(maxDistance.toDouble()) - 0.5) * 2).pow(2.0)).toInt() + this.deltaX = 1 + this.deltaZ = 0 + this.segmentLength = 1 + this.currentX = 0 + this.currentZ = 0 + this.stepsInCurrentSegment = 0 + this.pointsGenerated = 0 + } + + override fun next(): BlockPos? { + if (this.pointsGenerated >= this.totalPoints) return null + val output = BlockPos(this.currentX, 0, this.currentZ) + this.currentX += this.deltaX + this.currentZ += this.deltaZ + this.stepsInCurrentSegment += 1 + if (this.stepsInCurrentSegment == this.segmentLength) { + this.stepsInCurrentSegment = 0 + val buffer = this.deltaX + this.deltaX = -this.deltaZ + this.deltaZ = buffer + if (this.deltaZ == 0) { + this.segmentLength += 1 + } + } + this.pointsGenerated += 1 + return output + } + + override fun hasNext(): Boolean { + return this.pointsGenerated < this.totalPoints + } + + override fun remove() { + throw UnsupportedOperationException("remove") + } +} \ No newline at end of file