You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Minecraft插件中PlayerInteractEvent触发两次致物品重复使用问题

问题:Minecraft 1.16.5插件中FlashDust物品使用时事件重复触发导致物品重复消耗

背景

我正在开发Minecraft 1.16.5版本的插件,添加了特殊物品FlashDust,使用后会对范围内玩家施加效果。代码中注册了PlayerInteractEvent事件,尝试通过检查event.getHand()来限制物品的使用手部。

问题描述

玩家使用该物品时,PlayerInteractEvent事件会连续触发两次,导致物品被重复消耗,尽管已通过EquipmentSlot.HAND进行手部过滤仍无法解决。我怀疑事件同时触发主副手,但手部检查无效。

代码示例

FlashDust类代码

package so.max1soft.customitems.dusts;

import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitRunnable;
import org.bukkit.util.Vector;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.Particle;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class FlashDust implements Listener {

    private final JavaPlugin plugin;
    private final Map<Player, Long> lastUse = new HashMap<>();

    public FlashDust(JavaPlugin plugin) {
        this.plugin = plugin;
    }

    public static void register(JavaPlugin plugin) {
        plugin.getServer().getPluginManager().registerEvents(new FlashDust(plugin), plugin);
    }

    public static ItemStack createItem(JavaPlugin plugin) {
        FileConfiguration config = plugin.getConfig();
        ItemStack item = new ItemStack(Material.SUGAR);
        ItemMeta meta = item.getItemMeta();

        if (meta == null) {
            return item;
        }

        String name = config.getString("dusts.flash.name");
        List<String> lore = config.getStringList("dusts.flash.lore");
        meta.setDisplayName(name);
        meta.setLore(lore);

        List<String> enchantments = config.getStringList("dusts.flash.enchantments");

        for (String enchantString : enchantments) {
            String[] parts = enchantString.split(":");
            if (parts.length != 2) {
                plugin.getLogger().warning("Invalid enchantment format: " + enchantString);
                continue;
            }

            String enchantmentName = parts[0].toUpperCase();
            Enchantment enchantment = Enchantment.getByKey(NamespacedKey.minecraft(enchantmentName.toLowerCase()));

            if (enchantment == null) {
                plugin.getLogger().warning("Unknown enchantment: " + enchantmentName);
                continue;
            }

            try {
                int level = Integer.parseInt(parts[1]);
                meta.addEnchant(enchantment, level, true);
            } catch (NumberFormatException e) {
                plugin.getLogger().warning("Invalid enchantment level: " + parts[1] + " for enchantment " + enchantmentName);
            }
        }

        meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);

        item.setItemMeta(meta);
        return item;
    }

    @EventHandler
    public void onPlayerUseFlasDust(PlayerInteractEvent event) {


            if (event.getHand() == EquipmentSlot.HAND) {
            if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
                return;
            }


            Player player = event.getPlayer();
            ItemStack item = event.getItem();


            if (item == null || item.getType() != Material.SUGAR) {
                return;
            }

            ItemMeta meta = item.getItemMeta();
            if (meta == null || !meta.hasDisplayName() || !meta.getDisplayName().equals(plugin.getConfig().getString("dusts.flash.name"))) {
                return;
            }


            if (lastUse.containsKey(player) && lastUse.get(player) + 1000L > System.currentTimeMillis()) {
                return;
            }


            if (!isCooldownActive(player)) {
                player.sendMessage("You are on cooldown!");
                return;
            }


            setCooldown(player);


            event.setCancelled(true);


            processFlasDustUsage(player, item);
        }


    }



    private boolean isCooldownActive(Player player) {
        Long lastInteract = lastUse.get(player);
        if (lastInteract == null) {
            return true;
        }
        return System.currentTimeMillis() - lastInteract >= 20000L;
    }


    private void setCooldown(Player player) {
        lastUse.put(player, System.currentTimeMillis());
    }


    private void processFlasDustUsage(Player player, ItemStack item) {

        FileConfiguration config = plugin.getConfig();
        String itemName = config.getString("dusts.flash.name");
        List<String> expectedLore = config.getStringList("dusts.flash.lore");

        ItemMeta meta = item.getItemMeta();
        if (!meta.getDisplayName().equals(itemName) || !meta.hasLore() || !meta.getLore().equals(expectedLore)) {
            plugin.getLogger().info("Item does not match expected name or lore, cancelling event.");
            return;
        }


        if (item.getAmount() > 1) {
            item.setAmount(item.getAmount() - 1);
        } else {
            player.getInventory().remove(item);
        }


        applyEffectsAndParticles(player);
    }

    private void applyEffectsAndParticles(Player player) {
        Vector initialLocation = player.getLocation().toVector();
        double fixedY = player.getLocation().getY();

        new BukkitRunnable() {
            double radius = 0;
            final double maxRadius = 7;
            final double step = 0.5;

            @Override
            public void run() {
                radius += step;

                if (radius > maxRadius) {
                    cancel();
                    return;
                }

                for (double angle = 0; angle < 360; angle += 10) {
                    double radians = Math.toRadians(angle);
                    double x = radius * Math.cos(radians);
                    double z = radius * Math.sin(radians);

                    Vector particleLocation = initialLocation.clone().add(new Vector(x, 0, z));
                    player.getWorld().spawnParticle(Particle.CLOUD, particleLocation.getX(), fixedY + 0.5, particleLocation.getZ(), 1, 0, 0, 0, 0);
                }

                for (Player nearbyPlayer : player.getWorld().getPlayers()) {
                    if (nearbyPlayer.equals(player)) continue;
                    if (nearbyPlayer.getLocation().distance(initialLocation.toLocation(player.getWorld())) <= radius) {
                        if (!nearbyPlayer.hasPotionEffect(PotionEffectType.BLINDNESS)) {
                            nearbyPlayer.addPotionEffect(new PotionEffect(PotionEffectType.BLINDNESS, 7 * 20, 0, false, false));
                            nearbyPlayer.addPotionEffect(new PotionEffect(PotionEffectType.POISON, 3 * 20, 0, false, false));
                            nearbyPlayer.addPotionEffect(new PotionEffect(PotionEffectType.SLOW, 7 * 20, 0, false, false));
                            nearbyPlayer.addPotionEffect(new PotionEffect(PotionEffectType.GLOWING, 7 * 20, 0, false, false));
                        }
                    }
                }
            }
        }.runTaskTimer(plugin, 0, 2);
    }
}

