Skip to content

Commit 5fda156

Browse files
authored
fix: only handle npc interact when performed with main hand (#1673)
### Motivation The client sends the interact packet twice (in most cases): once for the main hand and once for the off hand. This causes all right click actions of selector entities to be executed twice. ### Modification Only actually handle the right click action to a selector entity if the interact happened with the main hand. In all other cases we just ignore the interact and, if needed, just cancel the interact event (required to cancel certain actions on mobs such as shearing a sheep). ### Result Selector entity right click actions are only performed once, not twice in many cases. ##### Other context https://canary.discord.com/channels/325362837184577536/818777626663321671/1388872733903028265
1 parent 78215bf commit 5fda156

2 files changed

Lines changed: 121 additions & 13 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2019-2024 CloudNetService team & contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package eu.cloudnetservice.modules.npc.impl.platform.bukkit;
18+
19+
import io.vavr.CheckedFunction1;
20+
import java.lang.invoke.MethodHandles;
21+
import java.lang.invoke.MethodType;
22+
import lombok.NonNull;
23+
import org.bukkit.event.player.PlayerInteractEntityEvent;
24+
import org.bukkit.inventory.EquipmentSlot;
25+
import org.jetbrains.annotations.ApiStatus;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
/**
30+
* Utility to ensure compatibility with a variety of minecraft versions.
31+
*
32+
* @since 4.0
33+
*/
34+
@ApiStatus.Internal
35+
public final class BukkitCompatibility {
36+
37+
private static final Logger LOGGER = LoggerFactory.getLogger(BukkitCompatibility.class);
38+
39+
// get the hand used in an interaction event, always returns EquipmentSlot.HAND on 1.8
40+
private static final CheckedFunction1<PlayerInteractEntityEvent, EquipmentSlot> GET_INTERACTION_HAND;
41+
42+
static {
43+
var lookup = MethodHandles.publicLookup();
44+
45+
// resolve a method handle to get the hand used in a player interact event (introduced in 1.9)
46+
CheckedFunction1<PlayerInteractEntityEvent, EquipmentSlot> getInteractionHand;
47+
try {
48+
var getHand = lookup.findVirtual(
49+
PlayerInteractEntityEvent.class,
50+
"getHand",
51+
MethodType.methodType(EquipmentSlot.class));
52+
getInteractionHand = event -> (EquipmentSlot) getHand.invokeExact(event);
53+
LOGGER.debug("org.bukkit.event.player.PlayerInteractEntityEvent.getHand(): available");
54+
} catch (Exception ex) {
55+
getInteractionHand = _ -> EquipmentSlot.HAND;
56+
LOGGER.debug("org.bukkit.event.player.PlayerInteractEntityEvent.getHand(): unavailable ({})", ex.getMessage());
57+
}
58+
59+
GET_INTERACTION_HAND = getInteractionHand;
60+
}
61+
62+
private BukkitCompatibility() {
63+
throw new UnsupportedOperationException();
64+
}
65+
66+
/**
67+
* Resolves the hand that triggered the given interact event. Returns {@link EquipmentSlot#HAND} in case the hand
68+
* method doesn't exist or the hand couldn't be resolved.
69+
*
70+
* @param event the event to get the used hand from.
71+
* @return the equipment slot of the hand that triggered the given event.
72+
* @throws NullPointerException if the given interact event is null.
73+
*/
74+
public static @NonNull EquipmentSlot usedHand(@NonNull PlayerInteractEntityEvent event) {
75+
try {
76+
return GET_INTERACTION_HAND.apply(event);
77+
} catch (Throwable throwable) {
78+
LOGGER.warn("could not resolve interaction hand from event {}", event, throwable);
79+
return EquipmentSlot.HAND;
80+
}
81+
}
82+
}

modules/npcs/impl/src/main/java/eu/cloudnetservice/modules/npc/impl/platform/bukkit/listener/BukkitFunctionalityListener.java

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.github.juliarn.npclib.api.protocol.meta.EntityMetadataFactory;
2626
import com.github.juliarn.npclib.ext.labymod.LabyModExtension;
2727
import eu.cloudnetservice.modules.npc.NPC;
28+
import eu.cloudnetservice.modules.npc.impl.platform.bukkit.BukkitCompatibility;
2829
import eu.cloudnetservice.modules.npc.impl.platform.bukkit.BukkitPlatformNPCManagement;
2930
import eu.cloudnetservice.modules.npc.impl.platform.bukkit.entity.NPCBukkitPlatformSelector;
3031
import jakarta.inject.Inject;
@@ -43,6 +44,7 @@
4344
import org.bukkit.event.inventory.InventoryClickEvent;
4445
import org.bukkit.event.player.PlayerInteractEntityEvent;
4546
import org.bukkit.event.player.PlayerJoinEvent;
47+
import org.bukkit.inventory.EquipmentSlot;
4648
import org.bukkit.inventory.ItemStack;
4749
import org.bukkit.plugin.Plugin;
4850
import org.bukkit.scheduler.BukkitScheduler;
@@ -108,24 +110,28 @@ public void handleNpcShow(@NonNull ShowNpcEvent.Post event) {
108110
public void handleNpcAttack(@NonNull AttackNpcEvent event) {
109111
this.scheduler.runTask(
110112
this.plugin,
111-
() -> this.handleClick(event.player(), null, event.npc().entityId(), true));
113+
() -> this.handleClick(event.player(), null, event.npc().entityId(), true, false));
112114
}
113115

114116
public void handleNpcInteract(@NonNull InteractNpcEvent event) {
115-
this.scheduler.runTask(
116-
this.plugin,
117-
() -> this.handleClick(event.player(), null, event.npc().entityId(), false));
117+
if (event.hand() == InteractNpcEvent.Hand.MAIN_HAND) {
118+
this.scheduler.runTask(
119+
this.plugin,
120+
() -> this.handleClick(event.player(), null, event.npc().entityId(), false, false));
121+
}
118122
}
119123

120124
@EventHandler
121125
public void handle(@NonNull PlayerInteractEntityEvent event) {
122-
this.handleClick(event.getPlayer(), event, event.getRightClicked().getEntityId(), false);
126+
var hand = BukkitCompatibility.usedHand(event);
127+
var ignoreInteraction = hand != EquipmentSlot.HAND;
128+
this.handleClick(event.getPlayer(), event, event.getRightClicked().getEntityId(), false, ignoreInteraction);
123129
}
124130

125131
@EventHandler(ignoreCancelled = true)
126132
public void handle(@NonNull EntityDamageByEntityEvent event) {
127133
if (event.getDamager() instanceof Player damager) {
128-
this.handleClick(damager, event, event.getEntity().getEntityId(), true);
134+
this.handleClick(damager, event, event.getEntity().getEntityId(), true, false);
129135
}
130136
}
131137

@@ -210,20 +216,40 @@ public void playOnJoinEmoteIds(@NonNull PlayerJoinEvent event) {
210216
return status;
211217
}
212218

213-
private void handleClick(@NonNull Player player, @Nullable Cancellable cancellable, int entityId, boolean left) {
219+
/**
220+
* Handles a click by the given player to the entity with the given id, in case the associated entity is a selector
221+
* entity. An optional cancellable can be provided that will be marked as canceled in case and selector entity exists
222+
* for the entity with the given id.
223+
*
224+
* @param player the player that clicked the entity.
225+
* @param cancellable optional cancelable that will be marked as canceled if a selector entity was clicked.
226+
* @param entityId the id of the entity that was clicked.
227+
* @param left true if the click was performed with the left mouse button, false otherwise.
228+
* @param onlyCancel true to only mark the cancelable as canceled and ignore the click action otherwise.
229+
* @throws NullPointerException if the given player is null.
230+
*/
231+
private void handleClick(
232+
@NonNull Player player,
233+
@Nullable Cancellable cancellable,
234+
int entityId,
235+
boolean left,
236+
boolean onlyCancel
237+
) {
214238
this.management.trackedEntities().values().stream()
215239
.filter(npc -> npc.entityId() == entityId)
216240
.findFirst()
217241
.ifPresent(entity -> {
218-
// cancel the event if needed
219242
if (cancellable != null) {
220243
cancellable.setCancelled(true);
221244
}
222-
// handle click
223-
if (left) {
224-
entity.handleLeftClickAction(player);
225-
} else {
226-
entity.handleRightClickAction(player);
245+
246+
// handle the click if the invocation was not just performed to prevent an interaction from being performed
247+
if (!onlyCancel) {
248+
if (left) {
249+
entity.handleLeftClickAction(player);
250+
} else {
251+
entity.handleRightClickAction(player);
252+
}
227253
}
228254
});
229255
}

0 commit comments

Comments
 (0)