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

Руководство НПС с помощью ProtocolLib

Поддерживаемые версии
  1. 1.19
Предисловие

(Данный руководство является моим личным "творением" и был написан на англоязычный форум. Я решил перевести его на русский язык и поделиться с ним с моими собратьями из СНГ.)

[Протестировано и работает на spigot-1.19.4 и ProtocolLib 5.1.0]

Недавно я начал учиться работать с ProtocolLib. И первое, что я хотел сделать, это npc. Хорошего руководства я не нашел, использую информацию из вики и что-то с форума. Именно поэтому я делаю этот ресурс.

Как мы можем создать простого NPC
Чтобы создать NPC, нам нужно отправить два пакета: пакет с информацией об игроке и именованный объект. Сначала мы отправляем пакет с информацией об игроке, потому что, если мы не отправим этот пакет первым, NPC не появится.

Пакет информации об игроке содержит информацию об игровом профиле игрока, задержке, игровом режиме и компоненте чата.
Насколько нам известно, игровой профиль игрока содержит отображаемое имя игрока, uuid и скин.

Давайте начнем отправлять пакет с информацией об игроке.

(Извините, если я упустил что-то важное или не ясно выразился, напишите мне и я все исправлю при необходимости)


Java:
public class EntityInfoUpdate {

    private ProtocolManager protocolManager;
    private UUID uuid;

    public EntityInfoUpdate(
            ProtocolManager protocolManager,
            UUID uuid) {
        this.protocolManager = protocolManager;
        this.uuid = uuid;
    }

