Skip to content
Open
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
23 changes: 22 additions & 1 deletion src/main/java/com/lambda/mixin/render/HandledScreenMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,29 @@

package com.lambda.mixin.render;

import com.lambda.event.EventFlow;
import com.lambda.event.events.InventoryEvent;
import com.lambda.module.modules.render.ContainerPreview;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.client.gui.Click;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.slot.Slot;
import net.minecraft.screen.slot.SlotActionType;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(HandledScreen.class)
public class HandledScreenMixin {
public class HandledScreenMixin<T extends ScreenHandler> {
@Final
@Shadow
protected T handler;

@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
private void onMouseClicked(Click click, boolean doubled, CallbackInfoReturnable<Boolean> cir) {
if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) {
Expand All @@ -36,6 +49,14 @@ private void onMouseClicked(Click click, boolean doubled, CallbackInfoReturnable
}
}

@WrapMethod(method = "onMouseClick(Lnet/minecraft/screen/slot/Slot;IILnet/minecraft/screen/slot/SlotActionType;)V")
private void onMouseClickSlotPre(Slot slot, int slotId, int button, SlotActionType actionType, Operation<Void> original) {
if (!EventFlow.post(new InventoryEvent.SlotAction.Click(handler, slot, slotId, button, actionType)).isCanceled()) {
int slotIdFinal = slot == null ? slotId : slot.id;
original.call(slot, slotIdFinal, button, actionType);
}
}

@Inject(method = "mouseReleased", at = @At("HEAD"), cancellable = true)
private void onMouseReleased(Click click, CallbackInfoReturnable<Boolean> cir) {
if (ContainerPreview.INSTANCE.isEnabled() && ContainerPreview.isLocked()) {
Expand Down
29 changes: 29 additions & 0 deletions src/main/kotlin/com/lambda/event/events/InventoryEvent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import com.lambda.event.callback.Cancellable
import com.lambda.event.callback.ICancellable
import net.minecraft.item.ItemStack
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType

/**
* Represents various events related to inventory interactions, updates, and state changes.
Expand Down Expand Up @@ -82,6 +84,33 @@ sealed class InventoryEvent {
val stack: ItemStack,
) : Event

/**
* Represents an action performed on an inventory slot, such as clicking or interacting with it.
*/
abstract class SlotAction : Event, ICancellable {
/**
* Represents a click action performed on an inventory slot.
*
* This event provides detailed information about the click action, including the screen handler,
* the slot involved, the button used, and the type of action performed.
*
* This event is [Cancellable], allowing handlers to prevent the default click behavior if necessary.
*
* @property screenHandler The screen handler managing the inventory interaction.
* @property slot The slot that was clicked, or null if no slot was involved.
* @property slotId The ID of the slot that was clicked.
* @property button The mouse button used for the click action.
* @property actionType The type of action performed on the slot.
*/
data class Click(
val screenHandler: ScreenHandler,
val slot: Slot?,
val slotId: Int,
val button: Int,
val actionType: SlotActionType,
) : ICancellable by Cancellable()
}

abstract class HotbarSlot : Event {
/**
* Represents an event triggered when the client attempts to send slot update to the server.
Expand Down
121 changes: 121 additions & 0 deletions src/main/kotlin/com/lambda/module/modules/player/EasyTrash.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2025 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.module.modules.player

import com.lambda.context.SafeContext
import com.lambda.event.events.InventoryEvent
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
import com.lambda.interaction.material.container.containers.InventoryContainer
import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag
import com.lambda.util.Timer
import net.minecraft.entity.EntityType
import net.minecraft.entity.ItemEntity
import net.minecraft.item.Item
import net.minecraft.item.Items
import net.minecraft.registry.Registries.ITEM
import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.slot.SlotActionType
import kotlin.time.Duration.Companion.milliseconds

object EasyTrash : Module(
name = "EasyTrash",
description = "Automatically trashes unwanted items",
tag = ModuleTag.PLAYER
) {
private val itemsCanTrash by setting("Items Can Trash", setOf<Item>(Items.NETHERRACK, Items.COBBLESTONE), ITEM.toSet())

private val dropToPickup by setting("Drop To Pickup", true)
private val itemsToPickup by setting("Items To Pickup", setOf(), ITEM.toSet())

private val timer = Timer()

init {
listen<TickEvent.Pre> {
if (!timer.timePassed(200.milliseconds)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have an actions per second timer in the inventory config. I'd set the default value to this and let the inventory manager handle the delay

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I use the inventory request limit? Just spam inventory requests every tick?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, all requests are expected to be sent on a tickly basis to stay up to date with the rest of the game

return@listen
}
checkShouldTrashSomething()
}

listen<InventoryEvent.SlotAction.Click> { event ->
if (event.actionType != SlotActionType.QUICK_MOVE || event.button != 0) {
return@listen
}

if (event.screenHandler is GenericContainerScreenHandler) {
val sh = event.screenHandler
val rows = sh.rows

if (event.slotId >= rows * 9) {
// Clicked in inventory, not in container
return@listen
}

if (sh.slots.subList(rows * 9, sh.slots.size).any { !it.hasStack() }) {
// There is empty space in the inventory, no need to trash
return@listen
}

StackSelection.selectStack {
isOneOfItems(itemsCanTrash)
}.filterSlots(InventoryContainer.slots).firstOrNull()?.let { trashSlot ->
inventoryRequest {
pickup(event.slotId, 0)
pickup(trashSlot.id, 0)
pickup(ScreenHandler.EMPTY_SPACE_SLOT_INDEX, 0)
}.submit(true)
event.cancel()
}
}
}
}

fun SafeContext.checkShouldTrashSomething() {
if (player.health > 0.0f && !player.isSpectator && dropToPickup && !player.isCreative) {
val vehicle = player.vehicle
val box = if (vehicle != null && !vehicle.isRemoved) {
player.boundingBox.union(vehicle.boundingBox).expand(1.0, 0.0, 1.0)
} else {
player.boundingBox.expand(1.0, 0.5, 1.0)
}
val items = world.getEntitiesByType<ItemEntity>(EntityType.ITEM, box) {
entity -> itemsToPickup.contains(entity.stack.item) && entity.isOnGround
}.map { i -> i.stack.item }
if (items.isNotEmpty() && InventoryContainer.spaceAvailable(StackSelection.selectStack { isOneOfItems(items) }) <= 0) {
if (trashSomething()) timer.reset()
}
}
}

fun SafeContext.trashSomething(): Boolean {
StackSelection.selectStack {
isOneOfItems(itemsCanTrash)
}.filterSlots(InventoryContainer.slots).firstOrNull()?.let {
inventoryRequest {
throwStack(it.id)
}.submit(true)
return true
}
return false
}
}