Форум программистов, компьютерный форум, киберфорум
Javaican
Войти
Регистрация
Восстановить пароль

Разработка плагина для Minecraft

Запись от Javaican размещена 09.06.2025 в 21:49
Показов 3217 Комментарии 0

Нажмите на изображение для увеличения
Название: Разработка плагина для Minecraft.jpg
Просмотров: 121
Размер:	206.1 Кб
ID:	10892
За годы существования Minecraft сформировалась сложная экосистема серверов. Оригинальный (ванильный) сервер не поддерживает плагины, поэтому сообщество разработало множество альтернатив. CraftBukkit был первопроходцем, но из-за правовых проблем уступил место Spigot. Сегодня Paper - один из самых популярных серверов, который не только поддерживает разработку плагинов, но и значительно оптимизирует игровой процесс.

Когда я только начинал писать свой первый плагин, выбор технологий был для меня настоящим испытанием. Сейчас четко вижу, что для новичка оптимальный путь - использовать Paper сервер и Bukkit API. Такой подход обеспечивает совместимость с большинством серверов на базе Bukkit, включая Spigot и Paper.

Интересный вопрос: когда стоит разрабатывать плагин вместо датапака? Датапаки (наборы данных) могут изменять рецепты, добавлять предметы и команды, но им не хватает одной критической возможности - они не могут присваивать новым скрафченым предметам пользовательские NBT-теги. Именно этот недостаток часто вынуждает разработчиков переходить от датапаков к полноценным плагинам.

Анатомия плагина довольно проста: основной Java-класс, расширяющий JavaPlugin, плюс конфигурационные файлы. При этом функциональность может быть практически любой - от телепортации до создания полноценных экономических систем. Ограничением служит только ваша фантазия и знание API.

Настройка среды разработки



Первым делом нужно установить саму IntelliJ IDEA. Доступны две версии: платная Ultimate и бесплатная Community Edition. Для начинающих разработчиков плагинов Community Edition вполне достаточно, хотя если вы студент или автор открытого ПО, можете получить бесплатную лицензию на Ultimate. После установки IDE необходимо добавить плагин Minecraft Development. Это делается через меню File > Settings > Plugins > Marketplace. Плагин критически важен, т.к. он автоматизирует множество рутинных операций - от создания структуры проекта до генерации файла plugin.yml.

Теперь можно создать проект: File > New > Project > Minecraft. Здесь предстоит сделать несколько важных выборов:
Platform Type: Plugin
Platform: Bukkit
Bukkit Platform: Paper (лучший выбор на сегодняшний день)
Build System: Gradle или Maven (я предпочитаю Gradle для сложных проектов)

Важный момент - указание пакета и имени класса. Хорошей практикой является использование обратного доменного имени для пакета, например, com.yourdomain.pluginname. Если у вас нет домена, можно использовать me.yourname или io.github.yourname.

После создания проекта получаем готовую структуру:
src/main/java - папка с исходным кодом,
src/main/resources - ресурсы проекта, включая plugin.yml,
build.gradle или pom.xml - файл системы сборки

Файл plugin.yml особенно важен - он определяет метаданные плагина:

YAML
1
2
3
4
name: YourPlugin
version: '${version}'
main: com.yourdomain.pluginname.MainClass
api-version: '1.20'
Для тестирования плагина потребуется локальный сервер Minecraft. Я устанавливаю его в отдельную папку и использую скрипт для автоматической компиляции плагина и копирования JAR-файла в папку plugins сервера. Такой подход эканомит кучу времени при отладке.

Чтобы настроить тестовый сервер:
1. Скачайте последнюю версию Paper с официального сайта.
2. Создайте папку для сервера и поместите туда JAR-файл.
3. Запустите сервер командой java -jar paper.jar.
4. Примите лицензионное соглашение, отредактировав eula.txt.
5. Перезапустите сервер.

Теперь можно компилировать плагин и тестировать его на живом сервере. IntelliJ предлагает удобную интеграцию с системами сборки - достаточно запустить задачу build, и готовый JAR появится в папке build/libs или target. Особую ценность представляет горячая перезагрузка плагинов. Поместив JAR в папку plugins работающего сервера, можно ввести команду /reload confirm для перезагрузки всех плагинов без перезапуска сервера. Правда, для сложных плагинов иногда требуется полный перезапуск.

Разработка плагина (ClassCastException)
Проблема состоит в следующем. Пытаюсь написать ядро, к которому можно подключать плагины. Первым...

[Asset Store] Разработка плагина re::sprite
Однажды Джонни Айва – главного дизайнера компании Apple – спросили, каково быть дизайнером в лучшей...

Не декомпилируется рабочая область для создания модификаций для minecraft
Хочу создать модификацию для майнкрафта) но не могу разобраться с forge. Следуя руководством от...

Какие есть ресурсы для обучения созданию модов для Minecraft?
Предыстория: Хотел писать моды для Minecraft, знал, что они пишутся на Java. Начал учить, уже...


Основы Bukkit API



Погружение в разработку плагинов для Minecraft начинается с понимания Bukkit API - набора инструментов, который дает доступ к внутренностям сервера. По сути, это мост между вашим кодом и игровым миром. Ядро каждого плагина - класс, который наследуется от JavaPlugin. Вот базовый скелет:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
public final class MyCoolPlugin extends JavaPlugin {
    public Logger logger = getLogger();
    
    @Override
    public void onEnable() {
        logger.info("Плагин запущен!");
    }
    
    @Override
    public void onDisable() {
        logger.info("Плагин остановлен!");
    }
}
Методы onEnable() и onDisable() - точки входа и выхода вашего плагина. Первый вызывается при загрузке или перезагрузке сервера, второй - перед выключением или перезапуском. Именно в onEnable() происходит регистрация всех команд, слушателей событий и загрузка конфигураций.

Система событий - это настоящее сердце Bukkit API. Представьте, что игровой сервер генерирует тысячи событий каждую секунду: игроки кликают по блокам, существа перемещаются, блоки изменяются. Через систему событий ваш плагин может "подслушивать" эти действия и реагировать на них. Чтобы создать слушателя событий, класс должен реализовывать интерфейс Listener:

Java
1
2
3
4
5
6
7
8
public class MyListener implements Listener {
    
    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        Player player = event.getPlayer();
        player.sendMessage("Привет, " + player.getName() + "!");
    }
}
Не забудьте зарегистрировать слушателя в методе onEnable():

Java
1
2
3
4
@Override
public void onEnable() {
    getServer().getPluginManager().registerEvents(new MyListener(), this);
}
Отдельного внимания заслуживает аннотация @EventHandler. Она может принимать параметр priority, определяющий очередность обработки событий:

Java
1
2
3
4
@EventHandler(priority = EventPriority.HIGH)
public void onPlayerInteract(PlayerInteractEvent event) {
    // Код обработки
}
Приоритеты (от низшего к высшему): LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR. Парадоксально, но высокий приоритет означает, что обработчик будет вызван позже других! Это потому, что система задумана как цепочка фильтров: сначала событие проходит через обработчики с низким приоритетом, которые могут его изменить, а затем уже с высоким.

MONITOR - особый случай, он предназначен только для мониторинга событий без их изменения.

Работа с игроками и блоками через API интуитивна. Например, можно телепортировать игрока так:

Java
1
2
3
Player player = event.getPlayer();
Location location = new Location(player.getWorld(), 100, 64, 100);
player.teleport(location);
Или проверить/изменить блок:

Java
1
2
3
4
Block block = event.getClickedBlock();
if (block.getType() == Material.STONE) {
    block.setType(Material.DIAMOND_BLOCK);
}
Для хранения данных Bukkit предлагает несколько механизмов. Самый простой - конфигурационные файлы. Базовая работа с ними:

Java
1
2
3
4
5
6
7
// Сохранение дефолтного конфига
saveDefaultConfig();
// Чтение значения с дефолтом
int cooldown = getConfig().getInt("abilities.fireball.cooldown", 30);
// Запись значения
getConfig().set("players." + player.getUniqueId() + ".money", 1000);
saveConfig();
Для хранения данных внутри игровых объектов используются Persistent Data Containers (PDC). Они предотвращают конфликты между плагинами:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Создаем ключ
NamespacedKey key = new NamespacedKey(this, "custom_value");
// Получаем контейнер из предмета
ItemStack item = player.getInventory().getItemInMainHand();
ItemMeta meta = item.getItemMeta();
PersistentDataContainer container = meta.getPersistentDataContainer();
 
// Записываем данные
container.set(key, PersistentDataType.INTEGER, 42);
item.setItemMeta(meta);
 
// Читаем данные
if (container.has(key, PersistentDataType.INTEGER)) {
    int value = container.get(key, PersistentDataType.INTEGER);
}
Одна из самых полезных возможностей - создание кастомных рецептов крафта. Допустим, мы хотим создать рецепт супер-меча:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Создаем результат крафта
ItemStack superSword = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta meta = superSword.getItemMeta();
meta.setDisplayName("§6Супер-меч");
meta.addEnchant(Enchantment.DAMAGE_ALL, 10, true);
superSword.setItemMeta(meta);
 
// Создаем рецепт
ShapedRecipe recipe = new ShapedRecipe(new NamespacedKey(this, "super_sword"), superSword);
recipe.shape("DDD", " S ", " S ");
recipe.setIngredient('D', Material.DIAMOND);
recipe.setIngredient('S', Material.STICK);
 
// Регистрируем рецепт
getServer().addRecipe(recipe);
Другая важная часть Bukkit API - система команд. Она позволяет игрокам взаимодействовать с вашим плагином через чат. Базовый способ регистрации команды:

Java
1
2
3
4
@Override
public void onEnable() {
    getCommand("heal").setExecutor(new HealCommand());
}
Для этого метода необходимо добавить команду в plugin.yml:

YAML
1
2
3
4
5
commands:
  heal:
    description: Восстанавливает здоровье игрока
    usage: /heal [игрок]
    permission: myplugin.heal
