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

GUI内物品数量叠加Bug排查求助

问题描述

我开发了基于Bukkit的Minecraft插件购买GUI,中间物品由其他类传入,玩家可通过按钮调整购买数量,点击钻石按钮确认时,若拥有足够doubloons即可获取物品。但出现异常:首次购买1个物品正常,再次打开GUI购买1个时,玩家背包物品总数变为3个,后续每次购买都会累加上次的购买数量,直到服务器重启才重置。我尝试多种方式重置amount变量但无效,相关代码如下:

package pro.jabo.jabo.gui;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import pro.jabo.jabo.utils.PlayerConfigUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

public class PurchaseItemGUI implements Listener {

    private final Inventory inventory;
    private int amount = 1;
    private final PlayerConfigUtils playerConfigUtils;
    private final UUID shopOwnerUUID;
    private final double pricePerItem;
    private final String shopName;
    private boolean isConfirming = false; 

    public PurchaseItemGUI(JavaPlugin plugin, ItemStack item, Player player, UUID shopOwnerUUID, double pricePerItem, String shopName) {
        this.playerConfigUtils = new PlayerConfigUtils(plugin);
        this.shopOwnerUUID = shopOwnerUUID;
        this.pricePerItem = pricePerItem;
        this.shopName = shopName;

        this.inventory = Bukkit.createInventory(null, 27, ChatColor.GOLD + "Purchase " + item.getType());

        Bukkit.getPluginManager().registerEvents(this, plugin);

        ItemStack itemToPurchase = new ItemStack(item);
        itemToPurchase.setAmount(amount);
        ItemMeta meta = itemToPurchase.getItemMeta();
        meta.setDisplayName(ChatColor.GREEN + item.getType().name());
        List<String> lore = new ArrayList<>();
        lore.add(ChatColor.WHITE + "Amount: " + amount);
        lore.add(ChatColor.WHITE + "Total Price: " + pricePerItem * amount);
        meta.setLore(lore);
        itemToPurchase.setItemMeta(meta);

        inventory.setItem(13, itemToPurchase);


        setControlButtons();
    }

    private void setControlButtons() {
        inventory.setItem(10, createButton(Material.REDSTONE, ChatColor.RED + "-10"));
        inventory.setItem(11, createButton(Material.REDSTONE, ChatColor.RED + "-1"));
        inventory.setItem(15, createButton(Material.EMERALD, ChatColor.GREEN + "+1"));
        inventory.setItem(16, createButton(Material.EMERALD, ChatColor.GREEN + "+10"));
        inventory.setItem(22, createButton(Material.DIAMOND, ChatColor.BLUE + "CONFIRM"));
    }

    private ItemStack createButton(Material material, String name) {
        ItemStack item = new ItemStack(material);
        ItemMeta meta = item.getItemMeta();
        meta.setDisplayName(name);
        item.setItemMeta(meta);
        return item;
    }

    public Inventory getInventory() {
        return inventory;
    }

    public String getShopName() {
        return shopName;
    }

    public UUID getShopOwnerUUID() {
        return shopOwnerUUID;
    }

    @EventHandler
    public void onButtonClick(InventoryClickEvent event) {
        if (!event.getView().getTitle().startsWith(ChatColor.GOLD + "Purchase ")) {
            return;
        }

        event.setCancelled(true);

        Player player = (Player) event.getWhoClicked();
        ItemStack clickedItem = event.getCurrentItem();

        if (clickedItem == null || clickedItem.getType() == Material.AIR) {
            return;
        }

        if (clickedItem.getType() == Material.REDSTONE && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.RED + "-10")) {
            amount = Math.max(1, amount - 10);
        } else if (clickedItem.getType() == Material.REDSTONE && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.RED + "-1")) {
            amount = Math.max(1, amount - 1);
        } else if (clickedItem.getType() == Material.EMERALD && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.GREEN + "+1")) {
            amount++;
        } else if (clickedItem.getType() == Material.EMERALD && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.GREEN + "+10")) {
            amount += 10;
        } else if (clickedItem.getType() == Material.DIAMOND && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.BLUE + "CONFIRM")) {
            if (!isConfirming) {
                isConfirming = true;
                confirmPurchase(player);
                isConfirming = false;
            }
        }

        updateGUI();
    }

    private void updateGUI() {
        ItemStack item = inventory.getItem(13);
        if (item != null) {
            ItemMeta meta = item.getItemMeta();
            if (meta != null) {
                List<String> lore = new ArrayList<>();
                lore.add(ChatColor.WHITE + "Amount: " + amount);
                lore.add(ChatColor.WHITE + "Total Price: " + pricePerItem * amount);
                meta.setLore(lore);
                item.setItemMeta(meta);
                inventory.setItem(13, item);
            }
        }
    }

    private void confirmPurchase(Player player) {
        double totalCost = pricePerItem * amount;
        FileConfiguration playerConfig = playerConfigUtils.getPlayerConfig(player.getUniqueId());
        int playerDoubloons = playerConfig.getInt("doubloons");

        if (playerDoubloons < totalCost) {
            player.sendMessage(ChatColor.RED + "You do not have enough doubloons.");
            player.closeInventory();
            return;
        }

        playerConfig.set("doubloons", playerDoubloons - totalCost);
        FileConfiguration shopOwnerConfig = playerConfigUtils.getPlayerConfig(shopOwnerUUID);
        int shopOwnerDoubloons = shopOwnerConfig.getInt("doubloons");
        shopOwnerConfig.set("doubloons", shopOwnerDoubloons + totalCost);

        ItemStack purchasedItem = new ItemStack(inventory.getItem(13));
        purchasedItem.setAmount(amount);
        player.getInventory().addItem(purchasedItem);


        player.sendMessage(ChatColor.GREEN + "Purchase successful!");

        playerConfigUtils.savePlayerConfig(player.getUniqueId(), playerConfig);
        playerConfigUtils.savePlayerConfig(shopOwnerUUID, shopOwnerConfig);
        player.updateInventory();

        amount = 1;
        updateGUI();

        player.closeInventory();
    }
}
问题根源

