Разработка собственного плагина для сервера Minecraft - Ресурспаки и шейдеры для Minecraft!

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

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

Еще с детства я начал покорять бесконечные просторы Minecraft. Естественно о разработке в то время никакой речи не шло. Но с недавних пор загорелся идеей создать о свой проект серверов.

На Java до этого никогда не писал, но есть бекграунд на других языках, поэтому осталось только приспособиться. Соотвественно разработка плагинов, Bukkit и другие библиотеки вижу впервые, но посмотрев несколько туторов, стала понятна примерная концепция.

Ранее писал на таких языках как PHP, JS. В данный момент веду разработку на языке Go. Сильно привык к “гошке” и его синтаксису и в процессе написания плагина часто использовал синтаксис Go для написания логических конструкций.

Мне не сильно хотелось использовать какие-то готовые решения, ведь тогда не будет углубленных знаний, которые я получу в процессе написания кода. Хочется одновременно и поучить Java и написать что-то свое (самое главное).

В этой статье я не буду затрагивать процесс настройки окружения, установки IDE и стороннего софта.

Идея плагина

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

Прошерстив Google мне удалось найти парочку подходящих плагинов, которые уже решают это проблему. Но один из них, который оказался поддерживаем разработчиком и самими ядром сервера, предоставлял ограниченный функционал, расширенный можно было приобрести на X евро. Фича, которая мне понравилась в платном плагине – создание анимаций из частиц после телепортации из пустоты.

Мне захотелось самому понять, как это сделать, разработать собственный плагин, а потом с удовольствием им пользоваться, поддерживать, находить баги – мое мелкое детище, как никак.

Создаем сам плагин

Назвал я плагин просто – VoidTeleport.

Первым делом создал класс для управления конфигурацией плагина.

public class Config {
    private static File file;
    private static FileConfiguration config;

    private static final String fileNameConfig = "config.yml";

    /**
     * Initializes the static Config class.
     */
    public static void init() {
        // Получаем инстанс нашего плагина.
        Plugin plugin = Bukkit.getServer().getPluginManager().getPlugin(VoidTeleport.PluginName);
        if (plugin == null) {
            // На этом моменте что-то пошло не так, 
          	// нужно обработать и залогировать.
            Bukkit.getLogger().log(
                    Level.WARNING,
                    MessageFormat.format("Cannot get plugin {0}", VoidTeleport.PluginName)
            );
            return;
        }

        file = new File(plugin.getDataFolder(), fileNameConfig);

        // Мы не знаем существует ли файл, поэтому пытаемся создать его.
      	// Если файл уже есть, то выражение file.createNewFile() вернет false. 
        try {
            if (file.createNewFile()) {
                plugin.getLogger().log(
                        Level.INFO,
                        MessageFormat.format("New config file with name {0} was created", fileNameConfig)
                );
            }
        } catch (IOException e) {
            plugin.getLogger().log(Level.SEVERE, e.toString());
            return;
        }

        // На данно моменте наш конфиг пустой, 
      	// поэтому подгружаем его из файла.
        reload();
    }

    /**
     * Getter
     * @return FileConfiguration
     */
    public static FileConfiguration get() {
        return config;
    }

    public static void reload() {
      	// Самый простой анмаршаллер YAML из файла.
        config = YamlConfiguration.loadConfiguration(file);
    }

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

worlds:
    # Наименование мира, например spawn, world, world_the_end
  - name: spawn
  	# Координаты для респавна игрока при падении в пустоту
    spawnLocation:
      x: 0
      y: 0
      z: 0

Конфиг есть, теперь можно приступить к созданию обработчика событий. Мой выбор пал на событие EntityDamageByBlockEvent. Можно было бы и слушать событие PlayerMoveEvent, но оно случается гораздо чаще, чем триггер на получение урона. Лишняя нагрузка на сервер не нужна, поэтому стал слушать урон.

public class PlayerDamageListener implements Listener {
  	// Хеш мапа в которой хранится наименования мира и точка телепортации.
    private HashMap<String, Location> worlds = new HashMap<>();