Класс команды должен реализовывать интерфейс CommandExecutor:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class HealCommand implements CommandExecutor {
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!(sender instanceof Player) && args.length == 0) {
            sender.sendMessage("Эту команду может использовать только игрок!");
            return true;
        }
        
        Player target;
        if (args.length == 0) {
            target = (Player) sender;
        } else {
            target = Bukkit.getPlayer(args[0]);
            if (target == null) {
                sender.sendMessage("Игрок " + args[0] + " не найден!");
                return true;
            }
        }
        
        target.setHealth(target.getMaxHealth());
        sender.sendMessage("Здоровье восстановлено!");
        return true;
    }
}
Возврат true означает, что команда обработана успешно, а false приведет к отображению сообщения об использовании.
Для создания команд с автозаполнением можно реализовать интерфейс TabCompleter:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class HealTabCompleter implements TabCompleter {
    @Override
    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
        if (args.length == 1) {
            List<String> playerNames = new ArrayList<>();
            for (Player player : Bukkit.getOnlinePlayers()) {
                playerNames.add(player.getName());
            }
            return playerNames;
        }
        return null;
    }
}
И зарегистрировать его:

Java
1
getCommand("heal").setTabCompleter(new HealTabCompleter());
Система разрешений (permissions) в Bukkit - мощный инструмент для контроля доступа игроков к функциям вашего плагина. Разрешения объявляются в plugin.yml:

YAML
1
2
3
4
5
6
7
8
9
permissions:
  myplugin.heal:
    description: Позволяет лечить себя и других
    default: op
  myplugin.admin:
    description: Доступ к административным командам
    default: false
    children:
      myplugin.heal: true
Проверить наличие разрешения можно так:

Java
1
2
3
4
if (!sender.hasPermission("myplugin.heal")) {
    sender.sendMessage("У вас нет прав для использования этой команды!");
    return true;
}
Работа с инвентарем и предметами - еще одна обширная область Bukkit API. Допустим, мы хотим создать кастомный предмет с уникальными свойствами:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ItemStack magicWand = new ItemStack(Material.BLAZE_ROD);
ItemMeta meta = magicWand.getItemMeta();
meta.setDisplayName("§5Волшебная палочка");
List<String> lore = new ArrayList<>();
lore.add("§7Создает шар огня");
lore.add("§cПерезарядка: 10 секунд");
meta.setLore(lore);
meta.addEnchant(Enchantment.FIRE_ASPECT, 1, true);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
 
// Добавляем метаданные для идентификации
NamespacedKey key = new NamespacedKey(this, "magic_wand");
meta.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte)1);
 
magicWand.setItemMeta(meta);
 
// Дадим этот предмет игроку
player.getInventory().addItem(magicWand);
Для обработки использования такого предмета:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@EventHandler
public void onPlayerInteract(PlayerInteractEvent event) {
    if (!event.getAction().isRightClick()) return;
    
    ItemStack item = event.getItem();
    if (item == null) return;
    
    ItemMeta meta = item.getItemMeta();
    if (meta == null) return;
    
    NamespacedKey key = new NamespacedKey(this, "magic_wand");
    if (meta.getPersistentDataContainer().has(key, PersistentDataType.BYTE)) {
        // Это наша волшебная палочка!
        Player player = event.getPlayer();
        player.launchProjectile(Fireball.class);
        
        // Здесь можно добавить кулдаун...
    }
}
Система кулдаунов (перезарядки) может быть реализована через HashMap:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Map<UUID, Long> cooldowns = new HashMap<>();
private final long COOLDOWN_TIME = 10 * 1000; // 10 секунд в миллисекундах
 
// Проверка кулдауна
UUID playerId = player.getUniqueId();
long currentTime = System.currentTimeMillis();
 
if (cooldowns.containsKey(playerId) && 
    currentTime - cooldowns.get(playerId) < COOLDOWN_TIME) {
    long remainingTime = (cooldowns.get(playerId) + COOLDOWN_TIME - currentTime) / 1000;
    player.sendMessage("§cПерезарядка: " + remainingTime + " сек.");
    return;
}
 
// Действие выполняется, устанавливаем кулдаун
cooldowns.put(playerId, currentTime);
Для визуальных эффектов Bukkit предлагает API частиц и звуков:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Создаем спиральные частицы вокруг игрока
Location loc = player.getLocation();
World world = loc.getWorld();
 
for (double y = 0; y < 2; y += 0.1) {
    double radius = 1.0 - (y / 2);
    for (double angle = 0; angle < Math.PI * 2; angle += Math.PI / 16) {
        double x = Math.cos(angle) * radius;
        double z = Math.sin(angle) * radius;
        Location particleLoc = loc.clone().add(x, y, z);
        world.spawnParticle(Particle.FLAME, particleLoc, 1, 0, 0, 0, 0);
    }
}
 
// Проигрываем звук для всех игроков в радиусе 30 блоков
world.playSound(loc, Sound.ENTITY_ENDER_DRAGON_GROWL, 1.0f, 1.0f);
Отдельного упоминания заслуживает система планирования задач, которая позволяет выполнять код с задержкой или периодически:

Java
1
2
3
4
5
6
7
8
9
10
11
12
// Выполнить задачу через 20 тиков (1 секунда)
Bukkit.getScheduler().runTaskLater(this, () -> {
    player.sendMessage("Прошла 1 секунда!");
}, 20L);
 
// Запустить повторяющуюся задачу
int taskId = Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> {
    player.sendMessage("Эта задача выполняется каждые 5 секунд!");
}, 0L, 100L); // 0L - начальная задержка, 100L - период (5 секунд)
 
// Отменить задачу
Bukkit.getScheduler().cancelTask(taskId);
Важно отметить, что задачи по умолчанию выполняются в основном потоке сервера. Для тяжелых операций следует использовать асинхронное выполнение:

Java
1
2
3
4
5
6
7
8
9
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
    // Тяжелые вычисления или операции ввода-вывода
    // НЕ взаимодействуйте с API Bukkit здесь!
    
    // Вернуться в основной поток для работы с API
    Bukkit.getScheduler().runTask(this, () -> {
        // Безопасно использовать API Bukkit
    });
});
Для работы с базами данных обычно создают пул соединений и выполняют запросы асинхронно:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// В методе onEnable()
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/minecraft");
config.setUsername("user");
config.setPassword("pass");
dataSource = new HikariDataSource(config);
 
// Использование
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement("SELECT * FROM players WHERE uuid = ?")) {
        
        stmt.setString(1, player.getUniqueId().toString());
        ResultSet rs = stmt.executeQuery();
        
        if (rs.next()) {
            int balance = rs.getInt("balance");
            
            // Вернуться в основной поток
            Bukkit.getScheduler().runTask(this, () -> {
                player.sendMessage("Ваш баланс: " + balance);
            });
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
});
Наконец, стоит упомянуть о создании пользовательских интерфейсов через инвентарь. Это популярный способ создания меню в Minecraft:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Создание инвентаря-меню
Inventory menu = Bukkit.createInventory(null, 27, "Магазин предметов");
 
// Добавление элементов
ItemStack swordItem = new ItemStack(Material.DIAMOND_SWORD);
ItemMeta swordMeta = swordItem.getItemMeta();
swordMeta.setDisplayName("§6Купить меч");
swordItem.setItemMeta(swordMeta);
menu.setItem(13, swordItem);
 
// Показать меню игроку
player.openInventory(menu);
 
// Обработка кликов
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
    if (!event.getView().getTitle().equals("Магазин предметов")) return;
    
    event.setCancelled(true); // Отменяем клик для предотвращения взятия предметов
    
    if (event.getCurrentItem() == null) return;
    
    if (event.getCurrentItem().getType() == Material.DIAMOND_SWORD) {
        Player player = (Player) event.getWhoClicked();
        player.sendMessage("Вы купили меч!");
        player.closeInventory();
    }
}

Архитектурные паттерны в плагинах



Когда я только начинал разрабатывать плагины для Minecraft, весь мой код представлял собой один огромный класс с кучей методов. Быстро пришло понимание, что такой подход ведет в тупик - поддерживать и расширять монолитную структуру становится невозможно. Внедрение архитектурных паттернов в разработку плагинов стало для меня настоящим прорывом.

Начнем с паттерна Command, который идеально ложится на архитектуру команд Minecraft. Вместо того чтобы писать всю логику обработки в одном огромном методе onCommand, мы можем создать отдельные классы для каждой команды:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface ICommand {
    boolean execute(CommandSender sender, String[] args);
}
 
public class HealCommand implements ICommand {
    @Override
    public boolean execute(CommandSender sender, String[] args) {
        // Логика восстановления здоровья
        return true;
    }
}
 
public class CommandManager {
    private Map<String, ICommand> commands = new HashMap<>();
    
    public void registerCommand(String name, ICommand command) {
        commands.put(name, command);
    }
    
    public boolean executeCommand(CommandSender sender, String commandName, String[] args) {
        if (!commands.containsKey(commandName)) {
            return false;
        }
        return commands.get(commandName).execute(sender, args);
    }
}
Такой подход дает гибкость и соответствует принципу единственной ответственности (S в SOLID). Каждая команда занимается только своей задачей, что упрощает тестирование и модификацию.

Паттерн Observer уже встроен в систему событий Bukkit. Вместо того чтобы напрямую вызывать методы классов при возникновении событий, система уведомляет всех зарегистрированных слушателей. Однако мы можем пойти дальше и создать собственную систему событий для внутренней логики плагина:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface CustomEventListener {
    void onPlayerScoreChange(UUID playerId, int newScore);
}
 
public class ScoreManager {
    private Map<UUID, Integer> playerScores = new HashMap<>();
    private List<CustomEventListener> listeners = new ArrayList<>();
    
    public void addListener(CustomEventListener listener) {
        listeners.add(listener);
    }
    
    public void setPlayerScore(UUID playerId, int score) {
        playerScores.put(playerId, score);
        // Уведомляем всех слушателей
        for (CustomEventListener listener : listeners) {
            listener.onPlayerScoreChange(playerId, score);
        }
    }
}
Для создания игровых объектов удобно использовать Factory Method. Например, при создании кастомных предметов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface ItemFactory {
    ItemStack createItem();
}
 
public class MagicWandFactory implements ItemFactory {
    private final Plugin plugin;
    