    public void playerInfoUpdate(Player player) {

        PacketContainer npc = protocolManager.createPacket(PacketType.Play.Server.PLAYER_INFO); // Создание пакет PLAYER_INFO
        Set<EnumWrappers.PlayerInfoAction> playerInfoActionSet = new HashSet<>(); //Коллекция наборов, содержащая действие с информацией об игроке, очень важно

        /*
         Создайте новый пользовательский игровой профиль для нашего NPC.
         В первых аргументах укажите уникальный uuid, сгенерированный с помощью UUID.randomUUID();
         Во втором аргументе поместите имя NPC над головой.
          */
        WrappedGameProfile wrappedGameProfile = new WrappedGameProfile(uuid, "NPC");

        /*
         Создайте новое свойство скина игрока.

         В первом оставь как есть и не трогай, иначе ничего не работает.
         Во втором и третьем укажите те данные, которые получите следующим способом:

         Перейдите на https://minecraftuuid.com/ и найдите нужный скин.
         Если вы нашли скин, скопируйте его UUID игрока.

         Введите поиск в браузере, но не нажимайте Enter: вам нужно будет отредактировать этот URL-адрес;
         https://sessionserver.mojang.com/session/minecraft/profile/uuid?unsigned=false

         Удалите слово «uuid» и вставьте скопированный вами uuid игрока.
         И теперь вам нужно нажать Enter и вы получите результат

         Скопируйте значение и вставьте во вторые аргументы конструктора.
         Скопируйте подпись и вставьте ее в аргументы третьего конструктора.

          */
        WrappedSignedProperty property = new WrappedSignedProperty(
                "textures",
                "ewogICJ0aW1lc3RhbXAiIDogMTcwOTEzODQ2ODQwOSwKICAicHJvZmlsZUlkIiA6ICIwNjlhNzlmNDQ0ZTk0NzI2YTViZWZjYTkwZTM4YWFmNSIsCiAgInByb2ZpbGVOYW1lIiA6ICJOb3RjaCIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8yOTIwMDlhNDkyNWI1OGYwMmM3N2RhZGMzZWNlZjA3ZWE0Yzc0NzJmNjRlMGZkYzMyY2U1NTIyNDg5MzYyNjgwIgogICAgfQogIH0KfQ==",
                "ESOeT3Fo19IDBmQS231ZSm3XKCq5SVEbu74LArJoi9Ker1RXDsP3XV8mPgfeWCmm89v2RXLp7bcmmCtJI6JgKPZb3wCasSaM1SxwJ4WEaoByQvJ0FhRyeG126d9GEujQgOxmjErvKGLOKgRbF3ORoroSua8csiNn8+ADSyYrUud0lzczOMD9hx+qov+wSYb+zJ/9bR4aXFAnLxMbEAOYOyJCSJ+ZE4KQmvebgyNGHLSmC/Vm9zZIFAZrg62wV3SXBAuWGXv46jDPlaoimZv1/zmOMDj8dGwQ89WWR1zcR8ZBn9Ysgi+yfTvR3w/AHeuMa2IdQKxXw8QJl0kZp0c36RIIfKB25KcbED2HReuA/Nmg5S+HXju9mrK+M4YDvz34oRgBvnVjUTWtg6b8eW3OtaKlIkEgjlUGPZYU4OhoeoQaEylcjISyT4nS36iGumSSgXAW75GuiPI4b8KZ5iy0yXlWWSLTJr0axShFuvuv4Vd70qufnusnEGceMyOXBUCC5lcJ06RrIwUNdLWkwGh8OWdjXffvyhyAAbrq8oFNKxRzjf8E6XEAvClf5L6BSH8kd7OwxpEFzUMiJNHZ9tQ9j8EwsYv3cLUNhBV/yf2O96AqDmyCD3wTblQj8k4pJRyi+/Kj1476yVOfuc3LJ4jYcDX2pJv3LemUxLJ+JDMdJh0="        );

        //Теперь нам нужно добавить данные о скине нашего игрового профиля.
        wrappedGameProfile.getProperties().clear();
        wrappedGameProfile.getProperties()
                .put("textures", property);

        /*
         В первый аргумент конструктора помещаем наш игровой профиль
         Во втором аргументе конструктора указывается ваша задержка, которая вам нужна. (речь о пинге, если не ошибаюсь)
         В третий аргумент конструктора поставил режим игры нпс, я поставил креатив.
         На тему четвёртого аргумента, я не очень то понял:
         Я не вру и говорю правду – я не знаю, что это такое. Я думаю, это как-то связано с чатом,
         возможно, это имя игрока, которое отображается, если игрок что-то пишет в чате. Я не знаю, извините.
          */
        PlayerInfoData playerInfoData = new PlayerInfoData(
                wrappedGameProfile,
                0,
                EnumWrappers.NativeGameMode.CREATIVE,
                WrappedChatComponent.fromText("name"));


        List<PlayerInfoData> playerInfoDataList = Arrays.asList(playerInfoData); //Добавляем информацию об игроке в список, как здесь.

        playerInfoActionSet.add(EnumWrappers.PlayerInfoAction.ADD_PLAYER); //Добавляет действия игрока, которые должен выполнить сервер, в нашем случае это добавление игрока

        npc.getPlayerInfoActions()
                .write(0, playerInfoActionSet); //Добавляем действие игрока в наш пакет

        npc.getPlayerInfoDataLists().write(1, playerInfoDataList); //Добавляем список данных об игроке в пакет

        /*
         В аргументах первого метода нам нужно указать игрока, которому отправляется пакет.
         В аргументах второго метода нам нужно поместить отправляемый пакет.
        */
        protocolManager.sendServerPacket(player, npc);
    }

}

Создание нашего НПС

Теперь нам нужно создать метод для создания NPC.

Создаём новый класс, который будет отвечать за спавн нашего НПС.
Java:
public class EntitySpawn {
 
    private ProtocolManager protocolManager;
    private UUID uuid;

    public EntitySpawn(
            ProtocolManager protocolManager,
            UUID uuid) {
        this.protocolManager = protocolManager;
        this.uuid = uuid;
    }

