diff --git a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java index 702af3fec..309ec2dfb 100644 --- a/src/main/java/com/lambda/mixin/MinecraftClientMixin.java +++ b/src/main/java/com/lambda/mixin/MinecraftClientMixin.java @@ -20,6 +20,7 @@ import com.lambda.core.TimerManager; import com.lambda.event.EventFlow; import com.lambda.event.events.ClientEvent; +import com.lambda.event.events.GuiEvent; import com.lambda.event.events.InventoryEvent; import com.lambda.event.events.TickEvent; import com.lambda.gui.DearImGui; @@ -205,4 +206,10 @@ void updateWindowTitle(CallbackInfo ci) { WindowUtils.setLambdaTitle(); ci.cancel(); } + + @Inject(method = "setScreen", at = @At("HEAD"), cancellable = true) + private void injectSetScreen(Screen screen, CallbackInfo ci) { + var event = new GuiEvent.ScreenOpen(screen); + if (EventFlow.post(event).isCanceled()) ci.cancel(); + } } diff --git a/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java b/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java index 3525d6ce6..6ede77813 100644 --- a/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java +++ b/src/main/java/com/lambda/mixin/entity/ClientPlayInteractionManagerMixin.java @@ -21,6 +21,8 @@ import com.lambda.event.events.InventoryEvent; import com.lambda.event.events.PlayerEvent; import com.lambda.interaction.managers.inventory.InventoryManager; +import com.lambda.interaction.managers.rotating.RotationManager; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; import com.llamalad7.mixinextras.injector.wrapoperation.Operation; import net.minecraft.client.network.ClientPlayerEntity; @@ -73,6 +75,18 @@ public void interactItemHead(PlayerEntity player, Hand hand, CallbackInfoReturna } } + @ModifyExpressionValue(method = "method_41929", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;getYaw()F")) + private float modifyHand(float original) { + var headYaw = RotationManager.getHeadYaw(); + return headYaw != null ? headYaw : original; + } + + @ModifyExpressionValue(method = "method_41929", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/player/PlayerEntity;getPitch()F")) + private float modifySequence(float original) { + var headPitch = RotationManager.getHeadPitch(); + return headPitch != null ? headPitch : original; + } + @Inject(method = "attackBlock", at = @At("HEAD"), cancellable = true) public void onAttackBlock(BlockPos pos, Direction side, CallbackInfoReturnable cir) { if (EventFlow.post(new PlayerEvent.Attack.Block(pos, side)).isCanceled()) { diff --git a/src/main/kotlin/com/lambda/command/commands/StashMoverCommand.kt b/src/main/kotlin/com/lambda/command/commands/StashMoverCommand.kt new file mode 100644 index 000000000..de7ae5cd4 --- /dev/null +++ b/src/main/kotlin/com/lambda/command/commands/StashMoverCommand.kt @@ -0,0 +1,59 @@ +/* + * 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.command.commands + +import com.lambda.brigadier.argument.literal +import com.lambda.brigadier.execute +import com.lambda.brigadier.required +import com.lambda.command.LambdaCommand +import com.lambda.module.modules.world.StashMover +import com.lambda.threading.runSafe +import com.lambda.util.extension.CommandBuilder + +object StashMoverCommand : LambdaCommand( + name = "stashmover", + usage = "stashmover ", + description = "Set configurations for the StashMover module" +) { + override fun CommandBuilder.create() { + required(literal("index_selected_containers")) { + execute { runSafe { StashMover.indexSelectedContainers() } } + } + required(literal("remove_selected_containers")) { + execute { runSafe { StashMover.removeSelectedContainers() } } + } + required(literal("set_item_throw")) { + execute { runSafe { StashMover.setItemThrow() } } + } + required(literal("set_pearl_button_pos")) { + execute { runSafe { StashMover.setPearlButtonPos() } } + } + required(literal("set_pearl_throw")) { + execute { runSafe { StashMover.setPearlThrow() } } + } + required(literal("set_pearlbot_button")) { + execute { runSafe { StashMover.setPearlBotButton() } } + } + required(literal("start-stop")) { + execute { runSafe { StashMover.startStop() } } + } + required(literal("pause-unpause")) { + execute { runSafe { StashMover.pauseUnpause() } } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt b/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt index 99230e868..8752d08c9 100644 --- a/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt +++ b/src/main/kotlin/com/lambda/config/groups/BuildConfig.kt @@ -36,8 +36,9 @@ interface BuildConfig : ISettingGroup { val maxBuildDependencies: Int val limitTimeframe: Int - val actionPacketLimit: Int - val interactionPacketLimit: Int + val actionLimit: Int + val interactionLimit: Int + val inventoryLimit: Int val blockReach: Double val entityReach: Double diff --git a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt index f01e4bd2f..3f3262f45 100644 --- a/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt @@ -48,9 +48,10 @@ class BuildSettings( override val actionTimeout by c.setting("${prefix}Action Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks", visibility = visibility).group(*baseGroup, Group.General).index() override val maxBuildDependencies by c.setting("${prefix}Max Sim Dependencies", 3, 0..10, 1, "Maximum dependency build results", visibility = visibility).group(*baseGroup, Group.General).index() - override val limitTimeframe by c.setting("${prefix}Limit Timeframe", 6, 1..30, 1, "The timeframe in which the limit is bound to", "ticks", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() - override val actionPacketLimit by c.setting("${prefix}Action Packet Limit", 55, 1..100, 1, "The maximum allowed action packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() - override val interactionPacketLimit by c.setting("Interaction Limit", 9, 1..20, 1, "The maximum allowed interaction packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() + override val limitTimeframe by c.setting("${prefix}Limit Timeframe", 310, 50..1500, 1, "The timeframe in which the limit is bound to", "ms", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() + override val actionLimit by c.setting("${prefix}Action Limit", 59, 1..100, 1, "The maximum allowed action packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() + override val interactionLimit by c.setting("${prefix}Interaction Limit", 9, 1..20, 1, "The maximum allowed interaction packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() + override val inventoryLimit by c.setting("${prefix}Inventory Limit", 5, 1..100, 1, "The maximum allowed inventory packets to be sent to the server per given timeframe", visibility = visibility).group(*baseGroup, Group.PacketLimits).index() override var blockReach by c.setting("${prefix}Interact Reach", 4.5, 1.0..7.0, 0.01, "Maximum block interaction distance", visibility = visibility).group(*baseGroup, Group.Reach).index() override var entityReach by c.setting("${prefix}Attack Reach", 3.0, 1.0..7.0, 0.01, "Maximum entity interaction distance", visibility = visibility).group(*baseGroup, Group.Reach).index() diff --git a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt index e00c5a228..6b4fac6c9 100644 --- a/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt +++ b/src/main/kotlin/com/lambda/config/groups/InventorySettings.kt @@ -36,7 +36,6 @@ class InventorySettings( Access("Access") } - override val actionsPerSecond by c.setting("${prefix}Actions Per Second", 100, 0..100, 1, "How many inventory actions can be performed per tick", visibility = visibility).group(*baseGroup, Group.General).index() override val tickStageMask by c.setting("${prefix}Inventory Stage Mask", ALL_STAGES.toSet(), description = "The sub-tick timing at which inventory actions are performed", displayClassName = true, visibility = visibility).group(*baseGroup, Group.General).index() override val disposables by c.setting("${prefix}Disposables", ItemUtils.defaultDisposables, description = "Items that will be ignored when checking for a free slot", visibility = visibility).group(*baseGroup, Group.Container).index() override val swapWithDisposables by c.setting("${prefix}Swap With Disposables", true, "Swap items with disposable ones", visibility = visibility).group(*baseGroup, Group.Container).index() diff --git a/src/main/kotlin/com/lambda/config/migration/migrations/AutomationConfigMigration.kt b/src/main/kotlin/com/lambda/config/migration/migrations/AutomationConfigMigration.kt new file mode 100644 index 000000000..cef85a53b --- /dev/null +++ b/src/main/kotlin/com/lambda/config/migration/migrations/AutomationConfigMigration.kt @@ -0,0 +1,44 @@ +/* + * 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.migration.migrations + +import com.lambda.Lambda.LOG +import com.lambda.config.migration.StepConfigMigration + +object AutomationConfigMigration : StepConfigMigration() { + override val configName = "automation" + override val latestVersion = 2 + + init { + step(1, 2) { + var updateCount = 0 + entrySet().forEach { configPair -> + getAsJsonObject(configPair.key)?.let { config -> + config.get("Limit Timeframe")?.let { limitTimeframe -> + if (limitTimeframe.asInt < 50) { + config.addProperty("Limit Timeframe", 310) + updateCount++ + } + } + } + } + + LOG.info("Migrated Automation config schema v1 -> v2: $updateCount settings updated") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/event/events/GuiEvent.kt b/src/main/kotlin/com/lambda/event/events/GuiEvent.kt index fa04a2ad6..4cbd037f0 100644 --- a/src/main/kotlin/com/lambda/event/events/GuiEvent.kt +++ b/src/main/kotlin/com/lambda/event/events/GuiEvent.kt @@ -21,13 +21,14 @@ import com.lambda.event.Event import com.lambda.event.callback.Cancellable import com.lambda.event.callback.ICancellable import net.minecraft.block.entity.SignBlockEntity +import net.minecraft.client.gui.screen.Screen sealed class GuiEvent { /** * Triggered when a new ImGui frame is created and the client - * is allowed to submit any command from this point until [EndFrame]. + * is allowed to submit any command from this point until [EndImguiFrame]. */ - data object NewFrame : Event + data object NewImguiFrame : Event /** * Triggered when the previous ImGui frame is ended and the client @@ -35,7 +36,7 @@ sealed class GuiEvent { * * By default, the game's framebuffer is bound. */ - data object EndFrame : Event + data object EndImguiFrame : Event /** * Triggered when the sign editor GUI is opened. Can be canceled. @@ -44,4 +45,6 @@ sealed class GuiEvent { var sign: SignBlockEntity, var front: Boolean ) : ICancellable by Cancellable() + + data class ScreenOpen(val screen: Screen?) : ICancellable by Cancellable() } diff --git a/src/main/kotlin/com/lambda/gui/DearImGui.kt b/src/main/kotlin/com/lambda/gui/DearImGui.kt index 0f008d56c..32d71dcbd 100644 --- a/src/main/kotlin/com/lambda/gui/DearImGui.kt +++ b/src/main/kotlin/com/lambda/gui/DearImGui.kt @@ -116,9 +116,9 @@ object DearImGui : Loadable { ClickGuiLayout.applyStyle(lastScale) ImGui.newFrame() - GuiEvent.NewFrame.post() + GuiEvent.NewImguiFrame.post() ImGui.render() - GuiEvent.EndFrame.post() + GuiEvent.EndImguiFrame.post() implGl3.renderDrawData(ImGui.getDrawData()) diff --git a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt index 5ca1a40e6..342f5c58b 100644 --- a/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/ClickGuiLayout.kt @@ -244,7 +244,7 @@ object ClickGuiLayout : Loadable, Configurable(GuiConfig) { val modalWindowDimBg by setting("Modal Window Dim Background", Color(35, 0, 14, 90)).group(Group.Colors) init { - listen { + listen { if (!open) return@listen buildLayout { diff --git a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt index ead67b23c..c192edb3c 100644 --- a/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt +++ b/src/main/kotlin/com/lambda/gui/components/HudGuiLayout.kt @@ -81,7 +81,7 @@ object HudGuiLayout : Loadable, Configurable(HudConfig) { private const val TWO_PI_F = (2f * PI).toFloat() init { - listen { + listen { if (mc.options.hudHidden) return@listen buildLayout { diff --git a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt index 60e8bf249..10115bdd9 100644 --- a/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt +++ b/src/main/kotlin/com/lambda/gui/snap/SnapManager.kt @@ -54,7 +54,7 @@ object SnapManager : Loadable { ) init { - listen { + listen { val vp = ImGui.getMainViewport() val io = ImGui.getIO() beginFrame(vp.sizeX, vp.sizeY, io.fontGlobalScale) diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/InteractSim.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/InteractSim.kt index b20d4d348..0dd73ce83 100644 --- a/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/InteractSim.kt +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/checks/InteractSim.kt @@ -47,7 +47,6 @@ import com.lambda.util.item.ItemUtils.blockItem import com.lambda.util.math.MathUtils.floorToInt import com.lambda.util.math.minus import com.lambda.util.player.MovementUtils.sneaking -import com.lambda.util.player.SlotUtils.hotbarStacks import com.lambda.util.player.copyPlayer import com.lambda.util.world.raycast.RayCastUtils.blockResult import kotlinx.coroutines.CoroutineScope @@ -62,7 +61,7 @@ import net.minecraft.entity.Entity import net.minecraft.item.BlockItem import net.minecraft.item.Item import net.minecraft.item.ItemPlacementContext -import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot import net.minecraft.state.property.Properties import net.minecraft.util.Hand import net.minecraft.util.math.BlockPos @@ -142,7 +141,7 @@ class InteractSim private constructor(simInfo: InteractSimInfo) val interactContext = InteractContext( hitResult, rotationRequest { rotation(checkedHit.rotation) }, - getSwapStack(item, supervisorScope)?.inventoryIndex ?: return, + getSwapSlot(item, supervisorScope)?.index ?: return, pos, state, expectedState, @@ -195,7 +194,7 @@ class InteractSim private constructor(simInfo: InteractSimInfo) val interactContext = InteractContext( hitResult, rotationRequest { rotation(rotationRequest) }, - getSwapStack(item, supervisorScope)?.inventoryIndex ?: return, + getSwapSlot(item, supervisorScope)?.index ?: return, pos, state, rotatePlaceTest.resultState, @@ -211,7 +210,7 @@ class InteractSim private constructor(simInfo: InteractSimInfo) return } - private fun AutomatedSafeContext.getSwapStack(item: Item?, supervisorScope: CoroutineScope): ItemStack? { + private fun AutomatedSafeContext.getSwapSlot(item: Item?, supervisorScope: CoroutineScope): Slot? { if (item?.isEnabled(world.enabledFeatures) == false) { result(InteractResult.BlockFeatureDisabled(pos, item)) supervisorScope.cancel() @@ -224,9 +223,8 @@ class InteractSim private constructor(simInfo: InteractSimInfo) result(GenericResult.WrongItemSelection(pos, stackSelection, player.mainHandStack)) return null } - val hotbarStacks = player.hotbarStacks - return stackSelection.filterStacks(container.stacks).run { - firstOrNull { hotbarStacks.indexOf(it) == player.inventory.selectedSlot } + return stackSelection.filterSlots(container.slots).run { + firstOrNull { it.index == player.inventory.selectedSlot } ?: firstOrNull() } } diff --git a/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/property/placement/post/PoweredPostProcessor.kt b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/property/placement/post/PoweredPostProcessor.kt new file mode 100644 index 000000000..69e9048c8 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/construction/simulation/processing/preprocessors/property/placement/post/PoweredPostProcessor.kt @@ -0,0 +1,38 @@ +/* + * 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.interaction.construction.simulation.processing.preprocessors.property.placement.post + +import com.lambda.context.SafeContext +import com.lambda.interaction.construction.simulation.processing.PreProcessingInfoAccumulator +import com.lambda.interaction.construction.simulation.processing.PropertyPostProcessor +import net.minecraft.block.BlockState +import net.minecraft.block.ButtonBlock +import net.minecraft.block.LeverBlock +import net.minecraft.state.property.Properties +import net.minecraft.util.math.BlockPos + +// Collected using reflections and then accessed from a collection in ProcessorRegistry +@Suppress("unused") +object PoweredPostProcessor : PropertyPostProcessor { + override fun acceptsState(state: BlockState, targetState: BlockState) = + state.block is ButtonBlock || state.block is LeverBlock && state.get(Properties.POWERED) != targetState.get(Properties.POWERED) + + context(safeContext: SafeContext) + override fun PreProcessingInfoAccumulator.preProcess(state: BlockState, targetState: BlockState, pos: BlockPos) = + with(StandardInteractPostProcessor) { preProcess(state, targetState, pos) } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/managers/PacketLimitHandler.kt b/src/main/kotlin/com/lambda/interaction/managers/PacketLimitHandler.kt index 6c9567ba3..bd0407a22 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/PacketLimitHandler.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/PacketLimitHandler.kt @@ -21,45 +21,55 @@ import com.lambda.config.groups.BuildConfig import com.lambda.context.Automated import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen -import com.lambda.util.TickTimer +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.ComparableTimeMark +import kotlin.time.TimeSource object PacketLimitHandler { - private val packetLimitMap = PacketType.entries.associateWith { LimitHandler(0, 0, 0) } + private val packetLimitMap = PacketType.entries.associateWith { LimitHandler() } init { - listen(priority = { Int.MIN_VALUE }) { - packetLimitMap.values.forEach { limitHandler -> - with(limitHandler) { - tickTimer.tick() - if (tickTimer.hasSurpassed(timeframe)) { - packetsThisTimeframe = 0 - tickTimer.reset() - } - } - } + listen(priority = { Int.MAX_VALUE }) { + val currentTime = TimeSource.Monotonic.markNow() + packetLimitMap.values.forEach { it.removeStale(currentTime) } } } context(automated: Automated) - fun canSendPackets(packetCount: Int, packetType: PacketType) = - packetLimitMap[packetType]?.let { - it.maxPacketsThisTimeframe = packetType.maxPacketsPerTimeframe(automated.buildConfig) - it.timeframe = automated.buildConfig.limitTimeframe - it.packetsThisTimeframe + packetCount <= it.maxPacketsThisTimeframe - } ?: false + fun canSendPackets(packetCount: Int, packetType: PacketType): Boolean { + val handler = packetLimitMap[packetType] ?: return false + handler.maxPacketsThisTimeframe = packetType.maxPacketsPerTimeframe(automated.buildConfig) + handler.timeframe = automated.buildConfig.limitTimeframe.milliseconds + handler.removeStale(TimeSource.Monotonic.markNow()) + return handler.packetTimestamps.size + packetCount <= handler.maxPacketsThisTimeframe + } - fun sentPackets(packetCount: Int, packetType: PacketType) = - packetLimitMap[packetType]?.let { limitHandler -> - if (limitHandler.packetsThisTimeframe == 0) limitHandler.tickTimer.reset() - limitHandler.packetsThisTimeframe += packetCount + fun sentPackets(packetCount: Int, packetType: PacketType) { + packetLimitMap[packetType]?.let { handler -> + val currentTime = TimeSource.Monotonic.markNow() + repeat((0 until packetCount).count()) { + handler.packetTimestamps.addLast(currentTime) + } } + } - private data class LimitHandler(var maxPacketsThisTimeframe: Int, var packetsThisTimeframe: Int, var timeframe: Int) { - val tickTimer = TickTimer() + private class LimitHandler { + var maxPacketsThisTimeframe = 0 + var timeframe: Duration = Duration.ZERO + val packetTimestamps = ArrayDeque() + + fun removeStale(currentTime: ComparableTimeMark) { + if (timeframe <= Duration.ZERO) return + while (packetTimestamps.isNotEmpty() && packetTimestamps.first() <= currentTime - timeframe) { + packetTimestamps.removeFirst() + } + } } } enum class PacketType(val maxPacketsPerTimeframe: BuildConfig.() -> Int) { - PlayerAction({ actionPacketLimit }), - Interaction({ interactionPacketLimit }) + PlayerAction({ actionLimit }), + Interaction({ interactionLimit }), + Inventory({ inventoryLimit }) } \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryConfig.kt b/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryConfig.kt index dcc5f4d89..d62b02de1 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryConfig.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryConfig.kt @@ -28,7 +28,6 @@ import com.lambda.util.NamedEnum import net.minecraft.item.Item interface InventoryConfig : ISettingGroup { - val actionsPerSecond: Int val tickStageMask: Collection val disposables: Collection val swapWithDisposables: Boolean diff --git a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt b/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt index e116793c4..1d485a1c0 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/inventory/InventoryManager.kt @@ -23,7 +23,8 @@ import com.lambda.event.events.PacketEvent import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.interaction.managers.Manager -import com.lambda.interaction.managers.PacketLimitHandler +import com.lambda.interaction.managers.PacketLimitHandler.canSendPackets +import com.lambda.interaction.managers.PacketLimitHandler.sentPackets import com.lambda.interaction.managers.PacketType import com.lambda.interaction.managers.inventory.InventoryManager.actions import com.lambda.interaction.managers.inventory.InventoryManager.activeRequest @@ -31,6 +32,7 @@ import com.lambda.interaction.managers.inventory.InventoryManager.alteredSlots import com.lambda.interaction.managers.inventory.InventoryManager.processActiveRequest import com.lambda.module.modules.client.Client import com.lambda.threading.runSafe +import com.lambda.threading.runSafeAutomated import com.lambda.util.collections.LimitedDecayQueue import com.lambda.util.item.ItemStackUtils.equal import com.llamalad7.mixinextras.injector.wrapoperation.Operation @@ -67,9 +69,6 @@ object InventoryManager : Manager( field = value } - private var maxActionsThisSecond = 0 - private var actionsThisSecond = 0 - private var secondCounter = 0 private var actionsThisTick = 0 override fun load(): String { @@ -77,10 +76,6 @@ object InventoryManager : Manager( listen({ Int.MIN_VALUE }) { if (Client.avoidInventoryDesync) indexInventoryChanges() - if (++secondCounter >= 20) { - secondCounter = 0 - actionsThisSecond = 0 - } actionsThisTick = 0 activeRequest = null actions = mutableListOf() @@ -105,11 +100,11 @@ object InventoryManager : Manager( override fun AutomatedSafeContext.handleRequest(request: InventoryRequest) { if (activeRequest != null) return - val inventoryActionCount = request.actions.count { it is InventoryAction.Inventory } val playerActionCount = request.actions.count { it is InventoryAction.Player } - val canPerformAllPlayerActions = PacketLimitHandler.canSendPackets(playerActionCount, PacketType.PlayerAction) - val canPerformAllInventoryActions = inventoryActionCount <= request.inventoryConfig.actionsPerSecond - actionsThisSecond - if ((!canPerformAllInventoryActions || !canPerformAllPlayerActions) && + val inventoryActionCount = request.actions.count { it is InventoryAction.Inventory } + val canPerformAllPlayerActions = canSendPackets(playerActionCount, PacketType.PlayerAction) + val canPerformAllInventoryActions = canSendPackets(inventoryActionCount, PacketType.Inventory) + if ((!canPerformAllPlayerActions || !canPerformAllInventoryActions) && !request.settleForLess && !request.mustPerform) return @@ -125,7 +120,6 @@ object InventoryManager : Manager( private fun populateFrom(request: InventoryRequest) { activeRequest = request actions = request.actions.toMutableList() - maxActionsThisSecond = request.inventoryConfig.actionsPerSecond alteredSlots.setDecayTime(Client.desyncTimeout * 50L) alteredPlayerSlots.setDecayTime(Client.desyncTimeout * 50L) } @@ -136,18 +130,19 @@ object InventoryManager : Manager( * The [activeRequest] is then set to null. */ private fun SafeContext.processActiveRequest() { - activeRequest?.let { active -> + val active = activeRequest ?: return + active.runSafeAutomated { if (tickStage !in active.inventoryConfig.tickStageMask && active.nowOrNothing) return val iterator = actions.iterator() while (iterator.hasNext()) { val action = iterator.next() - if (action is InventoryAction.Inventory && actionsThisSecond + 1 > maxActionsThisSecond && !active.mustPerform) - break + if (action is InventoryAction.Player && !canSendPackets(1, PacketType.PlayerAction)) break + else if (action is InventoryAction.Inventory && !canSendPackets(1, PacketType.Inventory)) break action.action(this) - if (action is InventoryAction.Player) PacketLimitHandler.sentPackets(1, PacketType.PlayerAction) + if (action is InventoryAction.Player) sentPackets(1, PacketType.PlayerAction) + else if (action is InventoryAction.Inventory) sentPackets(1, PacketType.Inventory) if (Client.avoidInventoryDesync) indexInventoryChanges() actionsThisTick++ - if (action is InventoryAction.Inventory) actionsThisSecond++ iterator.remove() } @@ -234,7 +229,7 @@ object InventoryManager : Manager( val alteredSlots = if (packet.syncId == 0) alteredPlayerSlots else alteredSlots val matches = alteredSlots.removeIf { - it.syncId == packet.slot && it.after.equal(itemStack) + it.slotId == packet.slot && it.after.equal(itemStack) } if (packet.syncId == 0) { @@ -267,7 +262,7 @@ object InventoryManager : Manager( } private data class InventoryChange( - val syncId: Int, + val slotId: Int, val before: ItemStack, val after: ItemStack ) diff --git a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt index 206b1ae56..ddb15812f 100644 --- a/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt +++ b/src/main/kotlin/com/lambda/interaction/managers/rotating/RotationManager.kt @@ -76,6 +76,8 @@ object RotationManager : Manager( private val IRotationRequest.overridable get() = age >= 1 + private var pauseVanillaOverrides = false + override fun load(): String { super.load() @@ -121,11 +123,11 @@ object RotationManager : Manager( } // Override user interactions with max priority - listen({ Int.MAX_VALUE }) { activeRotation = player.rotation } - listen({ Int.MAX_VALUE }) { activeRotation = player.rotation } - listen({ Int.MAX_VALUE }) { activeRotation = player.rotation } - listen({ Int.MAX_VALUE }) { activeRotation = player.rotation } - listen({ Int.MAX_VALUE }) { activeRotation = player.rotation } + listen({ Int.MAX_VALUE }) { if (!pauseVanillaOverrides) activeRotation = player.rotation } + listen({ Int.MAX_VALUE }) { if (!pauseVanillaOverrides) activeRotation = player.rotation } + listen({ Int.MAX_VALUE }) { if (!pauseVanillaOverrides) activeRotation = player.rotation } + listen({ Int.MAX_VALUE }) { if (!pauseVanillaOverrides) activeRotation = player.rotation } + listen({ Int.MAX_VALUE }) { if (!pauseVanillaOverrides) activeRotation = player.rotation } return "Loaded Rotation Manager" } @@ -179,6 +181,12 @@ object RotationManager : Manager( setPlayerPitch(rotation.pitch) } + fun withoutVanillaOverrides(block: () -> Unit) { + pauseVanillaOverrides = true + block() + pauseVanillaOverrides = false + } + /** * If the rotation has not been changed this tick, the [activeRequest]'s target rotation is updated, and * likewise the [activeRotation]. The [activeRequest] is then updated, ticking the [RotationRequest.keepTicks] diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt index 3c6543b71..18cc96534 100644 --- a/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt +++ b/src/main/kotlin/com/lambda/interaction/material/container/containers/ChestContainer.kt @@ -61,7 +61,7 @@ data class ChestContainer( override fun accessThen(exitAfter: Boolean, taskGenerator: TaskGenerator): Task<*> = OpenContainerTask(blockPos, automatedSafeContext).then { taskGenerator.invoke(automatedSafeContext, Unit).finally { - if (exitAfter) automatedSafeContext.player.closeScreen() + if (exitAfter) automatedSafeContext.player.closeHandledScreen() } } } diff --git a/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt b/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt index 71458d0ca..e31076fc3 100644 --- a/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt +++ b/src/main/kotlin/com/lambda/module/modules/client/AutoUpdater.kt @@ -83,7 +83,7 @@ object AutoUpdater : Module( showInstallModal = false } - listen(alwaysListen = true) { + listen(alwaysListen = true) { initializeFirstLaunchStateIfNeeded() if (showFirstLaunchModal) { diff --git a/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt b/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt index 4869d37f4..a1e6cea1a 100644 --- a/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt +++ b/src/main/kotlin/com/lambda/module/modules/world/Scaffold.kt @@ -64,6 +64,7 @@ object Scaffold : Module( hide() } ::checkSideVisibility.edit { defaultValue(true) } + hide(::breakBlocks) } interactConfig::airPlace.edit { defaultValue(InteractConfig.AirPlaceMode.None) } rotationConfig.apply { @@ -71,21 +72,7 @@ object Scaffold : Module( ::mean.edit { defaultValue(120.0) } ::spread.edit { defaultValue(0.0) } } - inventoryConfig.apply { - hide( - ::actionsPerSecond, - ::tickStageMask, - ::swapWithDisposables, - ::providerPriority, - ::storePriority, - ::accessShulkerBoxes, - ::accessEnderChest, - ::accessChests, - ::accessStashes, - ::disposables - ) - } - hideAllGroupsExcept(buildConfig, interactConfig, rotationConfig, hotbarConfig, inventoryConfig) + hideAllGroupsExcept(buildConfig, interactConfig, rotationConfig, hotbarConfig) } } diff --git a/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt b/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt new file mode 100644 index 000000000..833f6b541 --- /dev/null +++ b/src/main/kotlin/com/lambda/module/modules/world/StashMover.kt @@ -0,0 +1,894 @@ +/* + * 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.world + +import baritone.api.pathing.goals.GoalBlock +import com.lambda.Lambda.mc +import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig +import com.lambda.config.applyEdits +import com.lambda.config.settings.complex.Bind +import com.lambda.config.settings.complex.KeybindSetting.Companion.onPress +import com.lambda.context.SafeContext +import com.lambda.event.events.ButtonEvent +import com.lambda.event.events.ChatEvent +import com.lambda.event.events.GuiEvent +import com.lambda.event.events.PacketEvent +import com.lambda.event.events.TickEvent +import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.event.listener.UnsafeListener.Companion.listenUnsafe +import com.lambda.graphics.mc.renderer.TickedRenderer.Companion.tickedRenderer +import com.lambda.interaction.BaritoneManager +import com.lambda.interaction.construction.verify.TargetState +import com.lambda.interaction.managers.hotbar.HotbarRequest +import com.lambda.interaction.managers.interacting.InteractConfig +import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest +import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest +import com.lambda.interaction.managers.rotating.Rotation +import com.lambda.interaction.managers.rotating.Rotation.Companion.dist +import com.lambda.interaction.managers.rotating.RotationManager +import com.lambda.interaction.material.container.containers.EnderChestContainer +import com.lambda.module.Module +import com.lambda.module.tag.ModuleTag +import com.lambda.task.RootTask.run +import com.lambda.task.Task +import com.lambda.task.tasks.BuildTask.Companion.build +import com.lambda.task.tasks.OpenContainerTask +import com.lambda.threading.runSafeAutomated +import com.lambda.util.BlockUtils.blockEntity +import com.lambda.util.BlockUtils.blockState +import com.lambda.util.Communication.info +import com.lambda.util.Communication.logError +import com.lambda.util.Communication.warn +import com.lambda.util.NamedEnum +import com.lambda.util.TickTimer +import com.lambda.util.extension.containerSlots +import com.lambda.util.extension.containerStacks +import com.lambda.util.extension.rotation +import com.lambda.util.math.distSq +import com.lambda.util.math.setAlpha +import com.lambda.util.player.SlotUtils.allSlots +import com.lambda.util.player.SlotUtils.hotbarAndInventorySlots +import com.lambda.util.player.SlotUtils.hotbarAndInventoryStacks +import com.lambda.util.player.SlotUtils.hotbarSlots +import com.lambda.util.player.SlotUtils.hotbarStacks +import com.lambda.util.player.SlotUtils.inventoryStacks +import com.lambda.util.player.SlotUtils.offHandSlots +import com.lambda.util.text.bold +import com.lambda.util.text.buildText +import com.lambda.util.text.color +import com.lambda.util.text.literal +import com.lambda.util.world.raycast.RayCastUtils.blockResult +import net.minecraft.block.Blocks +import net.minecraft.block.ButtonBlock +import net.minecraft.block.entity.LootableContainerBlockEntity +import net.minecraft.client.gui.screen.DeathScreen +import net.minecraft.item.ItemStack +import net.minecraft.item.Items +import net.minecraft.network.packet.c2s.play.ClientStatusC2SPacket +import net.minecraft.network.packet.s2c.play.PlayerPositionLookS2CPacket +import net.minecraft.network.packet.s2c.play.PlayerRespawnS2CPacket +import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot +import net.minecraft.state.property.Properties +import net.minecraft.util.Hand +import net.minecraft.util.math.BlockPos +import net.minecraft.util.math.Box +import org.lwjgl.glfw.GLFW +import java.awt.Color +import kotlin.math.min +import kotlin.run +import kotlin.to + +@Suppress("unused") +object StashMover : Module( + name = "StashMover", + description = "Moves items from one stash location to another", + tag = ModuleTag.WORLD +) { + private enum class Group(override val displayName: String) : NamedEnum { + General("General"), + CommandBinds("Command Binds") + } + + enum class Role(val createTask: () -> Task<*>) { + MoverBot({ MoverBot() }), + PearlBot({ PearlBot() }) + } + + private enum class DropOffMode(override val displayName: String) : NamedEnum { + Chests("Chests"), + Drop("Drop") + } + + val role: Role by setting("Role", Role.MoverBot).group(Group.General) + .onValueChange { _, _ -> clearModule() } + private val pearlBotName by setting("PearlBot Name", "Steve") { role == Role.MoverBot }.group(Group.General) + private val moverBotName by setting("MoverBot Name", "Steve") { role == Role.PearlBot }.group(Group.General) + private val dropOffMode by setting("Drop-Off Mode", DropOffMode.Chests) { role == Role.MoverBot }.group(Group.General) + private var chestPullSelMode: Boolean by setting("Chest Pull Sel Mode", false, "Enables the mode to select the stash containers you want to move items from") { role == Role.MoverBot }.group(Group.General) + .onValueChange { _, to -> if (to) { chestPutSelMode = false; StashMover.info("Enabled chest pull selection mode!") } } + private var chestPutSelMode: Boolean by setting("Chest Put Sel Mode", false, "Enables the mod to select the stash containers you want to move items into") { role == Role.MoverBot }.group(Group.General) + .onValueChange { _, to -> if (to) { chestPullSelMode = false; StashMover.info("Enabled chest put selection mode!") } } + private val pearlMsgTimeout by setting("Pearl Msg Timeout", 100, 0..1500, 1, "Ticks before messaging the pearl bot again", "ticks") { role == Role.MoverBot }.group(Group.General) + private val pearlButtonTimeout by setting("Pearl Button Timeout", 100, 0..1500, 1, "Ticks before pressing the pearl dispenser button again", "ticks") { role == Role.MoverBot }.group(Group.General) + private val killRespawnTimeout by setting("Kill/Respawn Timeout", 100, 0..1500, 1, "Ticks before sending the kill command or attempting to respawn again", "ticks") { role == Role.MoverBot }.group(Group.General) + private val actionDelay by setting("Action Delay", 3, 0..20, 1, "The delay after performing one action, before the next") { role == Role.MoverBot }.group(Group.General) + private val useEnderChest by setting("Use Ender Chest", false, "Uses the ender chest to move more items at once. (Ender chests are included in the pull/put selections)") { role == Role.MoverBot }.group(Group.General) + .onValueChange { _, to -> + if (!to) { + putEnderChests.clear() + pullEnderChests.clear() + } + } + private val breakEmptyPullContainers by setting("Break Empty Pull Containers", false, "Breaks empty pull containers after taking their items") { role == Role.MoverBot }.group(Group.General) + private val disconnectOnFinish by setting("Disconnect On Finish", false, "Disconnects the mover bot when it's finished") { role == Role.MoverBot }.group(Group.General) + private val disconnectOnFail by setting("Disconnect On Fail", false, "Disconnects the mover bot if it fails") { role == Role.MoverBot }.group(Group.General) + private val startStop by setting("Start/Stop", Bind.EMPTY, "Starts and stops the selected role").group(Group.General) + .onPress { event -> + event.cancel() + startStop() + } + private val pauseUnpause by setting("Pause/Unpause", Bind.EMPTY, "Pauses and unpauses the selected role").group(Group.General) + .onPress { event -> + event.cancel() + pauseUnpause() + } + + private val indexSelectedContainers by setting("Index Selected Containers", Bind.EMPTY, "Indexes the selected containers to pull/push items from/to") { role == Role.MoverBot }.group(Group.CommandBinds) + .onPress { event -> + event.cancel() + indexSelectedContainers() + } + private val removeSelectedContainers by setting("Remove Selected Containers", Bind.EMPTY, "Removes the selected containers from being pull/pushed from/to") { role == Role.MoverBot }.group(Group.CommandBinds) + .onPress { event -> + event.cancel() + removeSelectedContainers() + } + private val setItemThrowPosAndRotation by setting("Set Item Throw", Bind.EMPTY, "Sets the item throw position and rotation. (This is usually set to throw into hoppers to pickup the items)") { role == Role.MoverBot }.group(Group.CommandBinds) + .onPress { event -> + event.cancel() + setItemThrow() + } + private val setPearlButtonPos by setting("Set Pearl Button Pos", Bind.EMPTY, "Sets the button used to dispense a pearl for the player") { role == Role.MoverBot }.group(Group.CommandBinds) + .onPress { event -> + event.cancel() + setPearlButtonPos() + } + private val setPearlThrowPosAndRotation by setting("Set Pearl Throw", Bind.EMPTY, "Sets the pearl throw position and rotation. (This is best if you throw somewhat sideways into a line of bubble columns)") { role == Role.MoverBot }.group(Group.CommandBinds) + .onPress { event -> + event.cancel() + setPearlThrow() + } + private val setPearlBotButton by setting("Set PearlBot Button", Bind.EMPTY, "Sets the button position for the pearl bot to press to load the mover bot") { role == Role.PearlBot }.group(Group.CommandBinds) + .onPress { event -> + event.cancel() + setPearlBotButton() + } + + private var sel1: BlockPos? = null + private var sel2: BlockPos? = null + + private val pullContainers = hashSetOf() + private val putEnderChests = hashSetOf() + private val pulledContainers = hashSetOf() + private val putContainers = hashSetOf() + private val pullEnderChests = hashSetOf() + private val filledContainers = hashSetOf() + + private var itemThrowPos: BlockPos? = null + private var itemThrowRotation: Rotation? = null + + private var pearlDispensePos: BlockPos? = null + private var pearlThrowPos: BlockPos? = null + private var pearlRotation: Rotation? = null + + private var pearlBotButton: BlockPos? = null + + private var task: Task<*>? = null + + init { + setModulePriority(100) + setDefaultAutomationConfig { + applyEdits { + buildConfig.apply { + editTyped(::pathing, ::stayInRange, ::checkSideVisibility) { defaultValue(true) } + hide(::pathing, ::stayInRange, ::collectDrops, ::spleefEntities, ::entityReach) + hideGroup(eatConfig) + } + interactConfig::airPlace.edit { defaultValue(InteractConfig.AirPlaceMode.None) } + breakConfig.apply { + editTyped(::suitableToolsOnly, ::efficientOnly) { defaultValue(false) } + } + } + } + + listen { event -> + if (!chestPullSelMode && !chestPutSelMode) return@listen + if (event.action != GLFW.GLFW_PRESS || mc.currentScreen != null) return@listen + + if (event.button == 0) { + event.cancel() + sel1 = mc.crosshairTarget?.blockResult?.blockPos ?: return@listen + } + else if (event.button == 1) { + event.cancel() + sel2 = mc.crosshairTarget?.blockResult?.blockPos ?: return@listen + } + } + + onDisable { clearModule() } + + tickedRenderer("StashMover Immediate Renderer") { + if (!chestPullSelMode && !chestPutSelMode) return@tickedRenderer + sel1?.let { sel1 -> + box(Box(sel1)) { + colors(Color.PINK.setAlpha(0.1), Color.PINK) + } + } + sel2?.let { sel2 -> + box(Box(sel2)) { + colors(Color.MAGENTA.setAlpha(0.1), Color.MAGENTA) + } + } + sel1?.let { sel1 -> + sel2?.let { sel2 -> + val box = Box(sel1).union(Box(sel2)) + box(box) { + val color = if (chestPutSelMode) Color.GREEN else Color.BLUE + colors(color.setAlpha(0.1), color) + } + } + } + } + } + + private fun clearModule() { + task?.cancel() + task = null + sel1 = null + sel2 = null + pullContainers.clear() + putEnderChests.clear() + pulledContainers.clear() + putContainers.clear() + pullEnderChests.clear() + filledContainers.clear() + pearlDispensePos = null + pearlThrowPos = null + pearlRotation = null + pearlBotButton = null + itemThrowPos = null + itemThrowRotation = null + } + + context(safeContext: SafeContext) + fun indexSelectedContainers() { + var addCount = 0 + consumeSelection { pos -> + pulledContainers.remove(pos) + filledContainers.remove(pos) + if (useEnderChest && safeContext.blockState(pos).block === Blocks.ENDER_CHEST) { + addCount++ + if (chestPullSelMode) { + pullEnderChests.remove(pos) + putEnderChests.add(pos) + } else if (chestPutSelMode) { + putEnderChests.remove(pos) + pullEnderChests.add(pos) + } + return@consumeSelection + } + if (safeContext.blockEntity(pos) !is LootableContainerBlockEntity) return@consumeSelection + addCount++ + if (chestPullSelMode) { + putContainers.remove(pos) + pullContainers.add(pos) + } else if (chestPutSelMode) { + pullContainers.remove(pos) + putContainers.add(pos) + } + } + StashMover.info("Indexed $addCount ${if (chestPullSelMode) "pull" else "put"} containers!") + } + + context(safeContext: SafeContext) + fun removeSelectedContainers() { + var removeCount = 0 + consumeSelection { pos -> + if (safeContext.blockEntity(pos) !is LootableContainerBlockEntity) return@consumeSelection + if (pullContainers.remove(pos) || + pulledContainers.remove(pos) || + putContainers.remove(pos) || + filledContainers.remove(pos)) removeCount++ + } + StashMover.info("Removed $removeCount containers!") + } + + context(safeContext: SafeContext) + fun setItemThrow() { + itemThrowPos = safeContext.player.blockPos + itemThrowRotation = safeContext.player.rotation + } + + context(safeContext: SafeContext) + fun setPearlButtonPos() { + val pos = mc.crosshairTarget?.blockResult?.blockPos ?: return + if (safeContext.blockState(pos).block !is ButtonBlock) { + StashMover.warn("Given position does not contain a button!") + return + } + pearlDispensePos = pos + StashMover.info("Set pearl button position!") + } + + context(safeContext: SafeContext) + fun setPearlThrow() { + pearlThrowPos = safeContext.player.blockPos + pearlRotation = safeContext.player.rotation + StashMover.info("Set pearl throw position and rotation!") + } + + context(safeContext: SafeContext) + fun setPearlBotButton() { + val pos = mc.crosshairTarget?.blockResult?.blockPos ?: return + if (safeContext.blockState(pos).block !is ButtonBlock) { + StashMover.warn("Given position does not contain a button!") + return + } + pearlBotButton = pos + StashMover.info("Set pearl bot button position!") + } + + fun startStop() { + if (isDisabled) return + chestPullSelMode = false + chestPutSelMode = false + task?.let { runningTask -> + runningTask.cancel() + task = null + return + } + StashMover.info("Starting ${if (role == Role.MoverBot) "MoverBot" else "PearlBot"}!") + task = role + .createTask() + .onFailOrNull { + if (disconnectOnFail) connection.connection.disconnect( + buildText { + bold { + literal("StashMover") + color(Color.RED) { literal(" Failed") } + } + } + ) + task = null + null + } + .finally { message -> + StashMover.info("Finished! $message") + if (disconnectOnFinish) connection.connection.disconnect( + buildText { + bold { + literal("StashMover") + color(Color.GREEN) { literal(" Finished!") } + } + } + ) + task = null + } + .run() + } + + fun pauseUnpause() { + if (isDisabled) return + if (task?.isMuted == true) task?.activate() + else task?.pause() + } + + private fun consumeSelection(callback: (pos: BlockPos) -> Unit) { + sel1?.let { sel1 -> + sel2?.let { sel2 -> + val minX = minOf(sel1.x, sel2.x) + val maxX = maxOf(sel1.x, sel2.x) + val minY = minOf(sel1.y, sel2.y) + val maxY = maxOf(sel1.y, sel2.y) + val minZ = minOf(sel1.z, sel2.z) + val maxZ = maxOf(sel1.z, sel2.z) + (minX..maxX).forEach { x -> + (minZ..maxZ).forEach { z -> + (minY..maxY).forEach { y -> + callback.invoke(BlockPos(x, y, z)) + } + } + } + } + } + sel1 = null + sel2 = null + } + + private class MoverBot : Task() { + override val name + get() = "Moving a stash, pearled by $pearlBotName, current state: $moverState" + private var moverState = MoverState.TakingItems + set(newValue) { + actionDelayTimer.reset() + delayingNextAction = true + field = newValue + } + + private var actionDelayTimer = TickTimer() + private var delayingNextAction = false + + private var pearlThrown = false + + private var pullContainer: BlockPos? = null + private var tickTimer = TickTimer() + + private var putContainer: BlockPos? = null + + private var finished = false + private var finishedMessage = "" + + init { + listen { + if (delayingNextAction) { + actionDelayTimer.tick() + if (!actionDelayTimer.hasSurpassed(actionDelay)) return@listen + delayingNextAction = false + } + + val screenHandler = player.currentScreenHandler + + when (moverState) { + MoverState.OpeningPullContainer -> + openClosestContainer( + pullContainers, + { + finished = true + finishedMessage = "Pull containers exhausted!" + breakPulledOrPearl() + } + ) { pos -> + pullContainer = pos + moverState = MoverState.TakingItems + } + MoverState.TakingItems -> handleTakingItems(screenHandler) + MoverState.OpeningPutEnderChest -> + openClosestContainer( + putEnderChests, + { failWithLog("No pull ender chests indexed!") } + ) { moverState = MoverState.PuttingInEnderChest } + MoverState.PuttingInEnderChest -> handlePuttingInEnderChest(screenHandler) + MoverState.BreakingEmptyPullContainers -> handleBreakingEmptyPullContainers() + MoverState.MessagingForPearl -> handleMessagingForPearl() + MoverState.AwaitingTeleport -> + checkTimerProgress( + MoverState.MessagingForPearl, + pearlMsgTimeout + ) + MoverState.DispensingPearl -> handleDispensingPearl() + MoverState.AwaitingPearl -> + checkTimerProgress(MoverState.DispensingPearl, pearlButtonTimeout) { + if (player.hotbarAndInventoryStacks.any { it.item === Items.ENDER_PEARL }) { + pearlThrown = false + moverState = MoverState.ThrowingPearl + true + } else false + } + MoverState.ThrowingPearl -> handleThrowingPearl() + MoverState.DroppingItems -> handleDroppingItems() + MoverState.OpeningPutContainer -> + openClosestContainer( + putContainers, + { success("Put containers are full!") } + ) { pos -> + putContainer = pos + moverState = MoverState.PuttingItems + } + MoverState.PuttingItems -> handlePuttingItems(screenHandler) + MoverState.OpeningPullEnderChest -> + openClosestContainer( + pullEnderChests, + { failWithLog("No put ender chests indexed!") } + ) { moverState = MoverState.PullingFromEnderChest } + MoverState.PullingFromEnderChest -> handlePullingFromEnderChest(screenHandler) + MoverState.Killing -> handleKilling() + else -> {} + } + } + + listenUnsafe { + when (moverState) { + MoverState.AwaitingDeath -> checkTimerProgress(MoverState.Killing, killRespawnTimeout) + MoverState.Respawning -> handleRespawning() + MoverState.AwaitingRespawn -> checkTimerProgress(MoverState.Respawning, killRespawnTimeout) + else -> {} + } + } + + listenUnsafe { event -> + if (moverState != MoverState.AwaitingRespawn) return@listenUnsafe + if (event.packet !is PlayerRespawnS2CPacket) return@listenUnsafe + moverState = MoverState.OpeningPullContainer + } + + listen { event -> + if (moverState != MoverState.AwaitingTeleport) return@listen + val packet = event.packet + if (packet !is PlayerPositionLookS2CPacket) return@listen + if (finished && player.hotbarAndInventoryStacks.all { it.isEmpty }) { + success(finishedMessage) + return@listen + } + moverState = MoverState.DispensingPearl + } + + listen { event -> + if (moverState != MoverState.AwaitingDeath) return@listen + if (event.screen !is DeathScreen) return@listen + moverState = MoverState.Respawning + } + } + + private fun SafeContext.handleTakingItems(screenHandler: ScreenHandler) { + if (screenHandler === player.playerScreenHandler) { + moverState = MoverState.OpeningPullContainer + return + } + if (!moveFromContainerToContainer(screenHandler.containerSlots, player.hotbarAndInventoryStacks)) return + pullContainer?.let { container -> + if (screenHandler.containerStacks.all { it.isEmpty }) { + pullContainers.remove(container) + pulledContainers.add(container) + if (player.hotbarAndInventoryStacks.any { it.isEmpty }) { + moverState = MoverState.OpeningPullContainer + return + } + } + } + if (useEnderChest && (EnderChestContainer.stacks.isEmpty() || EnderChestContainer.stacks.any { it.isEmpty })) { + moverState = MoverState.OpeningPutEnderChest + return + } + breakPulledOrPearl() + } + + private fun SafeContext.handlePuttingInEnderChest(screenHandler: ScreenHandler) { + if (screenHandler === player.playerScreenHandler) { + moverState = MoverState.OpeningPutEnderChest + return + } + if (moveFromContainerToContainer(player.hotbarAndInventorySlots, screenHandler.containerStacks)) { + moverState = MoverState.OpeningPullContainer + } + } + + private fun handleBreakingEmptyPullContainers() { + pulledContainers + .associateWith { TargetState.Empty } + .build() + .onFailOrNull { + failWithLog("Failed to break empty pull containers!") + null + } + .finally { + pulledContainers.clear() + moverState = MoverState.MessagingForPearl + } + .execute(this) + } + + private fun SafeContext.handleMessagingForPearl() { + tickTimer.reset() + connection.sendChatCommand("msg $pearlBotName ${Math.random() * Double.MAX_VALUE}") + moverState = MoverState.AwaitingTeleport + } + + private fun SafeContext.handleDroppingItems() { + val throwPos = itemThrowPos ?: run { failWithLog("No item throw pos set!"); return } + if (player.blockPos != throwPos) { + BaritoneManager.setGoalAndPath(GoalBlock(throwPos)) + return + } + if (BaritoneManager.isActive) return + val rotation = itemThrowRotation ?: run { failWithLog("No item throw rotation set!"); return } + val rotationRequest = rotationRequest { + rotation(rotation) + }.submit() + if (!rotationRequest.done || rotation dist RotationManager.serverRotation > 0.001) return + val throwSlots = player.hotbarAndInventorySlots.filter { !it.stack.isEmpty } + if (throwSlots.isNotEmpty()) { + val inventoryRequest = inventoryRequest(settleForLess = true) { + throwSlots.forEach { slot -> + throwStack(slot.id) + } + }.submit() + if (!inventoryRequest.done) return + } + pullFromEnderChestOrContinue() + } + + private fun SafeContext.handlePuttingItems(screenHandler: ScreenHandler) { + if (screenHandler === player.playerScreenHandler) { + moverState = MoverState.OpeningPutContainer + return + } + if (!moveFromContainerToContainer(player.hotbarAndInventorySlots, screenHandler.containerStacks)) return + putContainer?.let { container -> + if (screenHandler.containerStacks.all { !it.isEmpty }) { + putContainers.remove(container) + filledContainers.add(container) + if (player.hotbarAndInventoryStacks.any { !it.isEmpty }) { + moverState = MoverState.OpeningPutContainer + return + } + } + } + pullFromEnderChestOrContinue() + } + + private fun SafeContext.handlePullingFromEnderChest(screenHandler: ScreenHandler) { + if (screenHandler === player.playerScreenHandler) { + moverState = MoverState.OpeningPullEnderChest + return + } + if (moveFromContainerToContainer(screenHandler.containerSlots, player.hotbarAndInventoryStacks)) { + putOrThrowItems() + } + } + + private fun SafeContext.handleDispensingPearl() { + val dispensePos = pearlDispensePos ?: run { failWithLog("No pearl button set!"); return } + if (player.blockPos != dispensePos) { + BaritoneManager.setGoalAndPath(GoalBlock(dispensePos)) + return + } + if (BaritoneManager.isActive) return + if (player.hotbarStacks.none { it.isEmpty }) { + val firstSlot = player.hotbarSlots.getOrNull(0) ?: run { failWithLog("No first slot? This shouldn't occur."); return } + if (player.inventoryStacks.any { it.isEmpty }) { + inventoryRequest { quickMove(firstSlot.id) }.submit() + return + } else if (player.offHandStack.isEmpty) { + inventoryRequest { swap(firstSlot.id, 40) }.submit() + return + } + failWithLog("No free slots for an ender pearl!") + return + } + getButtonPressTask(dispensePos) + ?.finally { + tickTimer.reset() + moverState = MoverState.AwaitingPearl + } + ?.execute(this@MoverBot) + } + + private fun SafeContext.handleThrowingPearl() { + val throwPos = pearlThrowPos ?: run { failWithLog("No pearl throw pos set!"); return } + if (player.blockPos != throwPos) { + BaritoneManager.setGoalAndPath(GoalBlock(throwPos)) + return + } + if (BaritoneManager.isActive) return + if (player.velocity.y < -0.08 || player.velocity.x !in -0.001..0.001 || player.velocity.z !in -0.001..0.001) return + + if (pearlThrown) { + if (!player.offHandStack.isEmpty) { + if (player.hotbarAndInventoryStacks.none { it.isEmpty }) { + failWithLog("No free slots to return the offhand stack to!") + return + } + val offhandSlot = player.offHandSlots.firstOrNull() ?: run { failWithLog("No offhand slot? This shouldn't occur."); return } + inventoryRequest { quickMove(offhandSlot.id) }.submit() + } + putOrThrowItems() + return + } + + val rotation = pearlRotation ?: run { failWithLog("No pearl rotation set!"); return } + val rotationRequest = rotationRequest { + rotation(rotation) + }.submit() + if (!rotationRequest.done) return + if (player.mainHandStack.item != Items.ENDER_PEARL) { + val hotbarSlot = player.hotbarSlots.firstOrNull { it.stack.item === Items.ENDER_PEARL } + if (hotbarSlot != null) { + val hotbarRequest = HotbarRequest(hotbarSlot.index, StashMover, nowOrNothing = false).submit() + if (!hotbarRequest.done) return + } else { + val inventorySlot = player.allSlots.firstOrNull { it.stack.item === Items.ENDER_PEARL } + if (inventorySlot == null) { + failWithLog("No pearl in inventory!") + return + } + inventoryRequest { swap(inventorySlot.id, 0) }.submit() + return + } + } + RotationManager.withoutVanillaOverrides { + interaction.interactItem(player, Hand.MAIN_HAND) + pearlThrown = true + } + } + + private fun SafeContext.handleKilling() { + tickTimer.reset() + connection.sendChatCommand("kill") + moverState = MoverState.AwaitingDeath + } + + private fun handleRespawning() { + tickTimer.reset() + mc.networkHandler?.sendPacket(ClientStatusC2SPacket(ClientStatusC2SPacket.Mode.PERFORM_RESPAWN)) + moverState = MoverState.AwaitingRespawn + } + + private fun breakPulledOrPearl() { + moverState = + if (breakEmptyPullContainers && pulledContainers.isNotEmpty()) MoverState.BreakingEmptyPullContainers + else MoverState.MessagingForPearl + } + + private fun pullFromEnderChestOrContinue() { + if (useEnderChest && (EnderChestContainer.stacks.isEmpty() || EnderChestContainer.stacks.any { !it.isEmpty })) { + moverState = MoverState.OpeningPullEnderChest + return + } + if (finished) { + success(finishedMessage) + return + } + moverState = MoverState.Killing + } + + private fun putOrThrowItems() { + moverState = + when (dropOffMode) { + DropOffMode.Chests -> MoverState.OpeningPutContainer + DropOffMode.Drop -> MoverState.DroppingItems + } + } + + private fun SafeContext.openClosestContainer( + positions: Collection, + onNoneAvailable: () -> Unit, + finally: SafeContext.(pos: BlockPos) -> Unit + ) { + val pos = positions.minByOrNull { it distSq player.blockPos } + ?: run { + onNoneAvailable() + return + } + + OpenContainerTask( + pos, + StashMover + ).finally { + finally(pos) + }.execute(this@MoverBot) + } + + private fun SafeContext.moveFromContainerToContainer( + from: Collection, + to: Collection + ): Boolean { + val filteredFrom = from.filter { !it.stack.isEmpty } + val filteredTo = to.filter { it.isEmpty } + if (filteredTo.isEmpty() || filteredFrom.isEmpty()) { + player.closeHandledScreen() + return true + } + val moveSlots = filteredFrom.subList(0, min(filteredTo.size, filteredFrom.size)) + if (moveSlots.isNotEmpty()) { + val request = inventoryRequest(settleForLess = true) { + moveSlots.forEach { slot -> + quickMove(slot.id) + } + }.submit() + if (!request.done) return false + } + player.closeHandledScreen() + return true + } + + private fun checkTimerProgress( + fallbackState: MoverState, + timeout: Int, + progressionCheck: (() -> Boolean)? = null + ) { + tickTimer.tick() + if (progressionCheck?.invoke() == true) return + if (tickTimer.hasSurpassed(timeout)) moverState = fallbackState + } + + private enum class MoverState { + OpeningPullContainer, + TakingItems, + OpeningPutEnderChest, + PuttingInEnderChest, + BreakingEmptyPullContainers, + MessagingForPearl, + AwaitingTeleport, + DispensingPearl, + AwaitingPearl, + ThrowingPearl, + DroppingItems, + OpeningPutContainer, + PuttingItems, + OpeningPullEnderChest, + PullingFromEnderChest, + Killing, + AwaitingDeath, + Respawning, + AwaitingRespawn + } + } + + private class PearlBot : Task() { + override val name + get() = "Pearling $moverBotName for stash moving, current state: $pearlState" + var pearlState = PearlState.Waiting + + init { + listen { + when (pearlState) { + PearlState.Pressing -> { + val buttonPos = pearlBotButton ?: run { failWithLog("No pearl bot button set!"); return@listen } + getButtonPressTask(buttonPos) + ?.finally { + pearlState = PearlState.Waiting + } + ?.execute(this@PearlBot) + } + else -> {} + } + } + + listen { event -> + if (!event.message.string.startsWith("$moverBotName whispers")) return@listen + pearlState = PearlState.Pressing + } + } + + private enum class PearlState { + Waiting, + Pressing + } + } + + context(safeContext: SafeContext) + private fun Task<*>.getButtonPressTask(pos: BlockPos): Task<*>? = + with (safeContext) { + val buttonState = blockState(pos) + if (buttonState.block !is ButtonBlock) { + failure("Pearl button position does not contain a button!") + } + return if (!buttonState.get(Properties.POWERED)) { + StashMover.runSafeAutomated { + mapOf(pos to TargetState.State(buttonState.with(Properties.POWERED, true))) + .build() + } + } else null + } + + private fun Task<*>.failWithLog(message: String) { + failure(message) + StashMover.logError(message) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt b/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt index 2aa6d3961..9a1e9aaec 100644 --- a/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt +++ b/src/main/kotlin/com/lambda/task/tasks/OpenContainerTask.kt @@ -27,6 +27,7 @@ import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotat import com.lambda.interaction.managers.rotating.visibilty.lookAtBlock import com.lambda.task.Task import com.lambda.threading.runSafeAutomated +import com.lambda.util.TickTimer import com.lambda.util.world.raycast.RayCastUtils.blockResult import net.minecraft.screen.ScreenHandler import net.minecraft.util.Hand @@ -39,18 +40,19 @@ class OpenContainerTask @Ta5kBuilder constructor( private val waitForSlotLoad: Boolean = true, private val sides: Set = Direction.entries.toSet() ) : Task(), Automated by automated { - override val name get() = "${containerState.description(inScope)} at ${blockPos.toShortString()}" + override val name get() = "${containerState.description()} at ${blockPos.toShortString()}" private var screenHandler: ScreenHandler? = null private var containerState = State.Scoping - private var inScope = 0 + + private val retryTimer = TickTimer() enum class State { Pathing, Scoping, Opening, SlotLoading; - fun description(inScope: Int) = when (this) { + fun description() = when (this) { Pathing -> "Pathing closer" - Scoping -> "Waiting for scope ($inScope)" + Scoping -> "Waiting for scope" Opening -> "Opening container" SlotLoading -> "Waiting for slots to load" } @@ -82,6 +84,15 @@ class OpenContainerTask @Ta5kBuilder constructor( } listen { + if (containerState == State.Opening) { + retryTimer.tick() + if (retryTimer.hasSurpassed(10)) { + retryTimer.reset() + containerState = State.Scoping + } + return@listen + } + if (containerState != State.Scoping && containerState != State.Pathing) return@listen val checkedHit = runSafeAutomated { lookAtBlock(blockPos, sides) } @@ -93,6 +104,7 @@ class OpenContainerTask @Ta5kBuilder constructor( if (interactConfig.rotate && !rotationRequest { rotation(checkedHit.rotation) }.submit().done) return@listen interaction.interactBlock(player, Hand.MAIN_HAND, checkedHit.hit.blockResult ?: return@listen) + player.swingHand(Hand.MAIN_HAND) containerState = State.Opening } diff --git a/src/main/kotlin/com/lambda/util/TickTimer.kt b/src/main/kotlin/com/lambda/util/TickTimer.kt index b77f61be7..ce9356668 100644 --- a/src/main/kotlin/com/lambda/util/TickTimer.kt +++ b/src/main/kotlin/com/lambda/util/TickTimer.kt @@ -24,7 +24,7 @@ class TickTimer { ticks++ } - fun hasSurpassed(ticks: Int) = this.ticks > ticks + fun hasSurpassed(ticks: Int) = this.ticks >= ticks fun reset() { ticks = 0