From d43ed5a40e5cf344c5dbc47254e3ce2fa2100655 Mon Sep 17 00:00:00 2001 From: Naz Date: Fri, 12 Sep 2025 02:23:47 +0800 Subject: [PATCH] Partially implement UseItemOnBlockEvent --- .../events/player/UseItemOnBlockEvent.java | 215 ++++++++++++++++++ .../BlockBehaviour$BlockStateBaseMixin.java | 30 +++ .../entity/mixin/common/ItemStackMixin.java | 29 +++ .../resources/porting_lib_entity.mixins.json | 154 ++++++------- 4 files changed, 352 insertions(+), 76 deletions(-) create mode 100644 modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/events/player/UseItemOnBlockEvent.java create mode 100644 modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/BlockBehaviour$BlockStateBaseMixin.java create mode 100644 modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/ItemStackMixin.java diff --git a/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/events/player/UseItemOnBlockEvent.java b/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/events/player/UseItemOnBlockEvent.java new file mode 100644 index 00000000..8da0fa97 --- /dev/null +++ b/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/events/player/UseItemOnBlockEvent.java @@ -0,0 +1,215 @@ +package io.github.fabricators_of_create.porting_lib.entity.events.player; + +import com.google.common.base.Preconditions; + +import io.github.fabricators_of_create.porting_lib.core.event.BaseEvent; +import io.github.fabricators_of_create.porting_lib.core.event.CancellableEvent; +import net.fabricmc.api.EnvType; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; + +import net.minecraft.world.level.block.state.BlockState; + +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +/** + * Fires on both the client and server thread when a player interacts with a block. + * + *

The event fires in three phases, corresponding with the three interaction behaviors: + * {@link IItemExtension#onItemUseFirst}, + * {@link BlockBehaviour#useItemOn}, + * and {@link Item#useOn}.

+ * + *

The event fires after the interaction logic decides to run the particular interaction behavior, + * as opposed to {@link PlayerInteractEvent.RightClickBlock} + * which fires once-per-right-click, before the behavior-choosing logic.

+ * + *

If the event is cancelled via {@link #cancelWithResult}, + * then the normal interaction behavior for that phase will not run, + * and the specified {@link InteractionResult} will be returned instead.

+ */ +public class UseItemOnBlockEvent extends BaseEvent implements CancellableEvent { + public static final Event EVENT = EventFactory.createArrayBacked(Callback.class, callbacks -> event -> { + for (Callback callback : callbacks) { + callback.onUseItemOnBlock(event); + } + }); + + public interface Callback { + void onUseItemOnBlock(UseItemOnBlockEvent event); + } + + private final Level level; + @Nullable + private final Player player; + private final InteractionHand hand; + private final ItemStack heldItem; + private final BlockPos pos; + @Nullable + private final Direction face; + private final UseOnContext context; + private final UsePhase usePhase; + private ItemInteractionResult cancellationResult = ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + + public UseItemOnBlockEvent(UseOnContext context, UsePhase usePhase) { + super(); + this.level = Preconditions.checkNotNull(context.getLevel(), "Null level in UseItemOnBlockEvent!"); + this.player = context.getPlayer(); + this.heldItem = Preconditions.checkNotNull(context.getItemInHand(), "Null heldItem in UseItemOnBlockEvent!"); + this.hand = Preconditions.checkNotNull(context.getHand(), "Null hand in UseItemOnBlockEvent!"); + this.pos = Preconditions.checkNotNull(context.getClickedPos(), "Null position in UseItemOnBlockEvent!"); + this.face = context.getClickedFace(); + this.context = context; + this.usePhase = usePhase; + } + + /** + * @return player or null + */ + @Nullable + public Player getPlayer() { + return player; + } + + /** + * @return The hand involved in this interaction. Will never be null. + */ + public InteractionHand getHand() { + return hand; + } + + /** + * @return The itemstack involved in this interaction, {@code ItemStack.EMPTY} if the hand was empty. + */ + public ItemStack getItemStack() { + return heldItem; + } + + /** + * If the interaction was on an entity, will be a BlockPos centered on the entity. + * If the interaction was on a block, will be the position of that block. + * Otherwise, will be a BlockPos centered on the player. + * Will never be null. + * + * @return The position involved in this interaction. + */ + public BlockPos getPos() { + return pos; + } + + /** + * @return The face involved in this interaction. For all non-block interactions, this will return null. + */ + @Nullable + public Direction getFace() { + return face; + } + + /** + * @return Convenience method to get the level of this interaction. + */ + public Level getLevel() { + return level; + } + + /** + * @return context + */ + public UseOnContext getUseOnContext() { + return this.context; + } + + /** + * {@return The Use Phase of the interaction} + * + * @see UsePhase for semantics + */ + public UsePhase getUsePhase() { + return this.usePhase; + } + + /** + * @return The effective, i.e. logical, side of this interaction. This will be {@link EnvType#CLIENT} on the client thread, and {@link EnvType#SERVER} on the server thread. + */ + public EnvType getSide() { + return getLevel().isClientSide ? EnvType.CLIENT : EnvType.SERVER; + } + + /** + *

Cancels the use interaction (preventing the block or item's use behavior from running) and provides the + * specified result to the interaction logic instead.

+ * + *

Invoke this if you intend to prevent the default interaction behavior and replace it with your own.

+ * + * @param result InteractionResult to return to the interaction logic + * + */ + public void cancelWithResult(ItemInteractionResult result) { + this.setCancellationResult(result); + this.setCanceled(true); + } + + /** + * @return The {@link ItemInteractionResult} that will be returned to vanilla if the event is cancelled, instead of calling the relevant + * method of the event. By default, this is {@link ItemInteractionResult#PASS_TO_DEFAULT_BLOCK_INTERACTION}, meaning cancelled events will cause + * the client to keep trying more interactions until something works. + */ + public ItemInteractionResult getCancellationResult() { + return cancellationResult; + } + + /** + * Set the {@link ItemInteractionResult} that will be returned to vanilla if the event is cancelled, instead of calling the relevant + * method of the event. + */ + public void setCancellationResult(ItemInteractionResult result) { + this.cancellationResult = result; + } + + @Override + public void sendEvent() { + EVENT.invoker().onUseItemOnBlock(this); + } + + public enum UsePhase { + /** + * The {@link IItemExtension#onItemUseFirst(ItemStack, UseOnContext)} interaction. + * This is noop/PASS for most items, but some mods' items have interactions here. + */ + ITEM_BEFORE_BLOCK, + + /** + * The {@link BlockBehaviour#useItemOn} interaction. + * Skipped if the player is sneaking and holding an item that skips the block while sneaking (most items). + */ + BLOCK, + + /** + * The {@link Item#useOn} interaction. + */ + ITEM_AFTER_BLOCK + } +} diff --git a/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/BlockBehaviour$BlockStateBaseMixin.java b/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/BlockBehaviour$BlockStateBaseMixin.java new file mode 100644 index 00000000..9a44c8d4 --- /dev/null +++ b/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/BlockBehaviour$BlockStateBaseMixin.java @@ -0,0 +1,30 @@ +package io.github.fabricators_of_create.porting_lib.entity.mixin.common; + +import io.github.fabricators_of_create.porting_lib.entity.events.player.UseItemOnBlockEvent; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockBehaviour; + +import net.minecraft.world.phys.BlockHitResult; + +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.CallbackInfoReturnable; + +@Mixin(BlockBehaviour.BlockStateBase.class) +public abstract class BlockBehaviour$BlockStateBaseMixin { + @Inject(method = "useItemOn", at = @At("HEAD"), cancellable = true) + private void callUseItemOnBlockEvent(ItemStack stack, Level level, Player player, InteractionHand hand, BlockHitResult hitResult, CallbackInfoReturnable cir) { + var useOnContext = new UseOnContext(level, player, hand, player.getItemInHand(hand).copy(), hitResult); + var event = new UseItemOnBlockEvent(useOnContext, UseItemOnBlockEvent.UsePhase.BLOCK); + + if (event.post()) { + cir.setReturnValue(event.getCancellationResult()); + } + } +} diff --git a/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/ItemStackMixin.java b/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/ItemStackMixin.java new file mode 100644 index 00000000..7c302dfe --- /dev/null +++ b/modules/entity/src/main/java/io/github/fabricators_of_create/porting_lib/entity/mixin/common/ItemStackMixin.java @@ -0,0 +1,29 @@ +package io.github.fabricators_of_create.porting_lib.entity.mixin.common; + +import io.github.fabricators_of_create.porting_lib.entity.events.player.UseItemOnBlockEvent; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.BlockHitResult; + +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.CallbackInfoReturnable; + +@Mixin(ItemStack.class) +public abstract class ItemStackMixin { + @Inject(method = "useOn", at = @At("HEAD"), cancellable = true) + private void callUseItemOnBlockEvent(UseOnContext context, CallbackInfoReturnable cir) { + var event = new UseItemOnBlockEvent(context, UseItemOnBlockEvent.UsePhase.ITEM_AFTER_BLOCK); + + if (event.post()) { + cir.setReturnValue(event.getCancellationResult().result()); + } + } +} diff --git a/modules/entity/src/main/resources/porting_lib_entity.mixins.json b/modules/entity/src/main/resources/porting_lib_entity.mixins.json index 00121b7d..554b204f 100644 --- a/modules/entity/src/main/resources/porting_lib_entity.mixins.json +++ b/modules/entity/src/main/resources/porting_lib_entity.mixins.json @@ -1,79 +1,81 @@ { - "required": true, - "minVersion": "0.8", - "package": "io.github.fabricators_of_create.porting_lib.entity.mixin", - "compatibilityLevel": "JAVA_17", - "mixins": [ - "accessor.PlayerDataStorageAccessor", - "common.AbstractArrowMixin", - "common.AbstractHorseMixin", - "common.AbstractHurtingProjectileMixin", - "common.AbstractMinecartMixin", - "common.BlockableEventLoopAccessor", - "common.BlockBehaviourMixin", - "common.BoggedMixin", - "common.BundlePacketMixin", - "common.ChorusFruitItemMixin", - "common.EnderManMixin", - "common.EntityMixin", - "common.ExperienceOrbMixin", - "common.FireworkRocketEntityMixin", - "common.FishingHookMixin", - "common.FurnaceResultSlotMixin", - "common.ItemMixin", - "common.LevelMixin", - "common.LightningBoltMixin", - "common.LivingEntityMixin", - "common.LlamaSpitMixin", - "common.MagmaCubeMixin", - "common.MobEffectInstance$DetailsMixin", - "common.MobEffectInstanceMixin", - "common.MobEffectMixin", - "common.MobMixin", - "common.MushroomCowMixin", - "common.PersistentEntitySectionManager$CallbackMixin", - "common.PersistentEntitySectionManagerMixin", - "common.PlayerDataStorageMixin", - "common.PlayerListMixin", - "common.PlayerMixin", - "common.ProjectileUtilMixin", - "common.ResultSlotMixin", - "common.ServerEntityMixin", - "common.ServerGamePacketListenerImpl$1Mixin", - "common.ServerLevel$EntityCallbacksMixin", - "common.ServerLevelMixin", - "common.ServerPlayerGameModeMixin", - "common.ServerPlayerMixin", - "common.ShearableMixin", - "common.ShearsDispenseItemBehaviorMixin", - "common.SheepMixin", - "common.ShulkerBulletMixin", - "common.ShulkerMixin", - "common.SlimeMixin", - "common.SnowGolemMixin", - "common.SpreadPlayersCommandMixin", - "common.StartAttackingMixin", - "common.TeleportCommandMixin", - "common.ThrowableProjectileMixin", - "common.ThrownEnderpearlMixin", - "common.TransientEntitySectionManager$CallbackMixin" - ], - "injectors": { - "defaultRequire": 1, - "maxShiftBy": 5 - }, - "client": [ - "client.ClientLevel$EntityCallbacksMixin", - "client.ClientLevelMixin", - "client.EffectRenderingInventoryScreenMixin", - "client.EntityRenderDispatcherMixin", - "client.GuiMixin", - "client.LocalPlayerMixin", - "client.MinecraftMixin", - "client.MultiPlayerGameModeMixin", - "client.RemotePlayerMixin" - ], - "mixinextras": { - "minVersion": "0.5.0-rc.2" + "required": true, + "minVersion": "0.8", + "package": "io.github.fabricators_of_create.porting_lib.entity.mixin", + "compatibilityLevel": "JAVA_17", + "mixins": [ + "accessor.PlayerDataStorageAccessor", + "common.AbstractArrowMixin", + "common.AbstractHorseMixin", + "common.AbstractHurtingProjectileMixin", + "common.AbstractMinecartMixin", + "common.BlockableEventLoopAccessor", + "common.BlockBehaviour$BlockStateBaseMixin", + "common.BlockBehaviourMixin", + "common.BoggedMixin", + "common.BundlePacketMixin", + "common.ChorusFruitItemMixin", + "common.EnderManMixin", + "common.EntityMixin", + "common.ExperienceOrbMixin", + "common.FireworkRocketEntityMixin", + "common.FishingHookMixin", + "common.FurnaceResultSlotMixin", + "common.ItemMixin", + "common.ItemStackMixin", + "common.LevelMixin", + "common.LightningBoltMixin", + "common.LivingEntityMixin", + "common.LlamaSpitMixin", + "common.MagmaCubeMixin", + "common.MobEffectInstance$DetailsMixin", + "common.MobEffectInstanceMixin", + "common.MobEffectMixin", + "common.MobMixin", + "common.MushroomCowMixin", + "common.PersistentEntitySectionManager$CallbackMixin", + "common.PersistentEntitySectionManagerMixin", + "common.PlayerDataStorageMixin", + "common.PlayerListMixin", + "common.PlayerMixin", + "common.ProjectileUtilMixin", + "common.ResultSlotMixin", + "common.ServerEntityMixin", + "common.ServerGamePacketListenerImpl$1Mixin", + "common.ServerLevel$EntityCallbacksMixin", + "common.ServerLevelMixin", + "common.ServerPlayerGameModeMixin", + "common.ServerPlayerMixin", + "common.ShearableMixin", + "common.ShearsDispenseItemBehaviorMixin", + "common.SheepMixin", + "common.ShulkerBulletMixin", + "common.ShulkerMixin", + "common.SlimeMixin", + "common.SnowGolemMixin", + "common.SpreadPlayersCommandMixin", + "common.StartAttackingMixin", + "common.TeleportCommandMixin", + "common.ThrowableProjectileMixin", + "common.ThrownEnderpearlMixin", + "common.TransientEntitySectionManager$CallbackMixin" + ], + "injectors": { + "defaultRequire": 1, + "maxShiftBy": 5 + }, + "client": [ + "client.ClientLevel$EntityCallbacksMixin", + "client.ClientLevelMixin", + "client.EffectRenderingInventoryScreenMixin", + "client.EntityRenderDispatcherMixin", + "client.GuiMixin", + "client.LocalPlayerMixin", + "client.MinecraftMixin", + "client.MultiPlayerGameModeMixin", + "client.RemotePlayerMixin" + ], + "mixinextras": { + "minVersion": "0.5.0-rc.2" } }