Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -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<PlayerInteractEntityEvent, EquipmentSlot> 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<PlayerInteractEntityEvent, EquipmentSlot> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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);
}
}
});
}
Expand Down
Loading