Skip to content

Commit 143b9c2

Browse files
committed
More proper bow item detection
Introduces Projectile2ItemCache to reliably associate fired projectiles with the item used to shoot them. This replaces the previous temporary workaround that was vulnerable to player exploits, ensuring accurate item tracking during damage events.
1 parent cc3c6bc commit 143b9c2

File tree

8 files changed

+98
-8
lines changed

8 files changed

+98
-8
lines changed

bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@
147147
import com.leonardobishop.quests.bukkit.util.CompatUtils;
148148
import com.leonardobishop.quests.bukkit.util.FormatUtils;
149149
import com.leonardobishop.quests.bukkit.util.LogHistory;
150+
import com.leonardobishop.quests.bukkit.util.Projectile2ItemCache;
150151
import com.leonardobishop.quests.common.config.ConfigProblem;
151152
import com.leonardobishop.quests.common.config.ConfigProblemDescriptions;
152153
import com.leonardobishop.quests.common.config.QuestsConfig;
@@ -226,6 +227,7 @@ public class BukkitQuestsPlugin extends JavaPlugin implements Quests {
226227
private QuestsBossBar bossBarHandle;
227228
private QuestsActionBar actionBarHandle;
228229
private VersionSpecificHandler versionSpecificHandler;
230+
private Projectile2ItemCache projectile2ItemCache;
229231

230232
private LogHistory logHistory;
231233
private WrappedTask questAutoSaveTask;
@@ -370,6 +372,9 @@ public void onEnable() {
370372
};
371373
}
372374

375+
// Instantiate Projectile to ItemStack cache
376+
this.projectile2ItemCache = new Projectile2ItemCache();
377+
373378
// Set item getter to be used by Quests config
374379
this.questsConfig.setItemGetter(this.itemGetter);
375380

@@ -972,6 +977,10 @@ public VersionSpecificHandler getVersionSpecificHandler() {
972977
return versionSpecificHandler;
973978
}
974979

980+
public Projectile2ItemCache getProjectile2ItemCache() {
981+
return projectile2ItemCache;
982+
}
983+
975984
public QuestItemRegistry getQuestItemRegistry() {
976985
return questItemRegistry;
977986
}

bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,13 @@ public interface VersionSpecificHandler {
245245
/**
246246
* {@link DamageSource}s were introduced in {@code 1.20.4}.
247247
*/
248-
@SuppressWarnings("UnstableApiUsage")
249248
@Nullable Player getDamager(@Nullable EntityDamageEvent lastDamageCause);
250249

250+
/**
251+
* {@link DamageSource}s were introduced in {@code 1.20.4}.
252+
*/
253+
@Nullable Entity getDirectSource(@Nullable EntityDamageEvent lastDamageCause);
254+
251255
/**
252256
* {@link Tag#CANDLE_CAKES} was introduced in {@code 1.17}.
253257
*/

bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler20.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public String getSmithMode(SmithItemEvent event) {
5454
}
5555
}
5656