    public MagicWandFactory(Plugin plugin) {
        this.plugin = plugin;
    }
    
    @Override
    public ItemStack createItem() {
        ItemStack wand = new ItemStack(Material.BLAZE_ROD);
        ItemMeta meta = wand.getItemMeta();
        meta.setDisplayName("§5Волшебная палочка");
        
        // Добавляем метаданные
        NamespacedKey key = new NamespacedKey(plugin, "magic_wand");
        meta.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte)1);
        
        wand.setItemMeta(meta);
        return wand;
    }
}
Я часто сталкивался с необходимостью обрабатывать различные типы действий по-разному. Паттерн Strategy идеально решает эту проблему:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface SpellStrategy {
    void cast(Player player, ItemStack wand);
}
 
public class FireballSpell implements SpellStrategy {
    @Override
    public void cast(Player player, ItemStack wand) {
        player.launchProjectile(Fireball.class);
    }
}
 
public class LightningSpell implements SpellStrategy {
    @Override
    public void cast(Player player, ItemStack wand) {
        player.getWorld().strikeLightning(player.getTargetBlock(null, 50).getLocation());
    }
}
 
public class SpellManager {
    private Map<String, SpellStrategy> spells = new HashMap<>();
    
    public void registerSpell(String name, SpellStrategy spell) {
        spells.put(name, spell);
    }
    
    public void castSpell(String name, Player player, ItemStack wand) {
        if (spells.containsKey(name)) {
            spells.get(name).cast(player, wand);
        }
    }
}
Для расширения функциональности предметов отлично подходит паттерн Decorator. Допустим, мы хотим добавлять различные эффекты к оружию:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public interface Weapon {
    void attack(Player player, Entity target);
}
 
public class BaseWeapon implements Weapon {
    private final ItemStack item;
    
    public BaseWeapon(ItemStack item) {
        this.item = item;
    }
    
    @Override
    public void attack(Player player, Entity target) {
        // Базовая атака
        if (target instanceof LivingEntity) {
            ((LivingEntity) target).damage(5, player);
        }
    }
}
 
public abstract class WeaponDecorator implements Weapon {
    protected Weapon decoratedWeapon;
    
    public WeaponDecorator(Weapon weapon) {
        this.decoratedWeapon = weapon;
    }
}
 
public class FireWeaponDecorator extends WeaponDecorator {
    public FireWeaponDecorator(Weapon weapon) {
        super(weapon);
    }
    
    @Override
    public void attack(Player player, Entity target) {
        decoratedWeapon.attack(player, target);
        
        // Дополнительный эффект огня
        target.setFireTicks(100);
    }
}
Это позволяет динамически комбинировать различные эффекты. Например, можно создать меч с эффектами огня и яда одновременно.

Для обработки сложных последовательностей действий подойдет Chain of Responsibility. Например, при проверке возможности строительства:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public abstract class BuildValidator {
    private BuildValidator next;
    
    public BuildValidator setNext(BuildValidator next) {
        this.next = next;
        return next;
    }
    
    public boolean validate(Player player, Block block) {
        boolean result = doValidate(player, block);
        
        if (result && next != null) {
            return next.validate(player, block);
        }
        
        return result;
    }
    
    protected abstract boolean doValidate(Player player, Block block);
}
 
public class PermissionValidator extends BuildValidator {
    @Override
    protected boolean doValidate(Player player, Block block) {
        return player.hasPermission("build.place");
    }
}
 
public class RegionValidator extends BuildValidator {
    @Override
    protected boolean doValidate(Player player, Block block) {
        // Проверка региона
        return true; // Упрощено для примера
    }
}
 
// Использование
BuildValidator validator = new PermissionValidator();
validator.setNext(new RegionValidator());
 
@EventHandler
public void onBlockPlace(BlockPlaceEvent event) {
    if (!validator.validate(event.getPlayer(), event.getBlock())) {
        event.setCancelled(true);
    }
}
Еще один паттерн, который я использую почти в каждом серьезном плагине - Singleton. В мире Minecraft нам часто требуется иметь доступ к экземпляру плагина из любой точки кода. Вместо передачи ссылки через каждый метод можно реализовать безопасный доступ к единственному экземпляру:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
public final class MyPlugin extends JavaPlugin {
    private static MyPlugin instance;
    
    @Override
    public void onEnable() {
        instance = this;
        // Остальной код
    }
    
    public static MyPlugin getInstance() {
        return instance;
    }
}
Теперь из любого класса можно получить доступ к экземпляру плагина через MyPlugin.getInstance(). Однако стоит помнить, что злоупотребление этим паттерном может привести к сильной связанности кода. Я считаю, что его следует применять только для основного класса плагина, а для всех остальных компонентов лучше использовать dependency injection.

Когда дело доходит до создания сложных объектов с множеством настраиваемых параметров, паттерн Builder становится незаменимым:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class ArenaBuilder {
    private String name;
    private Location corner1;
    private Location corner2;
    private GameMode gameMode;
    private int timeLimit = 300; // 5 минут по умолчанию
    private int minPlayers = 2;
    private int maxPlayers = 8;
    private List<Location> spawnPoints = new ArrayList<>();
    
    public ArenaBuilder setName(String name) {
        this.name = name;
        return this;
    }
    
    public ArenaBuilder setBounds(Location corner1, Location corner2) {
        this.corner1 = corner1;
        this.corner2 = corner2;
        return this;
    }
    
    public ArenaBuilder setGameMode(GameMode gameMode) {
        this.gameMode = gameMode;
        return this;
    }
    
    public ArenaBuilder setTimeLimit(int seconds) {
        this.timeLimit = seconds;
        return this;
    }
    
    public ArenaBuilder setPlayerLimits(int min, int max) {
        this.minPlayers = min;
        this.maxPlayers = max;
        return this;
    }
    
    public ArenaBuilder addSpawnPoint(Location location) {
        this.spawnPoints.add(location);
        return this;
    }
    
    public Arena build() {
        // Проверяем обязательные параметры
        if (name == null || corner1 == null || corner2 == null || gameMode == null) {
            throw new IllegalStateException("Не все обязательные параметры указаны!");
        }
        
        if (spawnPoints.size() < minPlayers) {
            throw new IllegalStateException("Недостаточно точек возрождения!");
        }
        
        return new Arena(name, corner1, corner2, gameMode, timeLimit, minPlayers, maxPlayers, spawnPoints);
    }
}
Использование билдера делает код более читаемым и понятным:

Java
1
2
3
4
5
6
7
8
9
Arena arena = new ArenaBuilder()
    .setName("Дуэльная арена")
    .setBounds(loc1, loc2)
    .setGameMode(GameMode.PVP)
    .setTimeLimit(180)
    .setPlayerLimits(2, 2)
    .addSpawnPoint(spawn1)
    .addSpawnPoint(spawn2)
    .build();
Для работы с иерархическими структурами, такими как команды и подкоманды, отлично подходит паттерн Composite:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public interface CommandNode {
    boolean execute(CommandSender sender, String[] args);
    List<String> tabComplete(CommandSender sender, String[] args);
}
 
public class LeafCommand implements CommandNode {
    private final String name;
    private final Executable executor;
    
    public LeafCommand(String name, Executable executor) {
        this.name = name;
        this.executor = executor;
    }
    
    @Override
    public boolean execute(CommandSender sender, String[] args) {
        return executor.execute(sender, args);
    }
    
    @Override
    public List<String> tabComplete(CommandSender sender, String[] args) {
        return new ArrayList<>(); // Или реальное автодополнение
    }
}
 
public class CompositeCommand implements CommandNode {
    private final String name;
    private final Map<String, CommandNode> subcommands = new HashMap<>();
    
    public CompositeCommand(String name) {
        this.name = name;
    }
    
    public void addSubcommand(String name, CommandNode command) {
        subcommands.put(name, command);
    }
    
    @Override
    public boolean execute(CommandSender sender, String[] args) {
        if (args.length == 0) {
            // Показать справку по подкомандам
            return true;
        }
        
        CommandNode subcommand = subcommands.get(args[0]);
        if (subcommand == null) {
            sender.sendMessage("Неизвестная подкоманда: " + args[0]);
            return false;
        }
        
        // Передаем подкоманде оставшиеся аргументы
        return subcommand.execute(sender, Arrays.copyOfRange(args, 1, args.length));
    }
    
    @Override
    public List<String> tabComplete(CommandSender sender, String[] args) {
        if (args.length == 1) {
            // Возвращаем список подкоманд
            return new ArrayList<>(subcommands.keySet());
        } else if (args.length > 1) {
            CommandNode subcommand = subcommands.get(args[0]);
            if (subcommand != null) {
                return subcommand.tabComplete(sender, Arrays.copyOfRange(args, 1, args.length));
            }
        }
        
        return new ArrayList<>();
    }
}
Такая структура позволяет создавать сложные древовидные команды, например /clan create, /clan invite, /clan settings color и т.д.

Паттерн State идеально подходит для управления состояниями игровых объектов или миниигр:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public interface GameState {
    void enter();
    void update();
    void exit();
    boolean onPlayerJoin(Player player);
    boolean onPlayerQuit(Player player);
}
 
public class LobbyState implements GameState {
    private final MiniGame game;
    
    public LobbyState(MiniGame game) {
        this.game = game;
    }
    
    @Override
    public void enter() {
        game.broadcastMessage("Ожидание игроков...");
    }
    
    @Override
    public void update() {
        if (game.getPlayers().size() >= game.getMinPlayers()) {
            game.startCountdown(10); // 10 секунд до начала
        }
    }
    
    @Override
    public void exit() {
        game.broadcastMessage("Игра начинается!");
    }
    
    @Override
    public boolean onPlayerJoin(Player player) {
        game.addPlayer(player);
        player.teleport(game.getLobbySpawn());
        game.broadcastMessage(player.getName() + " присоединился! (" 
            + game.getPlayers().size() + "/" + game.getMaxPlayers() + ")");
        return true;
    }
    
    @Override
    public boolean onPlayerQuit(Player player) {
        game.removePlayer(player);
        game.broadcastMessage(player.getName() + " вышел! (" 
            + game.getPlayers().size() + "/" + game.getMaxPlayers() + ")");
        return true;
    }
}
 
