Иконка ресурса

Руководство Создание NPC на PacketEvents для маслят

Поддерживаемые версии
  1. 1.20
Вступление

В данном руководстве мы рассмотрим базовое создание NPC с использованием библиотеки PacketEvents.
Код был написан на Java 17 с использованием PE 2.3.0 и протестирован на сервере 1.20.4.

Как создать простого NPC?

Благо разработчики PacketEvents уже позаботились об этом, предоставив класс NPC, в котором реализована значительная часть необходимой логики.
Создадим обработчик команд в котором будем триггерить спавн NPC

Java:
public record NPCData(NPC npc, int entityId, String command, World world) {}
Java:
public class NPCCommand implements CommandExecutor, TabCompleter {
    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
        if (args.length == 0 || !(sender instanceof Player player)) {
            return false;
        }
        switch (args[0].toLowerCase()) {
            case "create":
                handleCreate(player, args);
                break;
            case "destroy":
                if (args.length > 1) {
                    try {
                        int entityId = Integer.parseInt(args[1]);
                        handleDestroy(player, entityId);
                    } catch (NumberFormatException e) {
                        player.sendMessage("Укажите валидный entityID.");
                    }
                } else {
                    player.sendMessage("Укажите entityID.");
                }
                break;
            default:
                return true;
        }
        return true;
    }

    private void handleCreate(Player player, String[] args) {
        int entityId = SpigotReflectionUtil.generateEntityId();
        UUID entityUUID = UUID.randomUUID();

        NPC npc = createNpc(player, entityUUID, entityId);
        String Command = (args.length > 1 && !args[1].isEmpty()) ? args[1] : "paper version"; // Устанавливаем дефолтную команду, если игрок не предоставил свою
        playerNPCs.computeIfAbsent(player.getUniqueId(), k -> new ArrayList<>()).add(new NPCData(npc, entityId, Command, player.getWorld()));
    
        // Отобразим NPC всем игрокам на сервере
        for (Player p : Bukkit.getOnlinePlayers()) {
            com.github.retrooper.packetevents.protocol.player.User user = PacketEvents.getAPI().getPlayerManager().getUser(p);
            npc.spawn(user.getChannel());
        }

        player.sendMessage("Создан NPC с EntityID: " + entityId + ".");
    }

    /**
     * Данный метод установит все необходимые свойства нашему NPC
     */
    private static @NotNull NPC createNpc(Player player, UUID entityUUID, int entityId) {
        NPC npc = new NPC(
                new UserProfile(entityUUID, player.getName() + "L"), // Фейковый профиль
                entityId,
                GameMode.SURVIVAL, // Игровой режим NPC
                null, // Имя в табе
                NamedTextColor.WHITE, // Цвет ника
                null, // Префикс
                null // Суффикс
        );
        npc.setLocation(new Location(
                player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
                player.getLocation().getYaw(), player.getLocation().getPitch()
        ));
        return npc;   }

Удаление NPC