    public void spawnEntity(Player player, Location location) {

        /*
        Создание пакет спавна нпс.
         */
        PacketContainer npc = protocolManager.createPacket(PacketType.Play.Server.NAMED_ENTITY_SPAWN);


        npc.getIntegers()
                .write(0, -1) //Указываем Entity Id
                .writeSafely(1, 122); //Id нашего нпс (игрок в 1.19.4 имеет идентификатор 122, получен с вики ProtocolLib)

        npc.getUUIDs()
                .write(0, uuid); //UUID должен быть такой же, как и тот который мы указывали в PLAYER_INFO

        npc.getEntityTypeModifier()
                .writeSafely(0, EntityType.PLAYER); //Указываем, что нам нужен именно НПС!

        npc.getDoubles()
                .write(0, location.getX())
                .write(1, location.getY())
                .write(2, location.getZ()); //Координаты в которых будет спавнится НПС

        npc.getBytes()
                .write(0, (byte) (0))
                .write(1, (byte) (0)); //Направление в которое смотрит НПС

        /*
         В аргументах первого метода нам нужно указать игрока, которому отправляется пакет.
         В аргументах второго метода нам нужно поместить отправляемый пакет.
          */
        protocolManager.sendServerPacket(player, npc);
    }

}

Укладываем весь процесс создания НПС в один единый класс, для удобства.

Java:
public class NPC {

    private ProtocolManager protocolManager;
    private UUID uuid;

    public NPC(
            ProtocolManager protocolManager,
            UUID uuid) {
        this.protocolManager = protocolManager;
        this.uuid = uuid;
    }

    public void spawn(Player player, Location location) {
        EntityInfoUpdate updateInfo = new EntityInfoUpdate(protocolManager, uuid);
        EntitySpawn spawnEntity = new EntitySpawn(protocolManager, uuid);

        updateInfo.playerInfoUpdate(player);
        spawnEntity.spawnEntity(player, location);
    }

}

Спавним НПС, когда игрок заходит на сервер:
Java:
public class PlayerJoin implements Listener {

    @EventHandler
    public void onJoin(PlayerJoinEvent event) {

        NPC npc = new NPC(
                ProtocolLibrary.getProtocolManager(),
                UUID.randomUUID());

        Location location = new Location(Bukkit.getServer().getWorld("world"), -1.0, 2.0, 0.0);

        npc.spawn(event.getPlayer(), location);

    }

}

НПС всегда смотрит на вас

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

Java:
public class EntityHeadAndBodyRotationUpdate {

    private ProtocolManager protocolManager;

    public EntityHeadAndBodyRotationUpdate(
            ProtocolManager protocolManager) {
        this.protocolManager = protocolManager;
    }

    public void updateRotation(Player player, Location location) {
        /*
         Как можно понять по названию пакета,
          первый пакет отвечает за вращение головы,
           второй пакет отвечает за вращение тела.
          */
        PacketContainer rotateHead = protocolManager.createPacket(PacketType.Play.Server.ENTITY_HEAD_ROTATION);
        PacketContainer rotateBody = protocolManager.createPacket(PacketType.Play.Server.ENTITY_LOOK);

        rotateHead.getIntegers()
                .write(0, -1); //Указываем EntityId игрока, который указывали ранее.
        rotateBody.getIntegers()
                .write(0, -1); //Указываем EntityId игрока, который указывали ранее.
        location.setDirection(player.getLocation().subtract(location).toVector()); //Устанавливаем ветро между игроком и НПС, да бы Нпс всегда смотрел на игрока

        float yaw = location.getYaw();
        float pitch = location.getPitch();

        rotateHead.getBytes()
                .write(0, (byte) ((yaw % 360) * 256 / 360)); //Направление головы
        rotateBody.getBytes()
                .write(0, (byte) ((yaw % 360) * 256 / 360)) //Направление головы
                .write(1, (byte) ((pitch % 360) * 256 / 360)); //Направление тела игрока

        /*
         В аргументах первого метода нам нужно указать игрока, которому отправляется пакет.
         В аргументах второго метода нам нужно поместить отправляемый пакет.
          */
        protocolManager.sendServerPacket(player, rotateHead);
        protocolManager.sendServerPacket(player, rotateBody);
    }

}

Но ясное дело, это не всё. Нам необходимо доделать начатое в PlayerMoveEvent.

Java:
public class PlayerMoving implements Listener {

    private ProtocolManager protocolManager;
    private Location location;
    private UUID uuid;

