From e3292b57dd196456812bcb415a8faf73b733a31c Mon Sep 17 00:00:00 2001 From: Ic3Tank <61137113+IceTank@users.noreply.github.com> Date: Sun, 17 May 2026 01:24:12 +0200 Subject: [PATCH] Allow InventoryTweaks to take shulkers from containers and automatically place them back --- .../mixin/render/ScreenHandlerMixin.java | 5 + .../com/lambda/event/events/InventoryEvent.kt | 7 ++ .../containers/CombinedPlayerInventory.kt | 41 +++++++ .../containers/InventoryContainer.kt | 2 +- .../module/modules/player/InventoryTweaks.kt | 102 ++++++++++++++++-- 5 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 src/main/kotlin/com/lambda/interaction/material/container/containers/CombinedPlayerInventory.kt diff --git a/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java b/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java index c27f30a0a..cd758e82a 100644 --- a/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java +++ b/src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java @@ -34,4 +34,9 @@ public class ScreenHandlerMixin { private void onUpdateSlotStacksHead(int revision, List stacks, ItemStack cursorStack, CallbackInfo ci) { EventFlow.post(new InventoryEvent.FullUpdate(revision, stacks, cursorStack)); } + + @Inject(method = "setStackInSlot", at = @At("TAIL")) + private void onSetStackInSlot(int syncId, int slot, ItemStack stack, CallbackInfo ci) { + EventFlow.post(new InventoryEvent.SlotUpdate2((ScreenHandler) (Object) this, syncId, slot, stack)); + } } diff --git a/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt b/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt index f0f8daa4c..db87641e9 100644 --- a/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt +++ b/src/main/kotlin/com/lambda/event/events/InventoryEvent.kt @@ -82,6 +82,13 @@ sealed class InventoryEvent { val stack: ItemStack, ) : Event + data class SlotUpdate2( + val screen: ScreenHandler, + val syncId: Int, + val slot: Int, + val stack: ItemStack, + ) : Event + abstract class HotbarSlot : Event { /** * Represents an event triggered when the client attempts to send slot update to the server. diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/CombinedPlayerInventory.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/CombinedPlayerInventory.kt new file mode 100644 index 000000000..52e47fc73 --- /dev/null +++ b/src/main/kotlin/com/lambda/interaction/material/container/containers/CombinedPlayerInventory.kt @@ -0,0 +1,41 @@ +/* + * 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.material.container.containers + +import com.lambda.Lambda.mc +import com.lambda.context.SafeContext +import com.lambda.interaction.material.container.MaterialContainer +import com.lambda.util.player.SlotUtils.hotbarSlots +import com.lambda.util.player.SlotUtils.hotbarStacks +import com.lambda.util.player.SlotUtils.inventorySlots +import com.lambda.util.player.SlotUtils.inventoryStacks +import com.lambda.util.text.buildText +import com.lambda.util.text.literal +import net.minecraft.item.ItemStack +import net.minecraft.screen.slot.Slot + +object CombinedPlayerInventory : MaterialContainer(Rank.Inventory) { + context(safeContext: SafeContext) + override val slots: List + get() = safeContext.player.inventorySlots + safeContext.player.hotbarSlots + override var stacks: List + get() = (mc.player?.inventoryStacks ?: emptyList()) + (mc.player?.hotbarStacks ?: emptyList()) + set(_) {} + + override val description = buildText { literal("Combined Inventory") } +} \ No newline at end of file diff --git a/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt b/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt index 10867bdb2..3b99f0e9b 100644 --- a/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt +++ b/src/main/kotlin/com/lambda/interaction/material/container/containers/InventoryContainer.kt @@ -35,5 +35,5 @@ object InventoryContainer : MaterialContainer(Rank.Inventory) { get() = mc.player?.inventoryStacks ?: emptyList() set(_) {} - override val description = buildText { literal("Inventory") } + override val description = buildText { literal("Main Inventory") } } diff --git a/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt b/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt index 37c6263cd..5832f1ec1 100644 --- a/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt +++ b/src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt @@ -19,9 +19,16 @@ package com.lambda.module.modules.player import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig import com.lambda.config.applyEdits +import com.lambda.context.Automated import com.lambda.event.events.InventoryEvent import com.lambda.event.events.PlayerEvent +import com.lambda.event.events.TickEvent import com.lambda.event.listener.SafeListener.Companion.listen +import com.lambda.interaction.managers.inventory.InventoryRequest.Companion.inventoryRequest +import com.lambda.interaction.material.StackSelection.Companion.selectStack +import com.lambda.interaction.material.container.ContainerManager.lastInteractedBlockEntity +import com.lambda.interaction.material.container.containers.ChestContainer +import com.lambda.interaction.material.container.containers.CombinedPlayerInventory import com.lambda.module.Module import com.lambda.module.tag.ModuleTag import com.lambda.task.RootTask.run @@ -29,9 +36,13 @@ import com.lambda.task.Task import com.lambda.task.tasks.BuildTask.Companion.breakAndCollectBlock import com.lambda.task.tasks.OpenContainerTask import com.lambda.task.tasks.PlaceContainerTask +import com.lambda.threading.runSafeAutomated import com.lambda.util.item.ItemUtils.shulkerBoxes +import net.minecraft.entity.player.PlayerInventory +import net.minecraft.item.ItemStack import net.minecraft.item.Items import net.minecraft.screen.ScreenHandler +import net.minecraft.screen.slot.Slot import net.minecraft.screen.slot.SlotActionType import net.minecraft.util.math.BlockPos @@ -40,11 +51,16 @@ object InventoryTweaks : Module( tag = ModuleTag.PLAYER, ) { private val instantShulker by setting("Instant Shulker", true, description = "Right-click shulker boxes in your inventory to instantly place them and open them.") + private val fromContainer by setting("From Container", true, description = "Allow instance shulkers from containers") { instantShulker } + private val backToContainer by setting("Back To Container", true, description = "After closing the shulker, put it back in the container it was taken from") { instantShulker && fromContainer } private val instantEChest by setting("Instant Ender-Chest", true, description = "Right-click ender chests in your inventory to instantly place them and open them.") private var placedPos: BlockPos? = null private var placeAndOpen: Task<*>? = null + private var takenFromContainer: BlockPos? = null + private var transferBack: Task<*>? = null private var lastBreak: Task<*>? = null private var lastOpenScreen: ScreenHandler? = null + private var lastPickedUpShulker: ItemStack? = null init { setDefaultAutomationConfig { @@ -55,23 +71,48 @@ object InventoryTweaks : Module( listen { if (it.action != SlotActionType.PICKUP || it.button != 1) return@listen - val slot = it.screenHandler.getSlot(it.slot) - if (!(instantShulker && slot.stack.item in shulkerBoxes) && !(instantEChest && slot.stack.item == Items.ENDER_CHEST)) return@listen - it.cancel() - lastOpenScreen = null - placeAndOpen = PlaceContainerTask(slot, this@InventoryTweaks).then { placePos -> - placedPos = placePos - OpenContainerTask(placePos, this@InventoryTweaks).finally { screenHandler -> - lastOpenScreen = screenHandler + it.screenHandler.getSlot(it.slot).let { slot -> + if (!(instantShulker && slot.stack.item in shulkerBoxes) && !(instantEChest && slot.stack.item == Items.ENDER_CHEST)) return@listen + it.cancel() + if (slot.inventory !is PlayerInventory && fromContainer) { + // TODO: place should be able to place blocks from containers without needing to transfer them to the player inventory first. Wait for beanbag to fix. + takenFromContainer = lastInteractedBlockEntity?.pos + val stackCopy = slot.stack.copy() + inventoryRequest { + quickMove(slot.id) + onComplete { + player.closeHandledScreen() + + (CombinedPlayerInventory.slots).firstOrNull { playerSlot -> + ItemStack.areItemsAndComponentsEqual(playerSlot.stack, stackCopy) + }?.let { invSlot -> + place(invSlot) + } + } + }.submit() + } else { + takenFromContainer = null + place(slot) } - }.run() + lastOpenScreen = null + } + } + + listen { event -> + if (event.stack.item !in shulkerBoxes) return@listen + lastPickedUpShulker = event.stack } listen { event -> if (event.screenHandler != lastOpenScreen) return@listen lastOpenScreen = null - placedPos?.let { - lastBreak = breakAndCollectBlock(it).run() + placedPos?.let { placePos -> + // TODO: breakAndCollectBlock should be able to return the drops it collected. If it did, we could chain a task to transfer the shulker back to the container it came from. Wait for bean to fix. + lastBreak = breakAndCollectBlock(placePos).thenOrNull { + val lastPos = takenFromContainer + if (backToContainer && lastPos != null) return@thenOrNull DepositBackTask(lastPos, this@InventoryTweaks) + else return@thenOrNull null + }.run() placedPos = null } } @@ -79,6 +120,45 @@ object InventoryTweaks : Module( onDisable { placeAndOpen?.cancel() lastBreak?.cancel() + transferBack?.cancel() + lastOpenScreen = null + takenFromContainer = null + } + } + + private fun place(slot: Slot) { + placeAndOpen = PlaceContainerTask(slot, this@InventoryTweaks).then { placePos -> + placedPos = placePos + OpenContainerTask(placePos, this@InventoryTweaks).finally { screenHandler -> + lastOpenScreen = screenHandler + } + }.run() + } + + class DepositBackTask @Ta5kBuilder constructor( + private val blockPos: BlockPos, + private val automated: Automated, + ) : Task(), Automated by automated { + override val name: String + get() = "Depositing back to container at ${blockPos.toShortString()}" + + init { + listen { + lastPickedUpShulker?.let { lastPickedUpShulker -> + runSafeAutomated { + // TODO: This only works if the target container is a chest. Wait for beanbag to fix. + CombinedPlayerInventory.transferByTask(selectStack { + { it: ItemStack -> ItemStack.areItemsEqual(it, lastPickedUpShulker) } + }, ChestContainer(listOf(), blockPos)) + .execute(this@DepositBackTask) + .thenOrNull { + success() + this@InventoryTweaks.lastPickedUpShulker = null + null + } + } + } + } } } }