diff options
| author | Krakenied <krakenied1@gmail.com> | 2025-12-15 04:10:16 +0100 |
|---|---|---|
| committer | Krakenied <46192742+Krakenied@users.noreply.github.com> | 2026-01-15 06:39:08 +0100 |
| commit | 03cbd00fe232e4dbe27cde854bba0af376802bb2 (patch) | |
| tree | b79629460b0d0b17fa4321c5fd0327b073c45be9 /bukkit | |
| parent | 3f3e927907e834c6e1b9370be48b568d5b5ff770 (diff) | |
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.
Diffstat (limited to 'bukkit')
8 files changed, 129 insertions, 8 deletions
diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java index 00920e77..586ba55d 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/BukkitQuestsPlugin.java @@ -147,6 +147,7 @@ import com.leonardobishop.quests.bukkit.tasktype.type.dependent.uSkyBlockLevelTa import com.leonardobishop.quests.bukkit.util.CompatUtils; import com.leonardobishop.quests.bukkit.util.FormatUtils; import com.leonardobishop.quests.bukkit.util.LogHistory; +import com.leonardobishop.quests.bukkit.util.Projectile2ItemCache; import com.leonardobishop.quests.common.config.ConfigProblem; import com.leonardobishop.quests.common.config.ConfigProblemDescriptions; import com.leonardobishop.quests.common.config.QuestsConfig; @@ -226,6 +227,7 @@ public class BukkitQuestsPlugin extends JavaPlugin implements Quests { private QuestsBossBar bossBarHandle; private QuestsActionBar actionBarHandle; private VersionSpecificHandler versionSpecificHandler; + private Projectile2ItemCache projectile2ItemCache; private LogHistory logHistory; private WrappedTask questAutoSaveTask; @@ -370,6 +372,10 @@ public class BukkitQuestsPlugin extends JavaPlugin implements Quests { }; } + // Instantiate Projectile to ItemStack cache + this.projectile2ItemCache = new Projectile2ItemCache(); + this.projectile2ItemCache.registerEvents(this); + // Set item getter to be used by Quests config this.questsConfig.setItemGetter(this.itemGetter); @@ -972,6 +978,10 @@ public class BukkitQuestsPlugin extends JavaPlugin implements Quests { return versionSpecificHandler; } + public Projectile2ItemCache getProjectile2ItemCache() { + return projectile2ItemCache; + } + public QuestItemRegistry getQuestItemRegistry() { return questItemRegistry; } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler.java index 08db3e50..dacca889 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler.java @@ -245,10 +245,14 @@ public interface VersionSpecificHandler { /** * {@link DamageSource}s were introduced in {@code 1.20.4}. */ - @SuppressWarnings("UnstableApiUsage") @Nullable Player getDamager(@Nullable EntityDamageEvent lastDamageCause); /** + * {@link DamageSource}s were introduced in {@code 1.20.4}. + */ + @Nullable Entity getDirectSource(@Nullable EntityDamageEvent lastDamageCause); + + /** * {@link Tag#CANDLE_CAKES} was introduced in {@code 1.17}. */ boolean isCake(Material type); diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler20.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler20.java index 5e7065dd..687da6fa 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler20.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler20.java @@ -54,7 +54,6 @@ public class VersionSpecificHandler20 extends VersionSpecificHandler17 implement } } - @SuppressWarnings("UnstableApiUsage") @Override public @Nullable Player getDamager(@Nullable EntityDamageEvent event) { if (!DAMAGE_SOURCE_API) { @@ -74,4 +73,18 @@ public class VersionSpecificHandler20 extends VersionSpecificHandler17 implement return null; } + + @Override + public @Nullable Entity getDirectSource(@Nullable EntityDamageEvent event) { + if (!DAMAGE_SOURCE_API) { + return super.getDamager(event); + } + + if (event == null) { + return null; + } + + DamageSource source = event.getDamageSource(); + return source.getDirectEntity(); + } } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler8.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler8.java index 7e3e2ee4..55ab69ba 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler8.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/hook/versionspecific/VersionSpecificHandler8.java @@ -227,6 +227,11 @@ public class VersionSpecificHandler8 implements VersionSpecificHandler { } @Override + public @Nullable Entity getDirectSource(@Nullable EntityDamageEvent lastDamageCause) { + return null; + } + + @Override public boolean isCake(Material type) { return type == Material.CAKE; } diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/DealDamageTaskType.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/DealDamageTaskType.java index 469f6c50..be15175d 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/DealDamageTaskType.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/DealDamageTaskType.java @@ -45,9 +45,7 @@ public final class DealDamageTaskType extends BukkitTaskType { @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) public void onEntityDamage(EntityDamageEvent event) { - Entity entity = event.getEntity(); Player player = plugin.getVersionSpecificHandler().getDamager(event); - if (player == null || player.hasMetadata("NPC")) { return; } @@ -57,10 +55,15 @@ public final class DealDamageTaskType extends BukkitTaskType { return; } + Entity entity = event.getEntity(); if (!(entity instanceof Damageable damageable)) { return; } + Entity directSource = plugin.getVersionSpecificHandler().getDirectSource(event); + ItemStack bowItem = directSource != null ? plugin.getProjectile2ItemCache().getItem(directSource) : null; + ItemStack item = bowItem != null ? bowItem : plugin.getVersionSpecificHandler().getItemInMainHand(player); + // Clamp entity damage as getDamage() returns Float.MAX_VALUE for killing a parrot with a cookie // https://github.com/LMBishop/Quests/issues/753 double finalDamage = event.getFinalDamage(); @@ -87,7 +90,6 @@ public final class DealDamageTaskType extends BukkitTaskType { } if (task.hasConfigKey("item")) { - ItemStack item = plugin.getVersionSpecificHandler().getItemInMainHand(player); if (item == null) { super.debug("Specific item is required, player has no item in hand; continuing...", quest.getId(), task.getId(), player.getUniqueId()); continue; diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/MobkillingTaskType.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/MobkillingTaskType.java index e38cc01f..6b843d89 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/MobkillingTaskType.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/MobkillingTaskType.java @@ -106,6 +106,11 @@ public final class MobkillingTaskType extends BukkitTaskType { return; } + EntityDamageEvent lastDamageCause = entity.getLastDamageCause(); + Entity directSource = plugin.getVersionSpecificHandler().getDirectSource(lastDamageCause); + ItemStack bowItem = directSource != null ? plugin.getProjectile2ItemCache().getItem(directSource) : null; + ItemStack item = bowItem != null ? bowItem : plugin.getVersionSpecificHandler().getItemInMainHand(player); + //noinspection deprecation String customName = entity.getCustomName(); @@ -144,7 +149,6 @@ public final class MobkillingTaskType extends BukkitTaskType { } if (task.hasConfigKey("item")) { - ItemStack item = plugin.getVersionSpecificHandler().getItemInMainHand(player); if (item == null) { super.debug("Specific item is required, player has no item in hand; continuing...", quest.getId(), task.getId(), player.getUniqueId()); continue; diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/dependent/MythicMobsDealDamageTaskType.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/dependent/MythicMobsDealDamageTaskType.java index cdd4578d..f322f856 100644 --- a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/dependent/MythicMobsDealDamageTaskType.java +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/tasktype/type/dependent/MythicMobsDealDamageTaskType.java @@ -100,7 +100,6 @@ public final class MythicMobsDealDamageTaskType extends BukkitTaskType { } private void handle(final EntityDamageEvent event, final String mobName, final double level) { - Entity entity = event.getEntity(); Player player = plugin.getVersionSpecificHandler().getDamager(event); if (player == null || player.hasMetadata("NPC")) { @@ -112,10 +111,15 @@ public final class MythicMobsDealDamageTaskType extends BukkitTaskType { return; } + Entity entity = event.getEntity(); if (!(entity instanceof Damageable damageable)) { return; } + Entity directSource = plugin.getVersionSpecificHandler().getDirectSource(event); + ItemStack bowItem = directSource != null ? plugin.getProjectile2ItemCache().getItem(directSource) : null; + ItemStack item = bowItem != null ? bowItem : plugin.getVersionSpecificHandler().getItemInMainHand(player); + // Clamp entity damage as getDamage() returns Float.MAX_VALUE for killing a parrot with a cookie // https://github.com/LMBishop/Quests/issues/753 double finalDamage = event.getFinalDamage(); @@ -147,7 +151,6 @@ public final class MythicMobsDealDamageTaskType extends BukkitTaskType { } if (task.hasConfigKey("item")) { - ItemStack item = plugin.getVersionSpecificHandler().getItemInMainHand(player); if (item == null) { super.debug("Specific item is required, player has no item in hand; continuing...", quest.getId(), task.getId(), player.getUniqueId()); continue; diff --git a/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/Projectile2ItemCache.java b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/Projectile2ItemCache.java new file mode 100644 index 00000000..1b6b91e0 --- /dev/null +++ b/bukkit/src/main/java/com/leonardobishop/quests/bukkit/util/Projectile2ItemCache.java @@ -0,0 +1,80 @@ +package com.leonardobishop.quests.bukkit.util; + +import com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent; +import com.leonardobishop.quests.bukkit.BukkitQuestsPlugin; +import org.bukkit.entity.Entity; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.entity.Projectile; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityShootBowEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.PluginManager; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Map; +import java.util.WeakHashMap; + +/** + * Provides a cache that links projectiles to the item used to fire them. + * + * <p>This cache exists because damage-related events do not expose + * information about the item from which a projectile was fired. + * By capturing this association at the time the projectile is created, + * the item can later be retrieved when handling damage events.</p> + */ +@NullMarked +public final class Projectile2ItemCache implements Listener { + + private final Map<Entity, @Nullable ItemStack> backingMap; + + public Projectile2ItemCache() { + this.backingMap = WeakHashMap.newWeakHashMap(1024); + } + + public void registerEvents(final BukkitQuestsPlugin plugin) { + final PluginManager pluginManager = plugin.getServer().getPluginManager(); + + pluginManager.registerEvents(this, plugin); + + try { + Class.forName("com.destroystokyo.paper.event.player.PlayerLaunchProjectileEvent"); + pluginManager.registerEvents(new PlayerLaunchProjectileListener(), plugin); + } catch (final ClassNotFoundException e) { + // not supported on Spigot + } + } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onEntityShootBow(final EntityShootBowEvent event) { + final LivingEntity shooter = event.getEntity(); + final Entity projectile = event.getProjectile(); + final ItemStack bow = event.getBow(); + + // Currently there are no advantages of caching projectiles for non-player arrows. + // It would be needed to cache these if we needed a task to take damage from mobs. + if (shooter instanceof Player) { + this.backingMap.put(projectile, bow); + } + } + + public final class PlayerLaunchProjectileListener implements Listener { + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onPlayerLaunchProjectile(final PlayerLaunchProjectileEvent event) { + final Projectile projectile = event.getProjectile(); + final ItemStack item = event.getItemStack(); + + // TODO: doesn't really work for tridents + // https://github.com/LMBishop/Quests/pull/833 + Projectile2ItemCache.this.backingMap.put(projectile, item); + } + } + + public @Nullable ItemStack getItem(final Entity projectile) { + return this.backingMap.get(projectile); + } +} |
