-
Notifications
You must be signed in to change notification settings - Fork 506
Add InventoryEvents to fabric-screen-handler-api-v1 with Test Mod #4942
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 1.21.10
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * | ||
| * <p>Register listeners like: | ||
| * <pre>{@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; | ||
| * }); | ||
| * </pre> | ||
| * | ||
| */ | ||
| @ApiStatus.Experimental | ||
| public final class InventoryEvents { | ||
| /** | ||
| * Fires for slot clicks in ANY inventory GUI. | ||
| * | ||
| * <p>This event occurs before vanilla logic executes, allowing modders to intercept or cancel slot interactions. | ||
| * | ||
| * <p>Return: | ||
| * <ul> | ||
| * <li>{@code ActionResult.PASS} → Let vanilla and other listeners handle it</li> | ||
| * <li>{@code ActionResult.SUCCESS} → Stop propagation (no further listeners), allow vanilla action</li> | ||
| * <li>{@code ActionResult.FAIL} → Cancel the action </li> | ||
| * </ul> | ||
| */ | ||
| public static final Event<SlotClickCallback> SLOT_CLICK_EVENT = EventFactory.createArrayBacked( | ||
| SlotClickCallback.class, | ||
| (listeners) -> (handler, slot, slotId, clickType, actionType, player, cursor) -> { | ||
| for (SlotClickCallback listener : listeners) { | ||
| ActionResult result = listener.interact(handler, slot, slotId, clickType, actionType, player, cursor); | ||
| if (result != ActionResult.PASS) { | ||
| return result; | ||
| } | ||
| } | ||
| return ActionResult.PASS; | ||
| } | ||
| ); | ||
|
|
||
| private InventoryEvents() {} | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package net.fabricmc.fabric.api.inventory; | ||
|
|
||
| import net.minecraft.entity.player.PlayerEntity; | ||
| import net.minecraft.item.ItemStack; | ||
| import net.minecraft.screen.ScreenHandler; | ||
| import net.minecraft.screen.slot.Slot; | ||
| import net.minecraft.screen.slot.SlotActionType; | ||
| import net.minecraft.util.ActionResult; | ||
|
|
||
| /** | ||
| * Callback interface for handling slot click events in inventory GUIs. | ||
| * | ||
| * <p>Runs BEFORE any slot click is processed to allow interception or cancellation of the action. | ||
| * | ||
| * <p>Use cases: | ||
| * <ul> | ||
| * <li>Prevent shift-clicking protected items from containers</li> | ||
| * <li>Implement cooldowns for container access</li> | ||
| * <li>Perform permission checks for locked containers</li> | ||
| * </ul> | ||
| * | ||
| * <p>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, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be marked as @ Nullable, since you do pass null here. |
||
| int slotId, | ||
| int button, | ||
| SlotActionType actionType, | ||
| PlayerEntity player, | ||
| ItemStack cursor | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. | ||
| * | ||
| * <p>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<Slot> getSlots(); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package net.fabricmc.fabric.mixin.inventory; | ||
|
|
||
| import net.fabricmc.fabric.api.inventory.InventoryEvents; | ||
|
|
||
| import net.minecraft.entity.player.PlayerEntity; | ||
| import net.minecraft.item.ItemStack; | ||
| import net.minecraft.screen.ScreenHandler; | ||
| import net.minecraft.screen.slot.Slot; | ||
| import net.minecraft.screen.slot.SlotActionType; | ||
| import net.minecraft.util.ActionResult; | ||
| import org.spongepowered.asm.mixin.Mixin; | ||
| import org.spongepowered.asm.mixin.injection.At; | ||
| import org.spongepowered.asm.mixin.injection.Inject; | ||
| import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; | ||
|
|
||
| /** | ||
| * Mixin for {@link ScreenHandler} to inject inventory event handling. | ||
| * | ||
| * <p>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}. | ||
| * | ||
| * <p>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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you want for it to hold true that it always runs before actual logic, you would need to move this mixin to network handler, as mods can and do override onSlotClick method |
||
| 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(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think you should sync it like this. I'm pretty sure vanilla should have system for detecting desyncs natively, if not you should use that instead of syncing entire inventory |
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| // }); | ||
| // } | ||
| //} | ||
| // |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this class to your
InventoryEvents, example: https://github.com/FabricMC/fabric/blob/1.21.10/fabric-lifecycle-events-v1/src/main/java/net/fabricmc/fabric/api/event/lifecycle/v1/ServerLifecycleEvents.javaedit: And rename to
SlotClick, without "Callback"