С созданием разобрались, а как удалить?
Для этого добавим в наш класс NPCCommand еще два метода, handleDestroy
который будет обрабатывать удаление NPC по его EntityID и onTabComplete для обработки табкомплита наших команд
Java:
public class NPCCommand implements CommandExecutor, TabCompleter {
    @Override
    public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
        if (args.length == 0 || !(sender instanceof Player player)) {
            return false;
        }
        switch (args[0].toLowerCase()) {
            case "create":
                handleCreate(player, args);
                break;
            case "destroy":
                if (args.length > 1) {
                    try {
                        int entityId = Integer.parseInt(args[1]);
                        handleDestroy(player, entityId);
                    } catch (NumberFormatException e) {
                        player.sendMessage("Укажите валидный entityID.");
                    }
                } else {
                    player.sendMessage("Укажите entityID.");
                }
                break;
            default:
                return true;
        }
        return true;
    }

    private void handleCreate(Player player, String[] args) {
        int entityId = SpigotReflectionUtil.generateEntityId();
        UUID entityUUID = UUID.randomUUID();

        NPC npc = createNpc(player, entityUUID, entityId);
        String Command = (args.length > 1 && !args[1].isEmpty()) ? args[1] : "paper version"; // Устанавливаем дефолтную команду, если игрок не предоставил свою
        playerNPCs.computeIfAbsent(player.getUniqueId(), k -> new ArrayList<>()).add(new NPCData(npc, entityId, Command, player.getWorld()));

        // Отобразим NPC всем игрокам на сервере
        for (Player p : Bukkit.getOnlinePlayers()) {
            com.github.retrooper.packetevents.protocol.player.User user = PacketEvents.getAPI().getPlayerManager().getUser(p);
            npc.spawn(user.getChannel());
        }

        player.sendMessage("Создан NPC с EntityID: " + entityId + ".");
    }

    /**
     * Данный метод установит все необходимые свойства нашему NPC
     */
    private static @NotNull NPC createNpc(Player player, UUID entityUUID, int entityId) {
        NPC npc = new NPC(
                new UserProfile(entityUUID, player.getName() + "L"), // Фейковый профиль
                entityId,
                GameMode.SURVIVAL, // Игровой режим NPC
                null, // Имя в табе
                NamedTextColor.WHITE, // Цвет ника
                null, // Префикс
                null // Суффикс
        );
        npc.setLocation(new Location(
                player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
                player.getLocation().getYaw(), player.getLocation().getPitch()
        ));
        return npc;
    }

    private void handleDestroy(Player player, int entityId) {
        UUID playerUUID = player.getUniqueId();
        List<NPCData> npcs = playerNPCs.get(playerUUID);
        if (npcs == null) {
            return;
        }

        Iterator<NPCData> iterator = npcs.iterator();
        while (iterator.hasNext()) {
            NPCData npcData = iterator.next();
            if (npcData.entityId() == entityId) {
                com.github.retrooper.packetevents.protocol.player.User user = PacketEvents.getAPI().getPlayerManager().getUser(player);
                npcData.npc().despawn(user.getChannel());
                iterator.remove();
                player.sendMessage("NPC с EntityID: " + entityId + " уничтожено :}");
                return;
            }
        }

        player.sendMessage("NPC с EntityID " + entityId + " не найдено :{");
    }

    @Override
    public List<String> onTabComplete(@NotNull CommandSender sender, Command command, @NotNull String alias, String[] args) {
        if (command.getName().equalsIgnoreCase("npc")) {
            if (args.length == 1) {
                return Arrays.asList("create", "destroy");
            } else if (args.length == 2 && args[0].equalsIgnoreCase("destroy") && sender instanceof Player player) {
                UUID playerUUID = player.getUniqueId();
                List<NPCData> npcs = playerNPCs.get(playerUUID);
                if (npcs != null) {
                    // Инициализируем массив для хранения всех NPC EntityID игрока
                    List<String> entityIds = new ArrayList<>();
                    for (NPCData npcData : npcs) {
                        entityIds.add(String.valueOf(npcData.entityId()));
                    }
                    return entityIds;
                }
            }
        }
        return new ArrayList<>();
    }
}

Обработка нажатий

Для этого просто создадим пакетный слушатель в котором будем отслеживать получение пакета INTERACT_ENTITY
Java:
public class PacketInteractEntity extends PacketListenerAbstract {
    /**
     * Вызывается когда клиент отправляет INTERACT_ENTITY пакет
     */
    @Override
    public void onPacketReceive(PacketReceiveEvent event) {
        if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
            WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
            Player player = (Player) SpigotReflectionUtil.getCraftPlayer((Player) event.getPlayer()); // Получаем Bukkit Player`а
            int clickedEntityId = packet.getEntityId(); // Получаем EntityID сущности на которую кликнул игрок

            for (List<NPCData> npcList : playerNPCs.values()) {
                for (NPCData npcData : npcList) {
                    if (npcData.entityId() == clickedEntityId) { // Проверяем, кликнул ли игрок на нужную сущность
                        Bukkit.getScheduler().runTask(
                                NPCExample.instance,
                                () -> player.performCommand(npcData.command())); // С помощью планировщика Bukkit запускаем задачу в основном потоке через тик
                        return;
                    }
                }
            }
        }
    }
}

Поворот тела и головы NPC, отображение NPC для новых игроков

Для этого создадим новый класс, который будет слушать PlayerMove и PlayerJoin ивенты.
В PlayerJoinEvent`е мы будем спавнить NPC для каждого вошедшего игрока, а в PlayerMoveEvent`е мы будем отправлять игроку пакеты о том, как тело и голова NPC должны поворачиваться и перемещаться в зависимости от движения игрока.
Java:
public class PlayerEventListener implements Listener {
    @EventHandler
    public void onPlayerJoin(PlayerJoinEvent event) {
        for (Map.Entry<UUID, List<NPCData>> entry : playerNPCs.entrySet()) {
            List<NPCData> npcList = entry.getValue();

            for (NPCData npcData : npcList) {
                npcData.npc().spawn(getUser(event.getPlayer()).getChannel()); // Спавним NPC для каждого нового игрока
            }
        }
    }

    @EventHandler
    public void onPlayerMove(PlayerMoveEvent event) {
        for (Map.Entry<UUID, List<NPCData>> entry : playerNPCs.entrySet()) {
            List<NPCData> npcList = entry.getValue();

            for (NPCData npcData : npcList) {
                if (npcData.world() != event.getPlayer().getWorld()) return;
                // Конвертируем местоположение NPC в местоположение Bukkit
                org.bukkit.Location convertedNPCLocation = SpigotConversionUtil.toBukkitLocation(npcData.world(), npcData.npc().getLocation());
                float calculatedYaw = calculateYaw(event.getPlayer().getLocation(), convertedNPCLocation);
                // Создаем пакет для поворота головы
                WrapperPlayServerEntityHeadLook HeadLookPacket = new WrapperPlayServerEntityHeadLook(npcData.entityId(), calculatedYaw);
                // Создаем пакет для поворота тела
                WrapperPlayServerEntityRelativeMoveAndRotation RotationMovePacket = prepareRelativeMoveAndRotationPacket(npcData, convertedNPCLocation, calculatedYaw);
                // Отправляем пакеты игроку
                PacketEvents.getAPI().getPlayerManager().sendPacket(event.getPlayer(), RotationMovePacket);
                PacketEvents.getAPI().getPlayerManager().sendPacket(event.getPlayer(), HeadLookPacket);
            }
        }
    }

    private static @NotNull WrapperPlayServerEntityRelativeMoveAndRotation prepareRelativeMoveAndRotationPacket(NPCData npcData, Location convertedNPCLocation, float calculatedYaw) {
        double deltaX = (npcData.npc().getLocation().getX() - convertedNPCLocation.getX()) * 32 * 128;
        double deltaY = (npcData.npc().getLocation().getY() - convertedNPCLocation.getY()) * 32 * 128;
        double deltaZ = (npcData.npc().getLocation().getZ() - convertedNPCLocation.getZ()) * 32 * 128;
        return new WrapperPlayServerEntityRelativeMoveAndRotation(
                npcData.entityId(),
                (short) deltaX,
                (short) deltaY,
                (short) deltaZ,
                calculatedYaw, npcData.npc().getLocation().getPitch(), true);
    }

    private static User getUser(Player player) {
        return PacketEvents.getAPI().getPlayerManager().getUser(player);
    }

    /**
     * Вычисляет yaw в градусах от местоположения игрока к местоположению NPC.
     */
    private static float calculateYaw(org.bukkit.Location playerLocation, org.bukkit.Location npcLocation) {
        double deltaX = playerLocation.getX() - npcLocation.getX();
        double deltaZ = playerLocation.getZ() - npcLocation.getZ();
        return (float) Math.toDegrees(Math.atan2(deltaZ, deltaX)) - 90;
    }
}
Заключение
Java:
public class NPCExample extends JavaPlugin {
    public static final HashMap<UUID, List<NPCData>> playerNPCs = new HashMap<>();


    @Override
    public void onLoad() {
        instance = this;
        PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this));
        PacketEvents.getAPI().getSettings().reEncodeByDefault(false)
                .checkForUpdates(true)
                .bStats(true);
        PacketEvents.getAPI().load();
    }

    @Override
    public void onEnable() {
        PacketEvents.getAPI().init();
        PacketEvents.getAPI().getEventManager().registerListener(new PacketInteractEntity());
        Objects.requireNonNull(getCommand("npc")).setExecutor(new NPCCommand());
        getServer().getPluginManager().registerEvents(new PlayerEventListener(), this);
    }

    public static NPCExample instance;
}
Если я допустил какие либо ошибки или неточности - пишите мне, я всё исправлю
  • Мне нравится (+1)
Реакции: Kelsi
Автор
pink
Просмотры
966
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок

Поделиться ресурсом

Назад
Сверху Снизу