public class PlayingState implements GameState {
    private final MiniGame game;
    
    public PlayingState(MiniGame game) {
        this.game = game;
    }
    
    @Override
    public void enter() {
        for (Player player : game.getPlayers()) {
            player.teleport(game.getRandomSpawn());
            player.getInventory().clear();
            player.setHealth(player.getMaxHealth());
            // Выдать стартовый инвентарь
        }
        game.startTimer(game.getTimeLimit());
    }
    
    @Override
    public void update() {
        // Проверить условия завершения игры
        if (game.getAlivePlayers().size() <= 1 || game.getTimeLeft() <= 0) {
            game.changeState(new EndState(game));
        }
    }
    
    @Override
    public void exit() {
        // Отменить все задачи, связанные с игровым процессом
    }
    
    @Override
    public boolean onPlayerJoin(Player player) {
        // Игра уже идет, сделать зрителем или отказать
        player.sendMessage("Игра уже началась!");
        return false;
    }
    
    @Override
    public boolean onPlayerQuit(Player player) {
        game.eliminatePlayer(player);
        game.broadcastMessage(player.getName() + " выбыл из игры!");
        return true;
    }
}

Продвинутые техники разработки



В процессе разработки крупных плагинов я быстро понял, что базовых возможностей Bukkit API недостаточно для создания по-настоящему впечетляющих модификаций. Здесь приходится погружаться в более сложные техники, которые раскрывают истинный потенциал плагинов. Асинхронное программирование - первый барьер, с которым сталкиваются разработчики серьезных плагинов. Minecraft сервер работает в одном потоке, и любая долгая операция может вызвать лаги для всех игроков. Помню свой первый экономический плагин - он напрочь вешал сервер при обращении к базе данных. Решение проблемы пришло с пониманием асинхронного выполнения задач:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Никогда не делайте так в основном потоке!
public void onCommand(...) {
    Connection conn = database.getConnection();
    ResultSet rs = conn.executeQuery("SELECT * FROM huge_table");
    // Сервер зависает, пока выполняется запрос
}
 
// Правильный подход
public void onCommand(...) {
    Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
        try (Connection conn = database.getConnection()) {
            ResultSet rs = conn.executeQuery("SELECT * FROM huge_table");
            
            // Обрабатываем результаты в асинхронном потоке
            List<PlayerData> players = new ArrayList<>();
            while (rs.next()) {
                players.add(new PlayerData(rs));
            }
            
            // Возвращаемся в основной поток для работы с API
            Bukkit.getScheduler().runTask(plugin, () -> {
                // Здесь безопасно использовать Bukkit API
                for (PlayerData data : players) {
                    Player player = Bukkit.getPlayer(data.getUuid());
                    if (player != null) {
                        player.sendMessage("Ваш баланс: " + data.getBalance());
                    }
                }
            });
        } catch (SQLException e) {
            e.printStackTrace();
        }
    });
}
Для серьезных проектов я рекомендую использовать пулы соединений вроде HikariCP - они значительно повышают производительность при работе с базами данных.

Другая продвинутая техника - создание сложных пользовательских интерфейсов. Базовые меню с предметами мы уже рассмотрели, но как насчет динамичных, интерактивных, многостраничных меню? Я разработал собственный фреймворк для этого:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public abstract class MenuPage {
    protected final Inventory inventory;
    protected final Player player;
    protected final MenuManager manager;
    
    public MenuPage(Player player, String title, int rows, MenuManager manager) {
        this.player = player;
        this.manager = manager;
        this.inventory = Bukkit.createInventory(null, rows * 9, title);
        initializeItems();
    }
    
    protected abstract void initializeItems();
    
    public void open() {
        player.openInventory(inventory);
        manager.registerActiveMenu(player.getUniqueId(), this);
    }
    
    public abstract void handleClick(InventoryClickEvent event);
}
 
public class ShopPage extends MenuPage {
    private int currentPage = 0;
    private final List<ShopItem> items;
    
    public ShopPage(Player player, MenuManager manager, List<ShopItem> items) {
        super(player, "Магазин - Страница " + (currentPage + 1), 6, manager);
        this.items = items;
    }
    
    @Override
    protected void initializeItems() {
        // Очищаем инвентарь
        inventory.clear();
        
        // Добавляем товары для текущей страницы
        int itemsPerPage = 45; // 5 рядов по 9 ячеек
        int startIndex = currentPage * itemsPerPage;
        
        for (int i = 0; i < itemsPerPage && i + startIndex < items.size(); i++) {
            ShopItem shopItem = items.get(i + startIndex);
            inventory.setItem(i, shopItem.createDisplayItem());
        }
        
        // Добавляем кнопки навигации
        if (currentPage > 0) {
            inventory.setItem(45, createNavigationButton("Предыдущая страница"));
        }
        if ((currentPage + 1) * itemsPerPage < items.size()) {
            inventory.setItem(53, createNavigationButton("Следующая страница"));
        }
        
        // Кнопка возврата
        inventory.setItem(49, createNavigationButton("Вернуться"));
    }
    
