diff --git a/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/BukkitCompatibility.java b/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/BukkitCompatibility.java new file mode 100644 index 0000000000..bceee440c8 --- /dev/null +++ b/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/BukkitCompatibility.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019-2024 CloudNetService team & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.cloudnetservice.modules.npc.impl.platform.bukkit; + +import io.vavr.CheckedFunction1; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import lombok.NonNull; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility to ensure compatibility with a variety of minecraft versions. + * + * @since 4.0 + */ +@ApiStatus.Internal +public final class BukkitCompatibility { + + private static final Logger LOGGER = LoggerFactory.getLogger(BukkitCompatibility.class); + + // get the hand used in an interaction event, always returns EquipmentSlot.HAND on 1.8 + private static final CheckedFunction1 GET_INTERACTION_HAND; + + static { + var lookup = MethodHandles.publicLookup(); + + // resolve a method handle to get the hand used in a player interact event (introduced in 1.9) + CheckedFunction1 getInteractionHand; + try { + var getHand = lookup.findVirtual( + PlayerInteractEntityEvent.class, + "getHand", + MethodType.methodType(EquipmentSlot.class)); + getInteractionHand = event -> (EquipmentSlot) getHand.invokeExact(event); + LOGGER.debug("org.bukkit.event.player.PlayerInteractEntityEvent.getHand(): available"); + } catch (Exception ex) { + getInteractionHand = _ -> EquipmentSlot.HAND; + LOGGER.debug("org.bukkit.event.player.PlayerInteractEntityEvent.getHand(): unavailable ({})", ex.getMessage()); + } + + GET_INTERACTION_HAND = getInteractionHand; + } + + private BukkitCompatibility() { + throw new UnsupportedOperationException(); + } + + /** + * Resolves the hand that triggered the given interact event. Returns {@link EquipmentSlot#HAND} in case the hand + * method doesn't exist or the hand couldn't be resolved. + * + * @param event the event to get the used hand from. + * @return the equipment slot of the hand that triggered the given event. + * @throws NullPointerException if the given interact event is null. + */ + public static @NonNull EquipmentSlot usedHand(@NonNull PlayerInteractEntityEvent event) { + try { + return GET_INTERACTION_HAND.apply(event); + } catch (Throwable throwable) { + LOGGER.warn("could not resolve interaction hand from event {}", event, throwable); + return EquipmentSlot.HAND; + } + } +} diff --git a/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/listener/BukkitFunctionalityListener.java b/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/listener/BukkitFunctionalityListener.java index 1ce3fe67bc..4ab7450067 100644 --- a/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/listener/BukkitFunctionalityListener.java +++ b/modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/listener/BukkitFunctionalityListener.java @@ -25,6 +25,7 @@ import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory; import com.github.juliarn.npclib.ext.labymod.LabyModExtension; import eu.cloudnetservice.modules.npc.NPC; +import eu.cloudnetservice.modules.npc.impl.platform.bukkit.BukkitCompatibility; import eu.cloudnetservice.modules.npc.impl.platform.bukkit.BukkitPlatformNPCManagement; import eu.cloudnetservice.modules.npc.impl.platform.bukkit.entity.NPCBukkitPlatformSelector; import jakarta.inject.Inject; @@ -43,6 +44,7 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.player.PlayerInteractEntityEvent; import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.Plugin; import org.bukkit.scheduler.BukkitScheduler; @@ -108,24 +110,28 @@ public void handleNpcShow(@NonNull ShowNpcEvent.Post event) { public void handleNpcAttack(@NonNull AttackNpcEvent event) { this.scheduler.runTask( this.plugin, - () -> this.handleClick(event.player(), null, event.npc().entityId(), true)); + () -> this.handleClick(event.player(), null, event.npc().entityId(), true, false)); } public void handleNpcInteract(@NonNull InteractNpcEvent event) { - this.scheduler.runTask( - this.plugin, - () -> this.handleClick(event.player(), null, event.npc().entityId(), false)); + if (event.hand() == InteractNpcEvent.Hand.MAIN_HAND) { + this.scheduler.runTask( + this.plugin, + () -> this.handleClick(event.player(), null, event.npc().entityId(), false, false)); + } } @EventHandler public void handle(@NonNull PlayerInteractEntityEvent event) { - this.handleClick(event.getPlayer(), event, event.getRightClicked().getEntityId(), false); + var hand = BukkitCompatibility.usedHand(event); + var ignoreInteraction = hand != EquipmentSlot.HAND; + this.handleClick(event.getPlayer(), event, event.getRightClicked().getEntityId(), false, ignoreInteraction); } @EventHandler(ignoreCancelled = true) public void handle(@NonNull EntityDamageByEntityEvent event) { if (event.getDamager() instanceof Player damager) { - this.handleClick(damager, event, event.getEntity().getEntityId(), true); + this.handleClick(damager, event, event.getEntity().getEntityId(), true, false); } } @@ -210,20 +216,40 @@ public void playOnJoinEmoteIds(@NonNull PlayerJoinEvent event) { return status; } - private void handleClick(@NonNull Player player, @Nullable Cancellable cancellable, int entityId, boolean left) { + /** + * Handles a click by the given player to the entity with the given id, in case the associated entity is a selector + * entity. An optional cancellable can be provided that will be marked as canceled in case and selector entity exists + * for the entity with the given id. + * + * @param player the player that clicked the entity. + * @param cancellable optional cancelable that will be marked as canceled if a selector entity was clicked. + * @param entityId the id of the entity that was clicked. + * @param left true if the click was performed with the left mouse button, false otherwise. + * @param onlyCancel true to only mark the cancelable as canceled and ignore the click action otherwise. + * @throws NullPointerException if the given player is null. + */ + private void handleClick( + @NonNull Player player, + @Nullable Cancellable cancellable, + int entityId, + boolean left, + boolean onlyCancel + ) { this.management.trackedEntities().values().stream() .filter(npc -> npc.entityId() == entityId) .findFirst() .ifPresent(entity -> { - // cancel the event if needed if (cancellable != null) { cancellable.setCancelled(true); } - // handle click - if (left) { - entity.handleLeftClickAction(player); - } else { - entity.handleRightClickAction(player); + + // handle the click if the invocation was not just performed to prevent an interaction from being performed + if (!onlyCancel) { + if (left) { + entity.handleLeftClickAction(player); + } else { + entity.handleRightClickAction(player); + } } }); }