diff --git a/fabric-screen-handler-api-v1/src/main/java/net/fabricmc/fabric/api/inventory/InventoryEvents.java b/fabric-screen-handler-api-v1/src/main/java/net/fabricmc/fabric/api/inventory/InventoryEvents.java new file mode 100644 index 0000000000..863ed33373 --- /dev/null +++ b/fabric-screen-handler-api-v1/src/main/java/net/fabricmc/fabric/api/inventory/InventoryEvents.java @@ -0,0 +1,54 @@ +package net.fabricmc.fabric.api.inventory; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +import net.minecraft.util.ActionResult; + +import org.jetbrains.annotations.ApiStatus; + +/** + * Core entry point for inventory-related events in FabricMC. + * + *
Register listeners like: + *
{@code
+ * InventoryEvents.SLOT_CLICK_EVENT.register((handler, slot, slotId, clickType, actionType, player, cursor) -> {
+ * // Example: Prevent shift-clicking specific items
+ * if (actionType == SlotActionType.QUICK_MOVE && handler.getType() == ScreenHandlerType.CHEST) {
+ * // Add custom logic here
+ * return ActionResult.FAIL; // Cancel the action
+ * }
+ * return ActionResult.PASS;
+ * });
+ *
+ *
+ */
+@ApiStatus.Experimental
+public final class InventoryEvents {
+ /**
+ * Fires for slot clicks in ANY inventory GUI.
+ *
+ * This event occurs before vanilla logic executes, allowing modders to intercept or cancel slot interactions. + * + *
Return: + *
Runs BEFORE any slot click is processed to allow interception or cancellation of the action. + * + *
Use cases: + *
Canceling with {@code ActionResult.FAIL} stops the click entirely. Returning {@code ActionResult.SUCCESS} allows the action but consumes the event. + */ +@FunctionalInterface +public interface SlotClickCallback { + /** + * Called when a player clicks a slot in an inventory GUI. + * + * @param handler ScreenHandler (e.g., chest, furnace, crafting table) + * @param slot Clicked slot (null if clicked outside inventory) + * @param slotId Raw slot ID from packet (0–215 for most GUIs, -1 for outside clicks) + * @param button Mouse button (0 = left, 1 = right) + modifiers + * @param actionType Exact action: PICKUP, QUICK_MOVE, SWAP, etc. + * @param player The player who clicked (useful for permission checks) + * @param cursor Current cursor stack (what the player is holding, never null) + * @return PASS = continue to next listener, SUCCESS/FAIL = stop event chain + */ + ActionResult interact( + ScreenHandler handler, + Slot slot, + int slotId, + int button, + SlotActionType actionType, + PlayerEntity player, + ItemStack cursor + ); +} diff --git a/fabric-screen-handler-api-v1/src/main/java/net/fabricmc/fabric/mixin/inventory/ScreenHandlerAccessorMixin.java b/fabric-screen-handler-api-v1/src/main/java/net/fabricmc/fabric/mixin/inventory/ScreenHandlerAccessorMixin.java new file mode 100644 index 0000000000..dc98523b58 --- /dev/null +++ b/fabric-screen-handler-api-v1/src/main/java/net/fabricmc/fabric/mixin/inventory/ScreenHandlerAccessorMixin.java @@ -0,0 +1,23 @@ +package net.fabricmc.fabric.mixin.inventory; + +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.util.collection.DefaultedList; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +/** + * Accessor for private fields in {@link ScreenHandler} used by inventory events. + * + *
Provides access to the list of all slot instances in the current GUI screen, including player inventory and container-specific slots.
+ */
+@Mixin(ScreenHandler.class)
+public interface ScreenHandlerAccessorMixin {
+ /**
+ * Gets the list of all slots in this ScreenHandler.
+ *
+ * @return A DefaultedList of Slot objects representing all slots in the GUI
+ */
+ @Accessor("slots")
+ DefaultedList Intercepts slot click events to trigger the {@link InventoryEvents#SLOT_CLICK_EVENT} before vanilla processing,
+ * allowing modders to modify or cancel slot interactions in any inventory GUI (e.g., chests, furnaces, crafting tables).
+ */
+@Mixin(ScreenHandler.class)
+public class ScreenHandlerMixin {
+ /**
+ * Injects into {@link ScreenHandler#onSlotClick} to fire the {@link InventoryEvents#SLOT_CLICK_EVENT}.
+ *
+ * This injection occurs at the start of the method, before vanilla logic processes the slot click.
+ * If the event returns {@link ActionResult#FAIL}, the click is canceled, and the client state is synchronized
+ * to prevent desyncs. If {@link ActionResult#SUCCESS} is returned, the event is consumed but vanilla processing continues.
+ * {@link ActionResult#PASS} allows other listeners and vanilla logic to proceed.
+ *
+ * @param slotIndex The raw slot ID from the packet (0–215 for most GUIs, -1 for outside clicks)
+ * @param button Mouse button (0 = left, 1 = right) + modifiers
+ * @param actionType The exact action: PICKUP, QUICK_MOVE, SWAP, etc.
+ * @param player The player who clicked the slot
+ * @param ci Callback info for canceling the method
+ */
+ @Inject(method = "onSlotClick", at = @At("HEAD"), cancellable = true)
+ private void onSlotClick(
+ int slotIndex,
+ int button,
+ SlotActionType actionType,
+ PlayerEntity player,
+ CallbackInfo ci
+ ) {
+ ScreenHandler handler = (ScreenHandler) (Object) this;
+ ScreenHandlerAccessorMixin accessor = (ScreenHandlerAccessorMixin) handler;
+ Slot slot = slotIndex >= 0 ? handler.getSlot(slotIndex) : null;
+ ItemStack cursor = handler.getCursorStack();
+ ActionResult result = InventoryEvents.SLOT_CLICK_EVENT.invoker().interact(
+ handler, slot, slotIndex, button, actionType, player, cursor
+ );
+
+ if (result == ActionResult.FAIL) {
+ ci.cancel();
+ // Sync client state to prevent desync
+ handler.syncState();
+ }
+ }
+}
diff --git a/fabric-screen-handler-api-v1/src/main/resources/fabric-screen-handler-api-v1.mixins.json b/fabric-screen-handler-api-v1/src/main/resources/fabric-screen-handler-api-v1.mixins.json
index 40a4878ce9..56ca542099 100644
--- a/fabric-screen-handler-api-v1/src/main/resources/fabric-screen-handler-api-v1.mixins.json
+++ b/fabric-screen-handler-api-v1/src/main/resources/fabric-screen-handler-api-v1.mixins.json
@@ -1,10 +1,12 @@
{
"required": true,
- "package": "net.fabricmc.fabric.mixin.screenhandler",
+ "package": "net.fabricmc.fabric.mixin",
"compatibilityLevel": "JAVA_21",
"mixins": [
- "NamedScreenHandlerFactoryMixin",
- "ServerPlayerEntityMixin"
+ "screenhandler.NamedScreenHandlerFactoryMixin",
+ "screenhandler.ServerPlayerEntityMixin",
+ "inventory.ScreenHandlerAccessorMixin",
+ "inventory.ScreenHandlerMixin"
],
"client": [
],
diff --git a/fabric-screen-handler-api-v1/src/main/resources/fabric.mod.json b/fabric-screen-handler-api-v1/src/main/resources/fabric.mod.json
index 75fd1adcc8..bbd1b63dec 100644
--- a/fabric-screen-handler-api-v1/src/main/resources/fabric.mod.json
+++ b/fabric-screen-handler-api-v1/src/main/resources/fabric.mod.json
@@ -12,19 +12,21 @@
"issues": "https://github.com/FabricMC/fabric/issues",
"sources": "https://github.com/FabricMC/fabric"
},
- "authors": [
- "FabricMC"
- ],
+ "authors": ["FabricMC"],
"depends": {
"fabricloader": ">=0.17.0",
"fabric-api-base": "*",
"fabric-networking-api-v1": "*"
},
"entrypoints": {
- "main": ["net.fabricmc.fabric.impl.screenhandler.Networking"],
- "client": ["net.fabricmc.fabric.impl.screenhandler.client.ClientNetworking"]
+ "main": [
+ "net.fabricmc.fabric.impl.screenhandler.Networking"
+ ],
+ "client": [
+ "net.fabricmc.fabric.impl.screenhandler.client.ClientNetworking"
+ ]
},
- "description": "Hooks and extensions for creating screen handlers.",
+ "description": "Hooks and extensions for creating screen handlers and inventory events.",
"mixins": [
"fabric-screen-handler-api-v1.mixins.json"
],
diff --git a/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/inventory/InventoryEventsTest.java b/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/inventory/InventoryEventsTest.java
new file mode 100644
index 0000000000..c29fdf8364
--- /dev/null
+++ b/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/inventory/InventoryEventsTest.java
@@ -0,0 +1,36 @@
+//package net.fabricmc.fabric.test.inventory;
+//
+//import net.fabricmc.api.ModInitializer;
+//import net.fabricmc.fabric.api.inventory.InventoryEvents;
+//
+//import net.minecraft.item.Items;
+//import net.minecraft.screen.GenericContainerScreenHandler;
+//import net.minecraft.text.Text;
+//import net.minecraft.util.ActionResult;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//
+//public class InventoryEventsTest implements ModInitializer {
+// private static final Logger LOGGER = LoggerFactory.getLogger("fabric-screen-handler-api-v1-testmod");
+//
+// @Override
+// public void onInitialize() {
+// LOGGER.info("Initializing Fabric Inventory Events Test Mod...");
+//
+// // Prevent diamonds from being moved
+// InventoryEvents.SLOT_CLICK_EVENT.register((handler, slot, slotId, button, actionType, player, cursor) -> {
+// if (!(handler instanceof GenericContainerScreenHandler) || slot == null) {
+// return ActionResult.PASS;
+// }
+//
+// if (slot.getStack().isOf(Items.DIAMOND)) {
+// player.sendMessage(Text.literal("Diamonds are protected and cannot be moved."), false);
+// LOGGER.info("Player {} tried to move diamonds.", player.getName().getString());
+// return ActionResult.FAIL;
+// }
+//
+// return ActionResult.PASS;
+// });
+// }
+//}
+//
diff --git a/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/screenhandler/ScreenHandlerTest.java b/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/screenhandler/ScreenHandlerTest.java
index 3fe9e41afc..996f8a7556 100644
--- a/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/screenhandler/ScreenHandlerTest.java
+++ b/fabric-screen-handler-api-v1/src/testmod/java/net/fabricmc/fabric/test/screenhandler/ScreenHandlerTest.java
@@ -16,6 +16,7 @@
package net.fabricmc.fabric.test.screenhandler;
+
import net.minecraft.block.AbstractBlock;
import net.minecraft.block.Block;
import net.minecraft.block.Blocks;
@@ -71,5 +72,7 @@ public void onInitialize() {
Registry.register(Registries.SCREEN_HANDLER, id("bag"), BAG_SCREEN_HANDLER);
Registry.register(Registries.SCREEN_HANDLER, id("positioned_bag"), POSITIONED_BAG_SCREEN_HANDLER);
Registry.register(Registries.SCREEN_HANDLER, id("box"), BOX_SCREEN_HANDLER);
+
+
}
}