    @Override
    public void handleClick(InventoryClickEvent event) {
        event.setCancelled(true);
        
        if (event.getCurrentItem() == null) return;
        
        int slot = event.getSlot();
        
        if (slot == 45 && currentPage > 0) {
            currentPage--;
            initializeItems();
        } else if (slot == 53 && (currentPage + 1) * 45 < items.size()) {
            currentPage++;
            initializeItems();
        } else if (slot == 49) {
            // Вернуться в главное меню
            manager.openMainMenu(player);
        } else if (slot < 45) {
            // Клик по товару
            int itemIndex = currentPage * 45 + slot;
            if (itemIndex < items.size()) {
                ShopItem item = items.get(itemIndex);
                // Логика покупки товара
            }
        }
    }
}
Интеграция с внешними API открывает огромные возможности для плагинов. Например, можно связать игровые достижения с системой наград на вашем сайте или создать торговую площадку, отображаемую вне игры:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ExternalApiClient {
    private final String apiKey;
    private final String baseUrl;
    private final OkHttpClient client;
    
    public ExternalApiClient(String apiKey, String baseUrl) {
        this.apiKey = apiKey;
        this.baseUrl = baseUrl;
        this.client = new OkHttpClient.Builder()
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .build();
    }
    
    public CompletableFuture<Boolean> registerAchievement(UUID playerId, String achievementId) {
        CompletableFuture<Boolean> future = new CompletableFuture<>();
        
        Bukkit.getScheduler().runTaskAsynchronously(MyPlugin.getInstance(), () -> {
            try {
                JSONObject json = new JSONObject();
                json.put("player_id", playerId.toString());
                json.put("achievement_id", achievementId);
                json.put("timestamp", System.currentTimeMillis());
                
                Request request = new Request.Builder()
                    .url(baseUrl + "/achievements")
                    .addHeader("Authorization", "Bearer " + apiKey)
                    .post(RequestBody.create(
                        MediaType.parse("application/json"), json.toString()))
                    .build();
                
                try (Response response = client.newCall(request).execute()) {
                    future.complete(response.isSuccessful());
                }
            } catch (Exception e) {
                e.printStackTrace();
                future.completeExceptionally(e);
            }
        });
        
        return future;
    }
}
Но самая мощная (и опасная) техника - это прямой доступ к классам NMS (Net Minecraft Server). Это внутренние классы серверной части Minecraft, не предназначенные для использования плагинами. Однако, они предоставляют доступ к возможностям, недоступным через стандартные API:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void sendTitle(Player player, String title, String subtitle, int fadeIn, int stay, int fadeOut) {
    try {
        // Получаем версию сервера
        String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
        
        // Получаем класс CraftPlayer
        Class<?> craftPlayerClass = Class.forName("org.bukkit.craftbukkit." + version + ".entity.CraftPlayer");
        
        // Получаем объект EntityPlayer
        Object craftPlayer = craftPlayerClass.cast(player);
        Object entityPlayer = craftPlayerClass.getMethod("getHandle").invoke(craftPlayer);
        
        // Получаем соединение игрока
        Object playerConnection = entityPlayer.getClass().getField("playerConnection").get(entityPlayer);
        
        // Создаем пакеты для заголовка и подзаголовка
        Class<?> packetClass = Class.forName("net.minecraft.server." + version + ".PacketPlayOutTitle");
        Class<?> enumTitleAction = Class.forName("net.minecraft.server." + version + ".PacketPlayOutTitle$EnumTitleAction");
        Class<?> chatComponentClass = Class.forName("net.minecraft.server." + version + ".IChatBaseComponent");
        Class<?> chatSerializerClass = Class.forName("net.minecraft.server." + version + ".IChatBaseComponent$ChatSerializer");
        
        Object titleAction = enumTitleAction.getField("TITLE").get(null);
        Object subtitleAction = enumTitleAction.getField("SUBTITLE").get(null);
        
        Object titleComponent = chatSerializerClass.getMethod("a", String.class)
            .invoke(null, "{\"text\":\"" + title + "\"}");
        Object subtitleComponent = chatSerializerClass.getMethod("a", String.class)
            .invoke(null, "{\"text\":\"" + subtitle + "\"}");
        
        // Создаем и отправляем пакет времени
        Constructor<?> titleConstructor = packetClass.getConstructor(enumTitleAction, chatComponentClass, int.class, int.class, int.class);
        Object titleTimesPacket = packetClass.getConstructor(int.class, int.class, int.class)
            .newInstance(fadeIn, stay, fadeOut);
        
        // Отправляем пакеты
        playerConnection.getClass().getMethod("sendPacket", Class.forName("net.minecraft.server." + version + ".Packet"))
            .invoke(playerConnection, titleTimesPacket);
        
        Object titlePacket = titleConstructor.newInstance(titleAction, titleComponent, fadeIn, stay, fadeOut);
        playerConnection.getClass().getMethod("sendPacket", Class.forName("net.minecraft.server." + version + ".Packet"))
            .invoke(playerConnection, titlePacket);
        
        Object subtitlePacket = titleConstructor.newInstance(subtitleAction, subtitleComponent, fadeIn, stay, fadeOut);
        playerConnection.getClass().getMethod("sendPacket", Class.forName("net.minecraft.server." + version + ".Packet"))
            .invoke(playerConnection, subtitlePacket);
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}
Работа с NMS требует осторожности – она сильно зависит от версии Minecraft и может сломаться при обновлениях. Для облегчения работы я создаю вспомогательные классы-обертки:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class NMSHandler {
    private static final String VERSION = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
    
    public static Class<?> getNMSClass(String className) {
        try {
            return Class.forName("net.minecraft.server." + VERSION + "." + className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    public static Class<?> getCraftBukkitClass(String className) {
        try {
            return Class.forName("org.bukkit.craftbukkit." + VERSION + "." + className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}
Один из моих любимых трюков – отправка пакетов для создания голограмм. До появления API текстов в воздухе, приходилось изворачиватся через NMS:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void spawnHologram(Location location, String text) {
    try {
        // Создаем ArmorStand с невидимыми атрибутами
        Object world = location.getWorld().getClass().getMethod("getHandle").invoke(location.getWorld());
        
        Class<?> entityArmorStandClass = getNMSClass("EntityArmorStand");
        Constructor<?> constructor = entityArmorStandClass.getConstructor(getNMSClass("World"), double.class, double.class, double.class);
        
        Object armorStand = constructor.newInstance(world, location.getX(), location.getY(), location.getZ());
        
        // Настраиваем свойства ArmorStand
        entityArmorStandClass.getMethod("setInvisible", boolean.class).invoke(armorStand, true);
        entityArmorStandClass.getMethod("setCustomNameVisible", boolean.class).invoke(armorStand, true);
        entityArmorStandClass.getMethod("setGravity", boolean.class).invoke(armorStand, false);
        
        // Устанавливаем текст
        Class<?> chatComponentClass = getNMSClass("IChatBaseComponent");
        Class<?> chatSerializerClass = getNMSClass("IChatBaseComponent$ChatSerializer");
        Object chatComponent = chatSerializerClass.getMethod("a", String.class).invoke(null, "{\"text\":\"" + text + "\"}");
        
        entityArmorStandClass.getMethod("setCustomName", chatComponentClass).invoke(armorStand, chatComponent);
        
        // Спавним ArmorStand для всех игроков
        Class<?> worldClass = getNMSClass("World");
        worldClass.getMethod("addEntity", getNMSClass("Entity")).invoke(world, armorStand);
        
    } catch (Exception e) {
        e.printStackTrace();
    }
}
Еще один сложный, но эффектный прием – манипуляция пакетами для создания фальшивых блоков, видимых только определенным игрокам. Это полезно для отображения границ территорий или регионов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void sendFakeBlock(Player player, Location location, Material material) {
    try {
        // Получаем соединение игрока
        Object craftPlayer = getCraftBukkitClass("entity.CraftPlayer").cast(player);
        Object entityPlayer = craftPlayer.getClass().getMethod("getHandle").invoke(craftPlayer);
        Object connection = entityPlayer.getClass().getField("playerConnection").get(entityPlayer);
        
        // Создаем информацию о блоке
        Object blockPosition = getNMSClass("BlockPosition")
            .getConstructor(int.class, int.class, int.class)
            .newInstance(location.getBlockX(), location.getBlockY(), location.getBlockZ());
        
        // Получаем материал блока
        Object craftBlock = getCraftBukkitClass("util.CraftMagicNumbers")
            .getMethod("getBlock", Material.class)
            .invoke(null, material);
        
        // Создаем пакет изменения блока
        Object blockChangePacket = getNMSClass("PacketPlayOutBlockChange")
            .getConstructor()
            .newInstance();
        
        blockChangePacket.getClass().getField("a").set(blockChangePacket, blockPosition);
        blockChangePacket.getClass().getField("block").set(blockChangePacket, craftBlock);
        
        // Отправляем пакет
        connection.getClass()
            .getMethod("sendPacket", getNMSClass("Packet"))
            .invoke(connection, blockChangePacket);
            
    } catch (Exception e) {
        e.printStackTrace();
    }
}
Для обмена данными между сервером и клиентом без использования NMS можно применять плагинные каналы (Plugin Channels). Они позволяют обмениваться двоичными данными между плагином и модами на стороне клиента:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class PluginChannelManager implements PluginMessageListener {
    private final Plugin plugin;
    private final String channel;
    
    public PluginChannelManager(Plugin plugin, String channel) {
        this.plugin = plugin;
        this.channel = channel;
        
        plugin.getServer().getMessenger().registerOutgoingPluginChannel(plugin, channel);
        plugin.getServer().getMessenger().registerIncomingPluginChannel(plugin, channel, this);
    }
    
    public void sendData(Player player, byte[] data) {
        player.sendPluginMessage(plugin, channel, data);
    }
    
    @Override
    public void onPluginMessageReceived(String channel, Player player, byte[] message) {
        if (!channel.equals(this.channel)) return;
        
        // Обработка полученных данных
        ByteArrayInputStream stream = new ByteArrayInputStream(message);
        DataInputStream in = new DataInputStream(stream);
        
        try {
            // Пример чтения строки из сообщения
            String subChannel = in.readUTF();
            
            if (subChannel.equals("SomeAction")) {
                // Выполнить действие на основе полученных данных
                String additionalData = in.readUTF();
                // ...
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
Для обработки большого количества объектов в игровом мире (например, для кастомной физики или AI) полезен пространственный индекс – структура данных, оптимизированная для быстрого поиска объектов в трехмерном пространстве:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class SpatialHashGrid {
    private final int cellSize;
    private final Map<Long, List<Entity>> grid = new HashMap<>();
    
    public SpatialHashGrid(int cellSize) {
        this.cellSize = cellSize;
    }
    
    private long hashPosition(Location location) {
        int x = (int) Math.floor(location.getX() / cellSize);
        int y = (int) Math.floor(location.getY() / cellSize);
        int z = (int) Math.floor(location.getZ() / cellSize);
        
        return ((long)x << 42) | ((long)y << 21) | z;
    }
    
    public void insertEntity(Entity entity) {
        long hash = hashPosition(entity.getLocation());
        grid.computeIfAbsent(hash, k -> new ArrayList<>()).add(entity);
    }
    
    public List<Entity> getNearbyEntities(Location location, double radius) {
        Set<Long> cellsToCheck = new HashSet<>();
        List<Entity> result = new ArrayList<>();
        
        // Определяем все ячейки в радиусе поиска
        int radiusCells = (int) Math.ceil(radius / cellSize);
        int baseX = (int) Math.floor(location.getX() / cellSize);
        int baseY = (int) Math.floor(location.getY() / cellSize);
        int baseZ = (int) Math.floor(location.getZ() / cellSize);
        
        for (int x = -radiusCells; x <= radiusCells; x++) {
            for (int y = -radiusCells; y <= radiusCells; y++) {
                for (int z = -radiusCells; z <= radiusCells; z++) {
                    long hash = ((long)(baseX + x) << 42) | ((long)(baseY + y) << 21) | (baseZ + z);
                    cellsToCheck.add(hash);
                }
            }
        }
        
        // Собираем все сущности из выбранных ячеек
        double radiusSquared = radius * radius;
        for (Long hash : cellsToCheck) {
            List<Entity> entitiesInCell = grid.get(hash);
            if (entitiesInCell != null) {
                for (Entity entity : entitiesInCell) {
                    if (entity.getLocation().distanceSquared(location) <= radiusSquared) {
                        result.add(entity);
                    }
                }
            }
        }
        
        return result;
    }
}
Такая структура значительно ускоряет поиск объектов в больших мирах, особенно когда требуется регулярно проверять множество объектов.
Для визуализации сложных структур данных я часто использую систему отладочных лучей, которые рисуются прямо в игровом мире с помощью частиц:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class DebugRenderer {
    public static void drawLine(Location start, Location end, Particle particle, double density) {
        World world = start.getWorld();
        double distance = start.distance(end);
        Vector direction = end.toVector().subtract(start.toVector()).normalize();
        
        for (double d = 0; d < distance; d += 1.0 / density) {
            Location point = start.clone().add(direction.clone().multiply(d));
            world.spawnParticle(particle, point, 1, 0, 0, 0, 0);
        }
    }
    
    public static void drawCube(Location center, double size, Particle particle) {
        double half = size / 2;
        World world = center.getWorld();
        
        Location p1 = center.clone().add(-half, -half, -half);
        Location p2 = center.clone().add(half, -half, -half);
        Location p3 = center.clone().add(half, -half, half);
        Location p4 = center.clone().add(-half, -half, half);
        Location p5 = center.clone().add(-half, half, -half);
        Location p6 = center.clone().add(half, half, -half);
        Location p7 = center.clone().add(half, half, half);
        Location p8 = center.clone().add(-half, half, half);
        
        // Нижняя грань
        drawLine(p1, p2, particle, 10);
        drawLine(p2, p3, particle, 10);
        drawLine(p3, p4, particle, 10);
        drawLine(p4, p1, particle, 10);
        
        // Верхняя грань
        drawLine(p5, p6, particle, 10);
        drawLine(p6, p7, particle, 10);
        drawLine(p7, p8, particle, 10);
        drawLine(p8, p5, particle, 10);
        
        // Стороны
        drawLine(p1, p5, particle, 10);
        drawLine(p2, p6, particle, 10);
        drawLine(p3, p7, particle, 10);
        drawLine(p4, p8, particle, 10);
    }
}

Тестирование и отладка



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

Первое, что я всегда делаю - настраиваю локальную среду для тестов. Лучше всего держать отдельную папку с "чистым" сервером, специально для тестирования. В моей практике хорошо зарекомендовала себя следующая структура:

Java
1
2
3
4
5
6
7
8
9
10
11
12
dev/
  ├── server/
  │     ├── plugins/
  │     ├── server.jar
  │     └── ...
  ├── test-scripts/
  │     ├── start-server.bat
  │     ├── compile-and-deploy.bat
  │     └── ...
  └── mock-players/
        ├── player1.js
        └── ...
Скрипт для компиляции и деплоя плагина значительно ускоряет цикл разработки:

Bash
1
2
3
4
5
6
7
8
9
10
11
@echo off
cd ..
call gradle build
if %ERRORLEVEL% NEQ 0 (
    echo Ошибка сборки!
    pause
    exit /b %ERRORLEVEL%
)
copy /Y build\libs\myplugin-1.0.jar server\plugins\
cd server
echo Плагин обновлен! Введите "reload confirm" на сервере.
Для автоматизации тестирования я начал использовать Mineflayer - библиотеку на Node.js, которая позволяет создавать ботов-игроков. С ее помощью можно написать скрипты, имитирующие поведение реальных игроков:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const mineflayer = require('mineflayer')
 
const bot = mineflayer.createBot({
  host: 'localhost',
  port: 25565,
  username: 'TestBot'
})
 
bot.on('spawn', () => {
  console.log('Бот подключился к серверу')
  
  // Ждем 2 секунды и выполняем команду
  setTimeout(() => {
    bot.chat('/myplugin test')
  }, 2000)
  
  // Тестируем взаимодействие с блоками
  setTimeout(() => {
    const target = bot.blockAt(bot.entity.position.offset(0, -1, 0))
    bot.activateBlock(target)
  }, 4000)
})
 
bot.on('message', (msg) => {
  console.log(`Получено сообщение: ${msg.toString()}`)
})
Для профилирования производительности плагина я часто использую Timings - встроенный инструмент Paper. Активировать его можно командой /timings on, а получить отчет - /timings report. Но иногда нужна более детальная информация, поэтому я добавляю в код собственный профайлер:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SimpleProfiler {
  private final Map<String, Long> startTimes = new HashMap<>();
  private final Map<String, Long> totalTimes = new HashMap<>();
  private final Map<String, Integer> counts = new HashMap<>();
  
  public void start(String section) {
      startTimes.put(section, System.nanoTime());
  }
  
  public void end(String section) {
      if (!startTimes.containsKey(section)) {
          return;
      }
      
      long time = System.nanoTime() - startTimes.get(section);
      totalTimes.put(section, totalTimes.getOrDefault(section, 0L) + time);
      counts.put(section, counts.getOrDefault(section, 0) + 1);
  }
  
  public void printResults() {
      List<Map.Entry<String, Long>> entries = new ArrayList<>(totalTimes.entrySet());
      entries.sort(Map.Entry.<String, Long>comparingByValue().reversed());
      
      System.out.println("=== Результаты профилирования ===");
      for (Map.Entry<String, Long> entry : entries) {
          String section = entry.getKey();
          long total = entry.getValue();
          int count = counts.getOrDefault(section, 1);
          double avg = (double) total / count / 1_000_000; // в миллисекундах
          
          System.out.printf("%s: выполнено %d раз, среднее время: %.3f мс\n", 
              section, count, avg);
      }
  }
}
Один из самых болезненых аспектов тестирования - проверка работы плагина с разными версиями API. Здесь выручают абстрактные фабрики и адаптеры:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface VersionAdapter {
  void sendActionBar(Player player, String message);
  void spawnParticle(Location location, String particleType);
  // ... другие методы, зависящие от версии
}
 
public class VersionAdapterFactory {
  public static VersionAdapter getAdapter() {
      String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3];
      
      if (version.startsWith("v1_8")) {
          return new V1_8_Adapter();
      } else if (version.startsWith("v1_12")) {
          return new V1_12_Adapter();
      } else {
          return new ModernAdapter(); // 1.13+
      }
  }
}
Такой подход позволяет написать код один раз, а затем адаптировать его для разных версий Minecraft.
Отдельного внимания заслуживает обработка исключений. Неперехваченое исключение может обрушить весь сервер. Я всегда оборачиваю потенциально опасный код в try-catch и включаю подробное логирование:

Java
1
2
3
4
5
6
7
8
9
10
try {
  // Потенциально опасный код
} catch (Exception e) {
  plugin.getLogger().severe("Произошла ошибка при выполнении операции!");
  plugin.getLogger().severe("Детали: " + e.getMessage());
  
  StringWriter sw = new StringWriter();
  e.printStackTrace(new PrintWriter(sw));
  plugin.getLogger().severe("Стек вызовов: " + sw.toString());
}
Для критических ошибок я разработал систему "мягкой деактивации" - плагин продолжает работать, но отключает проблемные функции вместо полного крашения:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void handleCriticalError(String feature, Exception e) {
  disabledFeatures.add(feature);
  
  getLogger().severe("====== КРИТИЧЕСКАЯ ОШИБКА ======");
  getLogger().severe("Функция '" + feature + "' отключена из-за ошибки!");
  getLogger().severe("Причина: " + e.getMessage());
  e.printStackTrace();
  
  // Уведомить администраторов онлайн
  for (Player player : Bukkit.getOnlinePlayers()) {
      if (player.hasPermission("myplugin.admin")) {
          player.sendMessage(ChatColor.RED + "[MyPlugin] Критическая ошибка в функции '" 
              + feature + "'. Проверьте консоль!");
      }
  }
}

Практический пример



За годы разработки плагинов я накопил приличную коллекцию кода, и сегодня поделюсь полноценным примером плагина, объединяющего большинство рассмотренных концепций. Речь пойдет о плагине "TeleCompass" - модификации, которая позволяет игрокам создавать особые компасы для телепортации к привязанным точкам. Структура проекта организована с использованием принципа разделения ответственности:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
com.myserver.telecompass/
├── TeleCompass.java           // Основной класс плагина
├── commands/                  // Командные обработчики
│   ├── CommandManager.java
│   ├── GiveCommand.java
│   └── ReloadCommand.java
├── config/                    // Управление конфигурацией
│   ├── ConfigManager.java
│   └── Messages.java
├── items/                     // Фабрики предметов
│   ├── ItemFactory.java
│   └── TelecompassFactory.java
├── listeners/                 // Обработчики событий
│   ├── CraftingListener.java
│   └── TeleportListener.java
└── utils/                     // Вспомогательные классы
    ├── ParticleUtils.java
    └── CooldownManager.java
Начнем с основного класса плагина, который следует принципам чистой архитектуры - главный класс не содержит бизнес-логики, а лишь координирует работу компонентов:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public final class TeleCompass extends JavaPlugin {
    private static TeleCompass instance;
    private ConfigManager configManager;
    private CommandManager commandManager;
    private CooldownManager cooldownManager;
    
    @Override
    public void onEnable() {
        instance = this;
        
        // Инициализация менеджеров
        configManager = new ConfigManager(this);
        cooldownManager = new CooldownManager(this);
        commandManager = new CommandManager(this);
        
        // Регистрация команд
        commandManager.registerCommand("telecompass", new GiveCommand(this));
        commandManager.registerCommand("telecompassreload", new ReloadCommand(this));
        
        // Регистрация обработчиков событий
        getServer().getPluginManager().registerEvents(new CraftingListener(this), this);
        getServer().getPluginManager().registerEvents(new TeleportListener(this), this);
        
        // Регистрация рецепта
        Bukkit.addRecipe(TelecompassFactory.createRecipe(this));
        
        getLogger().info("TeleCompass активирован!");
    }
    
    @Override
    public void onDisable() {
        getLogger().info("TeleCompass деактивирован!");
    }
    
    public static TeleCompass getInstance() {
        return instance;
    }
    
    public ConfigManager getConfigManager() {
        return configManager;
    }
    
    public CooldownManager getCooldownManager() {
        return cooldownManager;
    }
}
Для управления командами я применил паттерн Command, создав интерфейс для всех команд и менеджер, который ими управляет:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public interface ICommand {
    boolean execute(CommandSender sender, String[] args);
    List<String> tabComplete(CommandSender sender, String[] args);
}
 
public class CommandManager implements CommandExecutor, TabCompleter {
    private final TeleCompass plugin;
    private final Map<String, ICommand> commands = new HashMap<>();
    
    public CommandManager(TeleCompass plugin) {
        this.plugin = plugin;
    }
    
    public void registerCommand(String name, ICommand command) {
        commands.put(name.toLowerCase(), command);
        PluginCommand pluginCommand = plugin.getCommand(name);
        if (pluginCommand != null) {
            pluginCommand.setExecutor(this);
            pluginCommand.setTabCompleter(this);
        }
    }
    
    @Override
    public boolean onCommand(CommandSender sender, Command cmd, String label, String[] args) {
        ICommand command = commands.get(cmd.getName().toLowerCase());
        if (command != null) {
            return command.execute(sender, args);
        }
        return false;
    }
    
    @Override
    public List<String> onTabComplete(CommandSender sender, Command cmd, String alias, String[] args) {
        ICommand command = commands.get(cmd.getName().toLowerCase());
        if (command != null) {
            return command.tabComplete(sender, args);
        }
        return null;
    }
}
Управление конфигурацией вынесено в отдельный класс, чтобы изолировать логику работы с файлами от остального кода:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ConfigManager {
    private final TeleCompass plugin;
    private FileConfiguration config;
    private Messages messages;
    
    public ConfigManager(TeleCompass plugin) {
        this.plugin = plugin;
        reload();
    }
    
    public void reload() {
        plugin.saveDefaultConfig();
        plugin.reloadConfig();
        config = plugin.getConfig();
        messages = new Messages(this);
    }
    
    public int getCooldownTime() {
        return config.getInt("cooldown", 30);
    }
    
    public boolean isPermissionRequired() {
        return config.getBoolean("require_permission", true);
    }
    
    public FileConfiguration getConfig() {
        return config;
    }
    
    public Messages getMessages() {
        return messages;
    }
}
Для создания телекомпаса я использовал паттерн Factory, который инкапсулирует логику создания сложного объекта:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class TelecompassFactory {
    private static final int CUSTOM_MODEL_DATA = 1;
    
    public static ItemStack create() {
        ItemStack telecompass = new ItemStack(Material.COMPASS);
        ItemMeta meta = telecompass.getItemMeta();
        
        // Устанавливаем имя и описание
        meta.setDisplayName(ChatColor.LIGHT_PURPLE + "Телекомпас");
        List<String> lore = new ArrayList<>();
        lore.add(ChatColor.GRAY + "Привяжите к лодстоуну, затем");
        lore.add(ChatColor.GRAY + "используйте для телепортации.");
        meta.setLore(lore);
        
        // Добавляем метаданные для идентификации
        NamespacedKey key = new NamespacedKey(TeleCompass.getInstance(), "telecompass");
        meta.getPersistentDataContainer().set(key, PersistentDataType.BYTE, (byte)1);
        
        // Устанавливаем Custom Model Data для ресурспака
        meta.setCustomModelData(CUSTOM_MODEL_DATA);
        
        telecompass.setItemMeta(meta);
        return telecompass;
    }
    
    public static ShapedRecipe createRecipe(TeleCompass plugin) {
        ItemStack telecompass = create();
        NamespacedKey key = new NamespacedKey(plugin, "telecompass_recipe");
        
        ShapedRecipe recipe = new ShapedRecipe(key, telecompass);
        recipe.shape("EPE", "ECE", "EEE");
        recipe.setIngredient('E', Material.ENDER_PEARL);
        recipe.setIngredient('P', Material.ENDER_EYE);
        recipe.setIngredient('C', Material.COMPASS);
        
        return recipe;
    }
    
    public static boolean isTelecompass(ItemStack item) {
        if (item == null || item.getType() != Material.COMPASS) {
            return false;
        }
        
        ItemMeta meta = item.getItemMeta();
        if (meta == null) {
            return false;
        }
        
        NamespacedKey key = new NamespacedKey(TeleCompass.getInstance(), "telecompass");
        return meta.getPersistentDataContainer().has(key, PersistentDataType.BYTE);
    }
}
Одним из ключевых классов является обработчик событий телепортации, который реализует основную функциональность плагина:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class TeleportListener implements Listener {
    private final TeleCompass plugin;
    
    public TeleportListener(TeleCompass plugin) {
        this.plugin = plugin;
    }
    
    @EventHandler(priority = EventPriority.HIGH)
    public void onPlayerInteract(PlayerInteractEvent event) {
        // Проверяем, что игрок кликнул правой кнопкой мыши
        if (!event.getAction().isRightClick()) {
            return;
        }
        
        Player player = event.getPlayer();
        ItemStack item = player.getInventory().getItemInMainHand();
        
        // Проверяем, что предмет - телекомпас
        if (!TelecompassFactory.isTelecompass(item)) {
            return;
        }
        
        // Проверяем разрешение на использование
        if (plugin.getConfigManager().isPermissionRequired() && 
            !player.hasPermission("telecompass.use")) {
            player.sendMessage(plugin.getConfigManager().getMessages().getNoPermission());
            event.setCancelled(true);
            return;
        }
        
        // Если игрок кликает на лодстоун, позволяем стандартное взаимодействие
        if (event.getClickedBlock() != null && 
            event.getClickedBlock().getType() == Material.LODESTONE) {
            // Не отменяем событие - компас привяжется к лодстоуну
            return;
        }
        
        // Проверяем кулдаун
        if (!plugin.getCooldownManager().canUse(player)) {
            long remaining = plugin.getCooldownManager().getRemainingTime(player);
            player.sendMessage(plugin.getConfigManager().getMessages().getCooldownMessage(remaining));
            event.setCancelled(true);
            return;
        }
        
        // Телепортируем игрока
        teleportPlayer(player, item);
        event.setCancelled(true);
    }
    
    private void teleportPlayer(Player player, ItemStack compass) {
        ItemMeta meta = compass.getItemMeta();
        if (!(meta instanceof CompassMeta)) {
            player.sendMessage(plugin.getConfigManager().getMessages().getNotLinked());
            return;
        }
        
        CompassMeta compassMeta = (CompassMeta) meta;
        if (!compassMeta.isLodestoneTracked()) {
            player.sendMessage(plugin.getConfigManager().getMessages().getNotLinked());
            return;
        }
        
        Location destination = compassMeta.getLodestone();
        if (destination == null) {
            player.sendMessage(plugin.getConfigManager().getMessages().getNotLinked());
            return;
        }
        
        // Запоминаем начальную позицию для эффектов
        Location startLoc = player.getLocation().clone();
        
        // Эффекты перед телепортацией
        ParticleUtils.spawnTeleportParticles(startLoc);
        startLoc.getWorld().playSound(startLoc, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
        
        // Телепортация
        player.teleport(destination);
        
        // Эффекты после телепортации
        ParticleUtils.spawnTeleportParticles(destination);
        destination.getWorld().playSound(destination, Sound.ENTITY_ENDERMAN_TELEPORT, 1.0f, 1.0f);
        
        // Устанавливаем кулдаун
        plugin.getCooldownManager().setCooldown(player);
        
        player.sendMessage(plugin.getConfigManager().getMessages().getTeleportSuccess());
    }
}
Для управления кулдаунами я создал отдельный класс, который ведет учет времени последнего использования телекомпаса для каждого игрока:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class CooldownManager {
    private final TeleCompass plugin;
    private final Map<UUID, Long> cooldowns = new HashMap<>();
    
    public CooldownManager(TeleCompass plugin) {
        this.plugin = plugin;
    }
    
    public boolean canUse(Player player) {
        // Игроки с особым разрешением могут игнорировать кулдаун
        if (player.hasPermission("telecompass.nocooldown")) {
            return true;
        }
        
        UUID playerId = player.getUniqueId();
        if (!cooldowns.containsKey(playerId)) {
            return true;
        }
        
        long lastUsed = cooldowns.get(playerId);
        long currentTime = System.currentTimeMillis();
        int cooldownTime = plugin.getConfigManager().getCooldownTime() * 1000; // в миллисекундах
        
        return currentTime - lastUsed >= cooldownTime;
    }
    
    public long getRemainingTime(Player player) {
        UUID playerId = player.getUniqueId();
        if (!cooldowns.containsKey(playerId)) {
            return 0;
        }
        
        long lastUsed = cooldowns.get(playerId);
        long currentTime = System.currentTimeMillis();
        int cooldownTime = plugin.getConfigManager().getCooldownTime() * 1000;
        
        long remaining = (lastUsed + cooldownTime - currentTime) / 1000;
        return Math.max(0, remaining);
    }
    
    public void setCooldown(Player player) {
        cooldowns.put(player.getUniqueId(), System.currentTimeMillis());
    }
}
Для визуальных эффектов телепортации я выделил утилитный класс, который генерирует частицы при телепортации:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ParticleUtils {
    public static void spawnTeleportParticles(Location location) {
        World world = location.getWorld();
        
        // Создаем спиральные частицы портала
        for (double y = 0; y < 2; y += 0.1) {
            double radius = 1.0 - (y / 2);
            for (double angle = 0; angle < Math.PI * 2; angle += Math.PI / 16) {
                double x = Math.cos(angle) * radius;
                double z = Math.sin(angle) * radius;
                Location particleLoc = location.clone().add(x, y, z);
                world.spawnParticle(Particle.PORTAL, particleLoc, 2, 0, 0, 0, 0.05);
            }
        }
        
        // Добавляем облако частиц в центре
        world.spawnParticle(Particle.REVERSE_PORTAL, location.clone().add(0, 1, 0), 
                            50, 0.5, 0.5, 0.5, 0.1);
    }
}
Локализация сообщений - важный аспект разработки плагинов. Я создал класс Messages, который загружает все строки из конфигурации:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Messages {
    private final ConfigManager configManager;
    private String noPermission;
    private String cooldownMessage;
    private String notLinked;
    private String teleportSuccess;
    
    public Messages(ConfigManager configManager) {
        this.configManager = configManager;
        loadMessages();
    }
    
    private void loadMessages() {
        FileConfiguration config = configManager.getConfig();
        noPermission = ChatColor.translateAlternateColorCodes('&', 
                      config.getString("messages.no_permission", "&cУ вас нет разрешения!"));
        
        cooldownMessage = ChatColor.translateAlternateColorCodes('&', 
                         config.getString("messages.cooldown", "&cПодождите еще {time} сек.!"));
        
        notLinked = ChatColor.translateAlternateColorCodes('&', 
                   config.getString("messages.not_linked", "&cКомпас не привязан к лодстоуну!"));
        
        teleportSuccess = ChatColor.translateAlternateColorCodes('&', 
                         config.getString("messages.teleport_success", "&aТелепортация успешна!"));
    }
    
    public String getNoPermission() {
        return noPermission;
    }
    
    public String getCooldownMessage(long remainingSeconds) {
        return cooldownMessage.replace("{time}", String.valueOf(remainingSeconds));
    }
    
    public String getNotLinked() {
        return notLinked;
    }
    
    public String getTeleportSuccess() {
        return teleportSuccess;
    }
}
Для команды выдачи телекомпаса я реализовал отдельный класс:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class GiveCommand implements ICommand {
    private final TeleCompass plugin;
    
    public GiveCommand(TeleCompass plugin) {
        this.plugin = plugin;
    }
    
    @Override
    public boolean execute(CommandSender sender, String[] args) {
        // Проверка разрешения
        if (!sender.hasPermission("telecompass.give")) {
            sender.sendMessage(plugin.getConfigManager().getMessages().getNoPermission());
            return true;
        }
        
        Player target;
        
        // Определяем получателя
        if (args.length > 0) {
            target = Bukkit.getPlayer(args[0]);
            if (target == null) {
                sender.sendMessage(ChatColor.RED + "Игрок не найден!");
                return true;
            }
        } else if (sender instanceof Player) {
            target = (Player) sender;
        } else {
            sender.sendMessage(ChatColor.RED + "Укажите игрока!");
            return true;
        }
        
        // Выдаем телекомпас
        ItemStack telecompass = TelecompassFactory.create();
        target.getInventory().addItem(telecompass);
        
        sender.sendMessage(ChatColor.GREEN + "Телекомпас выдан игроку " + target.getName() + "!");
        return true;
    }
    
    @Override
    public List<String> tabComplete(CommandSender sender, String[] args) {
        if (args.length == 1) {
            List<String> players = new ArrayList<>();
            for (Player player : Bukkit.getOnlinePlayers()) {
                players.add(player.getName());
            }
            return players;
        }
        return Collections.emptyList();
    }
}
Класс для перезагрузки конфигурации плагина достаточно прост, но демонстрирует использование интерфейса ICommand:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ReloadCommand implements ICommand {
  private final TeleCompass plugin;
  
  public ReloadCommand(TeleCompass plugin) {
      this.plugin = plugin;
  }
  
  @Override
  public boolean execute(CommandSender sender, String[] args) {
      if (!sender.hasPermission("telecompass.reload")) {
          sender.sendMessage(plugin.getConfigManager().getMessages().getNoPermission());
          return true;
      }
      
      plugin.getConfigManager().reload();
      sender.sendMessage(ChatColor.GREEN + "Конфигурация плагина TeleCompass перезагружена!");
      return true;
  }
  
  @Override
  public List<String> tabComplete(CommandSender sender, String[] args) {
      return Collections.emptyList(); // Нет параметров для автодополнения
  }
}
Для обработки крафта телекомпаса я создал отдельный слушатель, который может проверять, имеет ли игрок право на создание этого предмета:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class CraftingListener implements Listener {
  private final TeleCompass plugin;
  
  public CraftingListener(TeleCompass plugin) {
      this.plugin = plugin;
  }
  
  @EventHandler
  public void onCraft(CraftItemEvent event) {
      ItemStack result = event.getRecipe().getResult();
      
      if (TelecompassFactory.isTelecompass(result)) {
          Player player = (Player) event.getWhoClicked();
          
          if (plugin.getConfigManager().isPermissionRequired() && 
              !player.hasPermission("telecompass.craft")) {
              
              event.setCancelled(true);
              player.sendMessage(plugin.getConfigManager().getMessages().getNoPermission());
              player.closeInventory();
          }
      }
  }
}
Файл конфигурации config.yml для плагина содержит все настраиваемые параметры:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[H2]Конфигурация TeleCompass[/H2]
 
# Время перезарядки в секундах
cooldown: 30
 
# Требовать ли разрешение для использования
require_permission: true
 
# Сообщения плагина
messages:
  no_permission: "&cУ вас нет разрешения для этого действия!"
  cooldown: "&cПодождите еще {time} секунд перед использованием!"
  not_linked: "&cЭтот телекомпас не привязан к лодстоуну!"
  teleport_success: "&aВы успешно телепортировались к точке назначения!"
И, разумеется, файл plugin.yml, который определяет метаданные плагина:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
name: TeleCompass
version: 1.0
main: com.myserver.telecompass.TeleCompass
api-version: 1.19
description: Плагин для создания телепортирующих компасов
authors: [YourName]
 
commands:
  telecompass:
    description: Выдать телекомпас игроку
    usage: /telecompass [игрок]
    permission: telecompass.give
  telecompassreload:
    description: Перезагрузить конфигурацию плагина
    usage: /telecompassreload
    permission: telecompass.reload
 
permissions:
  telecompass.use:
    description: Позволяет использовать телекомпас
    default: true
  telecompass.craft:
    description: Позволяет создавать телекомпас
    default: true
  telecompass.give:
    description: Позволяет выдавать телекомпасы другим игрокам
    default: op
  telecompass.reload:
    description: Позволяет перезагружать конфигурацию плагина
    default: op
  telecompass.nocooldown:
    description: Игнорирует время перезарядки для телекомпаса
    default: op
Плагин также требует ресурспак для изменения внешнего вида компаса. Для этого создается структура ресурспака с переопределенными текстурами для телекомпаса:

Java
1
2
3
4
5
6
7
8
resourcepack/
├── pack.mcmeta
├── pack.png
└── assets/
    └── minecraft/
        └── models/
            └── item/
                └── compass.json
Файл compass.json добавляет особую модель для компаса с Custom Model Data равным 1:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
  "parent": "item/generated",
  "textures": {
    "layer0": "item/compass_16"
  },
  "overrides": [
    { "predicate": { "angle": 0.000000 }, "model": "item/compass" },
    
    // ... (остальные стандартные предикаты)
    
    { "predicate": { "angle": 0.984375 }, "model": "item/compass" },
    
    // Наши кастомные предикаты для телекомпаса
    { "predicate": { "angle": 0.000000, "custom_model_data": 1 }, "model": "item/custom/telecompass" },
    { "predicate": { "angle": 0.015625, "custom_model_data": 1 }, "model": "item/custom/telecompass_17" },
    
    // ... (остальные кастомные предикаты)
  ]
}
В случае с большими плагинами я обычно добавляю API, позволяющий другим плагинам взаимодействовать с моим. Для TeleCompass это может выглядеть так:

Java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class TelecompassAPI {
  private static TeleCompass plugin;
  
  public static void setPlugin(TeleCompass teleCompass) {
      plugin = teleCompass;
  }
  
  public static boolean isTelecompass(ItemStack item) {
      return TelecompassFactory.isTelecompass(item);
  }
  
  public static ItemStack createTelecompass() {
      return TelecompassFactory.create();
  }
  
  public static boolean hasCooldown(Player player) {
      return !plugin.getCooldownManager().canUse(player);
  }
  
  public static long getRemainingCooldown(Player player) {
      return plugin.getCooldownManager().getRemainingTime(player);
  }
  
  public static void setCooldown(Player player) {
      plugin.getCooldownManager().setCooldown(player);
  }
}
Такой API позволяет другим разработчикам использовать функциональность TeleCompass без прямого доступа к внутренним классам.

При разработке плагинов всегда стоит думать о совместимости с другими плагинами и о чистоте архитектуры. Это особенно важно в экосистеме Minecraft, где одновременно могут работать десятки разных плагинов.

Модификация для игры Minecraft,нужны люди знающие Java
Мы создаём модификацию для знаменитой игры Minecraft и нам нужны люди знающие Java и готовы...

Неверная логика работы кода (мод для Minecraft)
Помогите найти ошибку в коде, пожалуйста =) Написанно в IDEA (модификация к minecraft). Проблемма...

Написать клиент для сервера minecraft
Доброго времени суток! Мой вопрос связан с игрой minecraft(взял эту игру для примера).Итак,я создал...

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

Компиляция мода для Minecraft
При комплиляции мода Shoppy в IntelliJ IDEA выползает такая ошибка. Пробовал менять настройки...

Как Нотчу удалось за 3 дня сделать Minecraft
Кто знает как Нотчу удалось за 3 дня сделать первую версию моей любимой игры minecraft:...

Minecraft was unable to start because it failed to find an accelerated OpenGL mode
Всем привет!Когда я запускаю minecraft 1.2.5 у меня идёт загруска а потом написано Bad...

Minecraft, "Java, could not create the java virtual machine"
Обычна проблема - &quot;Java, could not create the java virtual machine&quot;, всё что можно было найти в...

Minecraft "Java, could not create the java virtual machine" [2]!
Всё что можно было найти в Google или Яндексе я пробовал. Компьютер: Core i5, 4 Гб оперативки,...

Minecraft "Java, could not create the java virtual machine" [3]!
Вновь поднимаю тему так как столкнулся с той же проблемой, и дело не в пиратском майне, мне java...

Как сделать собственную авторизацию на Minecraft сервере?
Я не знал куда отнести эту тему и решил написать сюда, потому что то что я делаю очень связанно с...

Запуск "MineCraft.jar" - Exception in thread "main"
Что-то с Java приключилось на компе. --------------------------- Java Virtual Machine Launcher...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
JWT аутентификация в ASP.NET Core
UnmanagedCoder 18.06.2025
Разрабатывая веб-приложения, я постоянно сталкиваюсь с дилеммой: как обеспечить надежную аутентификацию пользователей без ущерба для производительности и масштабируемости? Классические подходы на. . .
Краткий курс по С#
aaLeXAA 18.06.2025
Здесь вы найдете все необходимые функции чтоб написать програму на C# Задание 1: КЛАСС FORM 1 public partial class Form1 : Form { Spisok listin = new Spisok(); . . .
50 самых полезных примеров кода Python для частых задач
py-thonny 17.06.2025
Эффективность работы разработчика часто измеряется не количеством написаных строк, а скоростью решения задач. Готовые сниппеты значительно ускоряют разработку, помогают избежать типичных ошибок и. . .
C# и продвинутые приемы работы с БД
stackOverflow 17.06.2025
Каждый . NET разработчик рано или поздно сталкивается с ситуацией, когда привычные методы работы с базами данных превращаются в источник бессонных ночей. Я сам неоднократно попадал в такие ситуации,. . .
Angular: Вопросы и ответы на собеседовании
Reangularity 15.06.2025
Готовишься к техническому интервью по Angular? Я собрал самые распространенные вопросы, с которыми сталкиваются разработчики на собеседованиях в этом году. От базовых концепций до продвинутых. . .
Архитектура Onion в ASP.NET Core MVC
stackOverflow 15.06.2025
Что такое эта "луковая" архитектура? Термин предложил Джеффри Палермо (Jeffrey Palermo) в 2008 году, и с тех пор подход только набирал обороты. Суть проста - представьте себе лук с его. . .
Unity 4D
GameUnited 13.06.2025
Четырехмерное пространство. . . Звучит как что-то из научной фантастики, правда? Однако для меня, как разработчика со стажем в игровой индустрии, четвертое измерение давно перестало быть абстракцией из. . .
SSE (Server-Sent Events) в ASP.NET Core и .NET 10
UnmanagedCoder 13.06.2025
Кажется, Microsoft снова подкинула нам интересную фичу в новой версии фреймворка. Работая с превью . NET 10, я наткнулся на нативную поддержку Server-Sent Events (SSE) в ASP. NET Core Minimal APIs. Эта. . .
С днём независимости России!
Hrethgir 13.06.2025
Решил побеседовать, с утра праздничного дня, с LM о завоеваниях. То что она написала о народе, представителем которого я являюсь сам сначала возмутило меня, но дальше только смешило. Это чисто. . .
Лето вокруг.
kumehtar 13.06.2025
Лето вокруг. Наполненное бурями и ураганами событий. На фоне магии Жизни, священной и вечной, неумелой рукой человека рисуется панорама душевного непокоя. Странные серые краски проникают и. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru