Разработка Java-серверов на Paper · 6 мин чтения

GUI плагина без утечки предметов и повторной покупки

Сделаем закрытое меню из девяти слотов с одной кнопкой. Полный пример узнаёт свой InventoryHolder, отменяет переносы и закрывает окно на следующем тике; затем проверьте shift-click, drag и цифровые клавиши.

Редакция LonelyCraft · Обновлено

Схема: Идентификация → Ввод → Операция
Владелец GUI, полный набор способов ввода и независимая проверка покупки. Авторская схема LonelyCraft.

Подготовьте проект и объявите команду

Это отдельный пример меню для Paper 1.21.1 и JDK 21. В проекте TrailNotes замените файл src/main/java/ru/example/trailnotes/TrailNotesPlugin.java целиком. Класс создаёт собственный InventoryHolder, чтобы отличать это окно от сундука и меню другого плагина.

В manifest добавьте commands: → safemenu: → description: Открыть защищённое меню. Класс ниже откроет окно только игроку; консоль получает объяснение. Для определения владельца используем подход из Custom InventoryHolders, а не заголовок окна.

Сохраните полный пример меню

Замените TrailNotesPlugin.java целиком. Меню является информационным: пока оно открыто, мы отменяем все клики и drag, включая нижний инвентарь. Это простое проверяемое правило, а не пример магазина. Единственная кнопка принимает только обычный левый клик в raw slot 4.

package ru.example.trailnotes;

import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.ClickType;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;

public final class TrailNotesPlugin extends JavaPlugin implements Listener {
    private static final class Menu implements InventoryHolder {
        private final Inventory inventory = Bukkit.createInventory(this, 9, "Безопасное меню");
        @Override public Inventory getInventory() { return inventory; }
    }
    @Override public void onEnable() {
        getServer().getPluginManager().registerEvents(this, this);
    }
    @Override public boolean onCommand(CommandSender sender, Command command,
                                        String label, String[] args) {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("Меню открывается игроку.");
            return true;
        }
        Menu menu = new Menu();
        menu.inventory.setItem(4, new ItemStack(Material.PAPER));
        player.openInventory(menu.inventory);
        return true;
    }
    @EventHandler public void click(InventoryClickEvent event) {
        Inventory top = event.getView().getTopInventory();
        if (!(top.getHolder() instanceof Menu)) return;
        event.setCancelled(true);
        if (event.getRawSlot() != 4 || event.getClick() != ClickType.LEFT) return;
        Player player = (Player) event.getWhoClicked();
        getServer().getScheduler().runTask(this, () -> {
            if (!player.isOnline() || player.getOpenInventory().getTopInventory() != top) return;
            player.closeInventory();
            player.sendMessage("Кнопка сработала; предмет остался в меню.");
        });
    }
    @EventHandler public void drag(InventoryDragEvent event) {
        if (event.getView().getTopInventory().getHolder() instanceof Menu) {
            event.setCancelled(true);
        }
    }
}

Проверьте каждое движение предметов

Соберите плагин и установите на остановленный тестовый сервер. Возьмите обычный предмет в горячую панель, выполните /safemenu и проверьте кнопки ниже. Отложенное закрытие соответствует ограничениям InventoryClickEvent.

  1. Левый клик по бумаге закрывает окно и отправляет сообщение. После повторного открытия бумага снова в центральном слоте, в личном инвентаре её не прибавилось.
  2. Shift-click по предмету в нижнем инвентаре не переносит его в меню. Цифровая клавиша над бумажной кнопкой не меняет предметы местами.
  3. Возьмите предмет на курсор до открытия и попробуйте перетащить по верхним слотам: drag отменён. Двойной клик и выбрасывание также не должны забирать бумагу.
  4. Закройте окно через Escape и быстро откройте другое меню: запланированная задача сверяет объект top и не закрывает новый экран.

Перенесите правило в магазин осознанно

Если нужно позволить игроку двигать вещи в нижнем инвентаре, разрешайте только конкретные операции после отдельного анализа shift-click и drag. Не убирайте общую отмену по одному удачному тесту мышкой. У Bedrock через Geyser повторите проверку реальным жестом на телефоне.

Для покупки кнопка лишь создаёт намерение. Право, актуальная цена, баланс, место для товара и повтор операции проверяются сервисом. Ожидание следующего тика защищает переход окна, но не обеспечивает однократное списание денег после рестарта.

У меню должны быть владелец и номер открытия

Используйте собственный InventoryHolder или другой устойчивый контракт принадлежности интерфейса. Заголовок подходит для человека, но плохо идентифицирует программный объект: его может повторить другой плагин. Храните в сессии UUID игрока, тип экрана и поколение открытия. Кнопки связывайте с допустимыми слотами и действиями, а не только с именем предмета.

Повторное открытие того же экрана создаёт новую сессию. Старый ответ базы или запланированный callback должен проверить поколение перед перерисовкой. Иначе игрок откроет магазин, перейдёт к другому товару, а запоздалый ответ вернёт цену первого. Такая ошибка влияет не только на внешний вид, но и на выбор подтверждаемой операции.

Применение ответа требует актуального игрового контекста и номера открытия. Возврат фонового результата.

Отменяйте перемещение до обработки разрешённой кнопки

InventoryClickEvent содержит raw slot и сведения о типе действия. Верхнее меню и нижний инвентарь игрока различаются; shift-click из нижней части может пытаться перенести предмет наверх. Нельзя защищать только щелчок по декоративной иконке. Для собственного закрытого GUI сначала отменяйте запрещённые действия, затем отдельно разрешайте осмысленный клик.

InventoryDragEvent проверяйте по всем затронутым raw slots. Перетаскивание через верхнее меню может не пройти ваш обработчик обычного клика. Дополнительно тестируйте цифровую клавишу, двойной клик, обмен второй рукой и выбрасывание. Наличие стеклянных заполнителей не блокирует все варианты перемещения; оно лишь оформляет интерфейс.

Открытие и закрытие из события

Документация InventoryClickEvent ограничивает некоторые изменения инвентаря внутри его обработки. Для открытия следующего окна или закрытия текущего используйте документированный безопасный переход, например задачу на следующий тик, когда это требуется API. В задаче снова проверьте, что исходная сессия ещё актуальна. Отложенный вызов не должен открыть окно поверх уже нового меню.

Переход кнопки назад должен учитывать источник, а не выполнять случайную общую команду. Карта экранов.

Сервис покупки повторно проверяет условия

GUI передаёт товар и идентификатор намерения. Сервис сверяет актуальную цену, право, баланс и возможность доставки. Отображение предмета за пять минут до клика не является гарантией той же цены сейчас. Если цена изменилась, покажите новую сумму и запросите осмысленное подтверждение по выбранным правилам.

Блокировка быстрого второго клика в памяти помогает интерфейсу, но не заменяет устойчивую идемпотентность. Два события, повторный HTTP-запрос и рестарт могут пересечься с одной операцией. Хранилище должно определить, что уже выполнено. У внешней экономики учитывайте неоднозначный ответ после списания и не повторяйте его слепо.

  1. Откройте меню с обычным аккаунтом.
  2. Нажмите допустимую кнопку один раз и быстро дважды.
  3. Проверьте shift-click снизу и перенос через несколько слотов.
  4. Используйте цифровую клавишу и обмен второй рукой.
  5. Закройте меню в момент фоновой операции.
  6. Переподключитесь и сравните баланс, товар и журнал.

Доставка при полном инвентаре

До списания определите, как попадёт товар к игроку. Если свободного места нет, можно отказать без изменения баланса либо использовать документированный устойчивый резерв. Случайное выпадение на пол не всегда допустимо: другой игрок может подобрать вещь, а регион запретить дроп. Выберите правило заранее и покажите его в интерфейсе.

После выдачи сохраните подтверждённый результат операции. Если игрок отключился между оплатой и сообщением, покупка всё равно должна иметь понятное состояние. На следующем входе интерфейс читает фактический результат, а не пытается восстановить его по отсутствующему тексту чата. Это особенно важно для рангов и одноразовых ключей.

Проверка с разными клиентами

Проведите сценарии обычным Java и реальным Bedrock, если сервер поддерживает Geyser. Тип отображения и доступные жесты могут различаться, однако серверная политика защиты должна остаться согласованной. Не предполагают одинаковую последовательность событий только по похожему виду меню; сравнивайте результат операции и невозможность забрать кнопку.

Добавьте диагностическую запись с типом экрана, поколением, слотом, действием и итогом. Ограничьте подробный режим тестовым аккаунтом. После проверки уберите шум, оставив идентификаторы важных операций и ошибки. Надёжный GUI понятен игроку и не зависит от идеального медленного клика мышью.

Частые вопросы

Можно ли узнавать меню по заголовку?

Это ненадёжно: другой плагин или перевод могут повторить строку. Используйте владельца интерфейса и устойчивый идентификатор сессии.

Нужно ли отдельно обрабатывать drag?

Да. Перетаскивание имеет другой event и набор затронутых слотов. Проверка только обычного клика не закрывает все пути переноса.

Зачем поколение открытия?

Оно отделяет старый callback от нового экрана. UUID игрока одинаков, но ответ прежней операции уже может быть неактуален.

Что делать, если товар не помещается?

Определите правило до списания: отказ без покупки либо предусмотренная устойчиво сохраняемая доставка. Не полагайтесь на случайный дроп на пол.

Просмотры: … · Один просмотр с браузера за день

Было полезно?Общий счёт: …

Одна реакция с браузера. Нажмите повторно, чтобы снять её, или выберите другую.

Источники и документация

Minecraftразработка плагиновPaper

Читайте дальше