核心问题是每次创建PurchaseItemGUI实例时,都会注册一个新的事件监听器,且旧的监听器不会被注销。当玩家多次打开GUI后,同一个点击事件会被所有已注册的监听器实例处理:

  • 第一次打开GUI,生成实例A并注册监听器A;
  • 第二次打开GUI,生成实例B并注册监听器B;
  • 点击确认按钮时,监听器A和B都会执行confirmPurchase方法,两个实例的amount都会用来给玩家添加物品,导致数量累加;
  • 服务器重启前,所有旧实例的监听器一直存在,每次购买都会触发所有实例的逻辑,数量越累越多。
修复方案

方案:分离监听器与GUI实例(推荐)

创建一个全局单例监听器处理所有购买GUI的点击事件,通过玩家或Inventory映射到对应的PurchaseItemGUI实例,避免重复注册监听器。

修改后的代码如下:

package pro.jabo.jabo.gui;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import pro.jabo.jabo.utils.PlayerConfigUtils;

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

// 全局单例监听器
public class PurchaseGUIListener implements Listener {
    private static final Map<Player, PurchaseItemGUI> playerGUIMap = new HashMap<>();

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

    public static void addPlayerGUI(Player player, PurchaseItemGUI gui) {
        playerGUIMap.put(player, gui);
    }

    @EventHandler
    public void onButtonClick(InventoryClickEvent event) {
        if (!(event.getWhoClicked() instanceof Player player)) {
            return;
        }

        PurchaseItemGUI gui = playerGUIMap.get(player);
        if (gui == null || !event.getView().getTitle().equals(gui.getInventory().getTitle())) {
            return;
        }

        event.setCancelled(true);
        gui.handleClick(event);
    }

    @EventHandler
    public void onInventoryClose(InventoryCloseEvent event) {
        if (!(event.getPlayer() instanceof Player player)) {
            return;
        }
        playerGUIMap.remove(player);
    }
}

// GUI逻辑类,不再实现Listener
public class PurchaseItemGUI {

    private final Inventory inventory;
    private int amount = 1;
    private final PlayerConfigUtils playerConfigUtils;
    private final UUID shopOwnerUUID;
    private final double pricePerItem;
    private final String shopName;
    private boolean isConfirming = false; 

    public PurchaseItemGUI(JavaPlugin plugin, ItemStack item, Player player, UUID shopOwnerUUID, double pricePerItem, String shopName) {
        this.playerConfigUtils = new PlayerConfigUtils(plugin);
        this.shopOwnerUUID = shopOwnerUUID;
        this.pricePerItem = pricePerItem;
        this.shopName = shopName;

        this.inventory = Bukkit.createInventory(null, 27, ChatColor.GOLD + "Purchase " + item.getType());

        // 将当前GUI实例与玩家绑定
        PurchaseGUIListener.addPlayerGUI(player, this);

        ItemStack itemToPurchase = new ItemStack(item);
        itemToPurchase.setAmount(amount);
        ItemMeta meta = itemToPurchase.getItemMeta();
        meta.setDisplayName(ChatColor.GREEN + item.getType().name());
        List<String> lore = new ArrayList<>();
        lore.add(ChatColor.WHITE + "Amount: " + amount);
        lore.add(ChatColor.WHITE + "Total Price: " + pricePerItem * amount);
        meta.setLore(lore);
        itemToPurchase.setItemMeta(meta);

        inventory.setItem(13, itemToPurchase);
        setControlButtons();
    }