    public PlayerMoving(
            ProtocolManager protocolManager,
            Location location,
            UUID uuid) {
        this.protocolManager = protocolManager;
        this.location = location;
        this.uuid = uuid;
    }

    @EventHandler
    public void onMove(PlayerMoveEvent event) {
        Player player = event.getPlayer();
        EntityHeadAndBodyRotationUpdate updateHeadAndBodyRotation = new EntityHeadAndBodyRotationUpdate(protocolManager);

        /*
        Чтобы не создавать дикую нагрузку на сервере, мы сделаем так, чтобы НПС смотрел на игрока только если тот на расстоянии 5 и меньше блоков.
        Представьте, если мы это не сделаем и на вашем сервере 100 игроков, НПС всегда будет смотреть на каждого игрока, даже если тот находится на 1000 блоков. Данный эффект не будет виден игроку, но будет виден в качестве нагрузки на сервере. :) Нам оно не надо.
         */
        if (player.getLocation().distance(location) <= 5) {
            updateHeadAndBodyRotation.updateRotation(player, location);
        }

    }

}

Нажатие на НПС

А что если, мы хотим иметь возможность нажать на нашего игрока и запускать к примеру SkyWars или открывать игроку GUI? Сейчас покажу, как это сделать.

В данном классе, мы создаём прослушиватель, который отслеживает все взаимодействия игрока с сущностью.

Java:
public class NPCClickelableEvent {

    private Protocolliblearn protocolliblearn;
    private ProtocolManager protocolManager;

    public NPCClickelableEvent(
            Protocolliblearn protocolliblearn,
            ProtocolManager protocolManager) {
        this.protocolliblearn = protocolliblearn;
        this.protocolManager = protocolManager;
    }

    public void registerEvent() {
        protocolManager.addPacketListener(
                new PacketAdapter(
                        protocolliblearn,
                        ListenerPriority.NORMAL,
                        PacketType.Play.Client.USE_ENTITY) {
            @Override
            public void onPacketReceiving(PacketEvent packetEvent) {

                Player player = packetEvent.getPlayer();
                PacketContainer packetContainer = packetEvent.getPacket();
                EnumWrappers.EntityUseAction action = packetContainer.getEnumEntityUseActions().read(0).getAction();

                int entityId = packetContainer.getIntegers().read(0);

                if (entityId == -1 && action == EnumWrappers.EntityUseAction.ATTACK) {
                    player.sendMessage("Fuck You");
                }
            }
        });
    }

}

На всякий, дам вам так же свой основной Main.class и весь проект который я залил на Github.

Java:
public final class Protocolliblearn extends JavaPlugin {

    @Override
    public void onEnable() {

        getServer().getPluginManager().registerEvents(new PlayerJoin(), this);

        NPCClickelableEvent npcClickelableEvent = new NPCClickelableEvent(this, ProtocolLibrary.getProtocolManager());

        npcClickelableEvent.registerEvent();

        UUID uuid = UUID.randomUUID();

        Bukkit.getPluginManager().registerEvents(new PlayerMoving(
                        ProtocolLibrary.getProtocolManager(),
                        new Location(
                                Bukkit.getServer().getWorld("world"),
                                -1.0,
                                2.0,
                                0.0),
                        uuid
                        ), this);

    }

    @Override
    public void onDisable() {
        // Plugin shutdown logic
    }
}

Я надеюсь, что объяснил всё понятно и ничего не пропустил. Я уверен, что кому-то данный ресурс среди русскоязычной аудитории Spigot будет полезен. В случае, если я допустил какие-то ошибки или оплошности - пишите, я всё исправлю и помогу в вашей проблеме. Спасибо за внимание! (Админам/модерам - пожизненный запас чая и тортиков)

Автор
Maksim1251
Просмотры
2 515
Первый выпуск
Обновление
Оценка
5.00 звёзд 3 оценок

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

Последние рецензии

Хороший гайд в условиях дефицита информации, автор молодец
Очень хороший гайд, будет полезно тем, кто хочет полазить в новых дебрях
Полезно!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
M
Maksim1251
Спасибо за отзыв! Для меня это очень много значит. :)
Назад
Сверху Снизу