Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/main/java/com/lambda/mixin/render/ScreenHandlerMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,9 @@ public class ScreenHandlerMixin {
private void onUpdateSlotStacksHead(int revision, List<ItemStack> 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));
}
}
7 changes: 7 additions & 0 deletions src/main/kotlin/com/lambda/event/events/InventoryEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<Slot>
get() = safeContext.player.inventorySlots + safeContext.player.hotbarSlots
override var stacks: List<ItemStack>
get() = (mc.player?.inventoryStacks ?: emptyList()) + (mc.player?.hotbarStacks ?: emptyList())
set(_) {}

override val description = buildText { literal("Combined Inventory") }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
}
102 changes: 91 additions & 11 deletions src/main/kotlin/com/lambda/module/modules/player/InventoryTweaks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,30 @@ 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
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

Expand All @@ -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 {
Expand All @@ -55,30 +71,94 @@ object InventoryTweaks : Module(

listen<PlayerEvent.SlotClick> {
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<InventoryEvent.SlotUpdate2> { event ->
if (event.stack.item !in shulkerBoxes) return@listen
lastPickedUpShulker = event.stack
}

listen<InventoryEvent.Close> { 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
}
}

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<Unit>(), Automated by automated {
override val name: String
get() = "Depositing back to container at ${blockPos.toShortString()}"

init {
listen<TickEvent.Pre> {
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
}
}
}
}
}
}
}