57-
@SuppressWarnings("UnstableApiUsage")
5857
@Override
5958
public @Nullable Player getDamager(@Nullable EntityDamageEvent event) {
6059
if (!DAMAGE_SOURCE_API) {
@@ -74,4 +73,18 @@ public String getSmithMode(SmithItemEvent event) {
7473

7574
return null;
7675
}
76+
77+
@Override
78+
public @Nullable Entity getDirectSource(@Nullable EntityDamageEvent event) {
79+
if (!DAMAGE_SOURCE_API) {
80+
return super.getDamager(event);
81+
}
82+
83+
if (event == null) {
84+
return null;
85+
}
86+
87+
DamageSource source = event.getDamageSource();
88+
return source.getDirectEntity();
89+
}
7790
}

bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler8.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,11 @@ public List<Entity> getPassengers(Entity entity) {
226226
return null;
227227
}
228228

229+
@Override
230+
public @Nullable Entity getDirectSource(@Nullable EntityDamageEvent lastDamageCause) {
231+
return null;
232+
}
233+
229234
@Override
230235
public boolean isCake(Material type) {
231236
return type == Material.CAKE;

bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/DealDamageTaskType.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ public void onReady() {
4545

4646
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
4747
public void onEntityDamage(EntityDamageEvent event) {
48-
Entity entity = event.getEntity();
4948
Player player = plugin.getVersionSpecificHandler().getDamager(event);
50-
5149
if (player == null || player.hasMetadata("NPC")) {
5250
return;
5351
}
@@ -57,10 +55,15 @@ public void onEntityDamage(EntityDamageEvent event) {
5755
return;
5856
}
5957

58+
Entity entity = event.getEntity();
6059
if (!(entity instanceof Damageable damageable)) {
6160
return;
6261
}
6362

63+
Entity directSource = plugin.getVersionSpecificHandler().getDirectSource(event);
64+
ItemStack bowItem = directSource != null ? plugin.getProjectile2ItemCache().getItem(directSource) : null;
65+
ItemStack item = bowItem != null ? bowItem : plugin.getVersionSpecificHandler().getItemInMainHand(player);
66+
6467
// Clamp entity damage as getDamage() returns Float.MAX_VALUE for killing a parrot with a cookie
6568
// https://github.com/LMBishop/Quests/issues/753
6669
double finalDamage = event.getFinalDamage();
@@ -87,7 +90,6 @@ public void onEntityDamage(EntityDamageEvent event) {
8790
}
8891

8992
if (task.hasConfigKey("item")) {
90-
ItemStack item = plugin.getVersionSpecificHandler().getItemInMainHand(player);
9193
if (item == null) {
9294
super.debug("Specific item is required, player has no item in hand; continuing...", quest.getId(), task.getId(), player.getUniqueId());
9395
continue;

bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/MobkillingTaskType.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ private void handle(Player player, LivingEntity entity, int eventAmount) {
9999
return;
100100
}
101101

102+
EntityDamageEvent lastDamageCause = entity.getLastDamageCause();
103+
Entity directSource = plugin.getVersionSpecificHandler().getDirectSource(lastDamageCause);
104+
ItemStack bowItem = directSource != null ? plugin.getProjectile2ItemCache().getItem(directSource) : null;
105+
ItemStack item = bowItem != null ? bowItem : plugin.getVersionSpecificHandler().getItemInMainHand(player);
106+
102107
//noinspection deprecation
103108
String customName = entity.getCustomName();
104109

@@ -137,7 +142,6 @@ private void handle(Player player, LivingEntity entity, int eventAmount) {
137142
}
138143

139144
if (task.hasConfigKey("item")) {
140-
ItemStack item = plugin.getVersionSpecificHandler().getItemInMainHand(player);
141145
if (item == null) {
142146
super.debug("Specific item is required, player has no item in hand; continuing...", quest.getId(), task.getId(), player.getUniqueId());
143147
continue;

bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/dependent/MythicMobsDealDamageTaskType.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ public void onEntityDamage(EntityDamageEvent event) {
100100
}
101101

102102
private void handle(final EntityDamageEvent event, final String mobName, final double level) {
103-
Entity entity = event.getEntity();
104103
Player player = plugin.getVersionSpecificHandler().getDamager(event);
105104

106105
if (player == null || player.hasMetadata("NPC")) {
@@ -112,10 +111,15 @@ private void handle(final EntityDamageEvent event, final String mobName, final d
112111
return;
113112
}
114113

114+
Entity entity = event.getEntity();
115115
if (!(entity instanceof Damageable damageable)) {
116116
return;
117117
}
118118

119+
Entity directSource = plugin.getVersionSpecificHandler().getDirectSource(event);
120+
ItemStack bowItem = directSource != null ? plugin.getProjectile2ItemCache().getItem(directSource) : null;
121+
ItemStack item = bowItem != null ? bowItem : plugin.getVersionSpecificHandler().getItemInMainHand(player);
122+
119123
// Clamp entity damage as getDamage() returns Float.MAX_VALUE for killing a parrot with a cookie
120124
// https://github.com/LMBishop/Quests/issues/753
121125
double finalDamage = event.getFinalDamage();
@@ -147,7 +151,6 @@ private void handle(final EntityDamageEvent event, final String mobName, final d
147151
}
148152

149153
if (task.hasConfigKey("item")) {
150-
ItemStack item = plugin.getVersionSpecificHandler().getItemInMainHand(player);
151154
if (item == null) {
152155
super.debug("Specific item is required, player has no item in hand; continuing...", quest.getId(), task.getId(), player.getUniqueId());
153156
continue;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package com.leonardobishop.quests.bukkit.util;
2+
3+
import org.bukkit.entity.Entity;
4+
import org.bukkit.entity.LivingEntity;
5+
import org.bukkit.entity.Player;
6+
import org.bukkit.event.EventHandler;
7+
import org.bukkit.event.EventPriority;
8+
import org.bukkit.event.Listener;
9+
import org.bukkit.event.entity.EntityShootBowEvent;
10+
import org.bukkit.inventory.ItemStack;
11+
import org.jspecify.annotations.NullMarked;
12+
import org.jspecify.annotations.Nullable;
13+
14+
import java.util.Map;
15+
import java.util.WeakHashMap;
16+
17+
/**
18+
* Provides a cache that links projectiles to the item used to fire them.
19+
*
20+
* <p>This cache exists because damage-related events do not expose
21+
* information about the item from which a projectile was fired.
22+
* By capturing this association at the time the projectile is created,
23+
* the item can later be retrieved when handling damage events.</p>
24+
*/
25+
@NullMarked
26+
public final class Projectile2ItemCache implements Listener {
27+
28+
private final Map<Entity, @Nullable ItemStack> backingMap;
29+
30+
public Projectile2ItemCache() {
31+
this.backingMap = WeakHashMap.newWeakHashMap(1024);
32+
}
33+
34+
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
35+
public void onEntityShootBow(final EntityShootBowEvent event) {
36+
final LivingEntity shooter = event.getEntity();
37+
final Entity projectile = event.getProjectile();
38+
final ItemStack bow = event.getBow();
39+
40+
// Currently there are no advantages of caching projectiles for non-player arrows.
41+
// It would be needed to cache these if we needed a task to take damage from mobs.
42+
if (shooter instanceof Player) {
43+
this.backingMap.put(projectile, bow);
44+
}
45+
}
46+
47+
public @Nullable ItemStack getItem(final Entity projectile) {
48+
return this.backingMap.get(projectile);
49+
}
50+
}

0 commit comments

Comments
 (0)