Main类代码

public class Main extends JavaPlugin {

    @Override
    public void onEnable() {
        saveDefaultConfig();
        FileConfiguration config = getConfig();

        // Отладочная информация
        getLogger().info("Спавн Брейкер шанс: " + config.getDouble("spawnbreak.chance"));
        FlashDust.register(this);
        ZenithDust.register(this);
        RadiusPickaxe.register(this);
        AutoSmeltingPickaxe.register(this);
        MagnetPickaxe.register(this);
        CombinedPickaxe.register(this);
        AutoSmeltingShovel.register(this);
        SpawnerBreaker.register(this);
        getCommand("socustomitems").setExecutor(this);
        getCommand("socustomitems").setTabCompleter(new CustomTabCompleter());
    }



    @Override
    public void onDisable() {

    }

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (command.getName().equalsIgnoreCase("socustomitems") && args.length == 3 && args[0].equalsIgnoreCase("give")) {
            if  (sender.hasPermission("socustomitems.give")) {
                Player target = getServer().getPlayer(args[1]);
                if (target != null) {
                    String pickaxeType = args[2];
                    if (pickaxeType.equalsIgnoreCase("radius")) {
                        target.getInventory().addItem(RadiusPickaxe.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("flame")) {
                        target.getInventory().addItem(AutoSmeltingPickaxe.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("magnet")) {
                        target.getInventory().addItem(MagnetPickaxe.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("combo")) {
                        target.getInventory().addItem(CombinedPickaxe.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("flameshovel")) {
                        target.getInventory().addItem(AutoSmeltingShovel.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("spawnbreaker")) {
                        target.getInventory().addItem(SpawnerBreaker.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("flashdust")) {
                        target.getInventory().addItem(FlashDust.createItem(this));
                    } else if (pickaxeType.equalsIgnoreCase("zenithdust")) {
                        target.getInventory().addItem(ZenithDust.createItem(this));
                    } else {
                        sender.sendMessage("Типы: radius, flame, magnet, combo, flameshovel, spawnbreaker, flashdust, zenithdust");
                    }
                    return true;
                } else {
                    sender.sendMessage("Такого кубхеда не существует!");
                }
            }
        }
        return false;
    }
}

解决方案

问题根源

在Minecraft 1.16.5的Spigot/Bukkit API中,PlayerInteractEvent会分别为主手(EquipmentSlot.HAND)和副手(EquipmentSlot.OFF_HAND)各触发一次事件。你的代码中手部判断的逻辑缩进存在隐患,且冷却判断逻辑混乱,导致两次事件都能通过验证,进而重复消耗物品。

修复步骤

  1. 统一事件过滤逻辑
    合并手部和动作判断,确保只处理主手的右键点击事件,避免无效事件进入后续流程。

  2. 简化冷却判断逻辑
    原代码中存在双重冷却检查(1秒和20秒),逻辑矛盾,合并为统一的20秒冷却判断,确保单次使用后立即拦截后续重复事件。

  3. 明确阻止物品重复使用
    处理完成后调用event.setUseItemInHand(Event.Result.DENY),彻底阻止事件的后续处理。

修复后的onPlayerUseFlasDust方法

@EventHandler
public void onPlayerUseFlasDust(PlayerInteractEvent event) {
    // 仅处理主手右键点击事件
    if (event.getHand() != EquipmentSlot.HAND 
        || (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK)) {
        return;
    }

    Player player = event.getPlayer();
    ItemStack item = event.getItem();

    // 验证物品是否为FlashDust
    if (item == null || item.getType() != Material.SUGAR) {
        return;
    }
    ItemMeta meta = item.getItemMeta();
    String expectedName = plugin.getConfig().getString("dusts.flash.name");
    if (meta == null || !meta.hasDisplayName() || !meta.getDisplayName().equals(expectedName)) {
        return;
    }

    // 检查冷却
    long currentTime = System.currentTimeMillis();
    if (lastUse.containsKey(player)) {
        long timeSinceLastUse = currentTime - lastUse.get(player);
        if (timeSinceLastUse < 20000L) {
            player.sendMessage("You are on cooldown!");
            event.setCancelled(true);
            return;
        }
    }

    // 设置冷却,拦截后续重复触发
    lastUse.put(player, currentTime);

    // 取消事件默认行为,阻止重复处理
    event.setCancelled(true);
    event.setUseItemInHand(org.bukkit.event.Event.Result.DENY);

    // 处理物品消耗和效果
    processFlasDustUsage(player, item);
}

额外优化建议

  • 移除processFlasDustUsage中重复的物品名称和lore检查,前面已完成验证,无需重复判断。
  • 优化物品消耗逻辑,直接操作对应手部的物品,避免remove(item)的不确定性:
    item.setAmount(item.getAmount() - 1);
    if (item.getAmount() <= 0) {
        player.getInventory().setItem(event.getHand(), null);
    }
    

内容的提问来源于stack exchange,提问作者So Max1soft

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 07:37:02