    private void setControlButtons() {
        inventory.setItem(10, createButton(Material.REDSTONE, ChatColor.RED + "-10"));
        inventory.setItem(11, createButton(Material.REDSTONE, ChatColor.RED + "-1"));
        inventory.setItem(15, createButton(Material.EMERALD, ChatColor.GREEN + "+1"));
        inventory.setItem(16, createButton(Material.EMERALD, ChatColor.GREEN + "+10"));
        inventory.setItem(22, createButton(Material.DIAMOND, ChatColor.BLUE + "CONFIRM"));
    }

    private ItemStack createButton(Material material, String name) {
        ItemStack item = new ItemStack(material);
        ItemMeta meta = item.getItemMeta();
        meta.setDisplayName(name);
        item.setItemMeta(meta);
        return item;
    }

    public Inventory getInventory() {
        return inventory;
    }

    public String getShopName() {
        return shopName;
    }

    public UUID getShopOwnerUUID() {
        return shopOwnerUUID;
    }

    // 处理点击事件的方法,由全局监听器调用
    public void handleClick(InventoryClickEvent event) {
        Player player = (Player) event.getWhoClicked();
        ItemStack clickedItem = event.getCurrentItem();

        if (clickedItem == null || clickedItem.getType() == Material.AIR) {
            return;
        }

        if (clickedItem.getType() == Material.REDSTONE && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.RED + "-10")) {
            amount = Math.max(1, amount - 10);
        } else if (clickedItem.getType() == Material.REDSTONE && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.RED + "-1")) {
            amount = Math.max(1, amount - 1);
        } else if (clickedItem.getType() == Material.EMERALD && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.GREEN + "+1")) {
            amount++;
        } else if (clickedItem.getType() == Material.EMERALD && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.GREEN + "+10")) {
            amount += 10;
        } else if (clickedItem.getType() == Material.DIAMOND && clickedItem.getItemMeta().getDisplayName().equals(ChatColor.BLUE + "CONFIRM")) {
            if (!isConfirming) {
                isConfirming = true;
                confirmPurchase(player);
                isConfirming = false;
            }
        }

        updateGUI();
    }

    private void updateGUI() {
        ItemStack item = inventory.getItem(13);
        if (item != null) {
            ItemMeta meta = item.getItemMeta();
            if (meta != null) {
                List<String> lore = new ArrayList<>();
                lore.add(ChatColor.WHITE + "Amount: " + amount);
                lore.add(ChatColor.WHITE + "Total Price: " + pricePerItem * amount);
                meta.setLore(lore);
                item.setItemMeta(meta);
                inventory.setItem(13, item);
            }
        }
    }

    private void confirmPurchase(Player player) {
        double totalCost = pricePerItem * amount;
        FileConfiguration playerConfig = playerConfigUtils.getPlayerConfig(player.getUniqueId());
        int playerDoubloons = playerConfig.getInt("doubloons");

        if (playerDoubloons < totalCost) {
            player.sendMessage(ChatColor.RED + "你没有足够的金币!");
            player.closeInventory();
            return;
        }

        playerConfig.set("doubloons", playerDoubloons - totalCost);
        FileConfiguration shopOwnerConfig = playerConfigUtils.getPlayerConfig(shopOwnerUUID);
        int shopOwnerDoubloons = shopOwnerConfig.getInt("doubloons");
        shopOwnerConfig.set("doubloons", shopOwnerDoubloons + totalCost);

        ItemStack purchasedItem = new ItemStack(inventory.getItem(13));
        purchasedItem.setAmount(amount);
        player.getInventory().addItem(purchasedItem);

        player.sendMessage(ChatColor.GREEN + "购买成功!");

        playerConfigUtils.savePlayerConfig(player.getUniqueId(), playerConfig);
        playerConfigUtils.savePlayerConfig(shopOwnerUUID, shopOwnerConfig);
        player.updateInventory();

        amount = 1;
        updateGUI();

        player.closeInventory();
    }
}

额外注意事项

  1. 在插件主类的onEnable方法中,需要先注册全局监听器:
@Override
public void onEnable() {
    PurchaseGUIListener.register(this);
    // 其他初始化逻辑
}
  1. 这样每次打开GUI时,只会将当前玩家与GUI实例绑定,关闭GUI时自动解除绑定,每个点击事件只会被对应的GUI实例处理,不会出现多实例累加的问题。

内容的提问来源于stack exchange,提问作者The Clashers

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 18:07:03