    @EventHandler
    public void onPlayerDamage(EntityDamageByBlockEvent e) {
        if (!(e.getEntity() instanceof Player)) {
            // Это не игрок.
            return;
        }

        if (e.getCause() != EntityDamageEvent.DamageCause.VOID) {
            // Урон не от пустоты.
            return;
        }

        Player player = (Player) e.getEntity();

      	// Получаем мир, в котором находится Игрок.
        World world = player.getWorld();
      	
      	// Пытаемся найти в хеш мапе значение по наименованию мира.
        Location spawnLocation = this.worlds.get(world.getName());
        if (spawnLocation == null) {
            // К этому миру не действует правило телепорта.
            return;
        }

        // Данный код является костылем, который я быстро сообразил.
      	// Проблема в том, что мир может быть = null.
      	// В таком случае устанавливаем мир на тот, в котором находится игрок.
        if (spawnLocation.getWorld() == null) {
            spawnLocation.setWorld(world);
        }

        // Добрались до самого главного.
      	// Отменяем событие, которое наносит урон игроку.
        e.setCancelled(true);

        // Отменяем сам урон от падения, 
				// чтобы при телепортации игрок не разбился.
        player.setFallDistance(0);

        // Телепортируем игрока.
        player.teleport(spawnLocation);

        // Доабвляем анимацию из частиц при попадании на точку телепортации.
        Spiral.spawn(player);
    }

    @SuppressWarnings("unchecked")
    public void updateWorlds(@Nullable ArrayList<HashMap<String, Object>> listWorlds) {
        if (listWorlds == null) {
          	// Ну если null, так null - ничего не делаем.
            return;
        }

        // Очищаем мапу.
        this.worlds = new HashMap<>();

        for (HashMap<String, Object> world: listWorlds) {
            String worldName = (String) world.get("name");
            if (Objects.equals(worldName, "")) {
                // Тут хорошо бы залогировать, но просто скипаем.
                continue;
            }

            Location spawnLocation = Location.deserialize((Map<String, Object>) world.get("spawnLocation"));
            
          	// Т.к. мир у нас не указан, поэтому получаем его.
          	spawnLocation.setWorld(Bukkit.getWorld(worldName));

            // Сохраняем в хеш мапу.
            this.worlds.put(worldName, spawnLocation);
        }
    }
}

Тепер разберем вызов эффекта анимации при телепортации Spiral.spawn(player). Назвал класс Spiral, потому что эффект будет в виде спирали.

Т.к. это мой первый плагин, то не стал заморачиваться с Пакетами и ProtocolLib.

Описываем анимацию в отдельном классе Spiral. Я предпочел реализовать спираль под названием Helix – достаточно простая в реализации модель. Пришлось немного вспомнить тригонометрию, но у меня получилось!

public class Spiral {
    public static void spawn(@NotNull Player player) {
        Location location = player.getLocation();
				
      	// Задаем радиут спирали.
        double radius = 0.5;
				
        for (double y = 0; y <= 23; y += 0.1) {
            double x = radius * Math.cos(y);
            double z = radius * Math.sin(y);

            Location particleLocation = new Location(location.getWorld(), location.getX(), location.getY(), location.getZ());
            player.spawnParticle(Particle.REDSTONE, particleLocation.add(x, y / 10, z), 2, new Particle.DustOptions(Color.AQUA, 1.0F));

            try {
              	// Думаю, что это плохо, но для первого раза сойдет.
                TimeUnit.NANOSECONDS.sleep(1);
            } catch (InterruptedException e) {
                Bukkit.getLogger().log(Level.SEVERE, e.toString());
            }
        }
    }
}

Почему в коде 23? Это число является ограничением для координаты y. Т.е. по сути спираль будет подниматься вверх на y = 2.3. Как можно заметить, при указании смещения particleLocation.add(x, y / 10, z) y делится на 10. Еще одной причиной стало то, что спираль не успевает несколько раз “обернуть” игрока.

Собираем все вместе

Наконец можем собрать наш код в единой точке и протестировать, что получилось.

public final class VoidTeleport extends JavaPlugin {
    public static final String PluginName = "VoidTeleport";

    @Override
    public void onEnable() {
        getLogger().log(Level.INFO, "Plugin enabled!");

        // Инициализируем конфиг
        Config.init();

      	// Регистрируем обработчик событий для входщего урона
        this.registerDamageEvent();
    }

    @Override
    public void onDisable() {
        getLogger().log(Level.INFO, "Plugin disabled!");
    }

    @SuppressWarnings("unchecked")
    private void registerDamageEvent() {
      	// Инициализируем обработчик
        PlayerDamageListener damageListener = new PlayerDamageListener();
      
      	// Достаем из конфига нужные значения и обновляем хеш мапу в обработчике
        damageListener.updateWorlds((ArrayList<HashMap<String, Object>>) Config.get().get("worlds"));
				
      	// Регистрируем новое событие на сервере
        getServer().getPluginManager().registerEvents(damageListener, this);
    }
}

Результат

При заданным настройкам файле конфигурации мы успешно попадаем в указанную точки и наблюдаем просто классную анимацию, как по мне.

И без указания мира в конфиге.