Добавление новых свойств и инструментов

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

Регистрация тегов

Теги, которые не принадлежат свойствам, регистрируются в определении класса типа объекта — конкретнее, в статическом методе registerTags. Загляните в класс PlayerTag, расположенный в пакете objects.

У каждого типа тегов есть свой tag processor (обработчик тегов), объявленный статическим членом класса типа как tagProcessor. Чтобы зарегистрировать тег, нужно вызвать у этого объекта метод registerTag. Метод принимает три параметра: класс возвращаемого типа, имя тега и лямбда-выражение, у которого тоже два параметра.

tagProcessor.registerTag(ElementTag.class, "tag_name", (attribute, object) -> {
    return new ElementTag("Hello, world!");
});

Давайте разберём лямбду подробнее. Параметр attribute — это экземпляр класса Attribute: универсальный служебный объект, который содержит всю информацию об обрабатываемом теге и набор вспомогательных методов. Например, получить параметр тега можно через attribute.getParam(). Если что-то пошло не так — вызывайте attribute.echoError("...") и возвращайте null.

Параметр object — это просто экземпляр текущего класса. То есть в классе PlayerTag в object будет лежать PlayerTag.

Воспользуемся этим и сделаем тег uuid_uppercase, который возвращает UUID игрока, переведённый в верхний регистр. У него нет реального применения — он нужен исключительно для примера и практики. Если бы у него было настоящее применение, автор этой страницы уже давно внёс бы его в проект.

Мы хотим вернуть одну строку, поэтому возвращаемым типом будет ElementTag. Кроме того, у object можно вызвать метод getUUID(), чтобы получить UUID игрока. UUID — стандартный Java-утилитный класс с методом toString(), так что можно вызвать его, а затем toUpperCase() — и получим то, что нужно.

У класса ElementTag много конструкторов, в том числе принимающий одну String — его и возьмём.

tagProcessor.registerTag(ElementTag.class, "uuid_uppercase", (attribute, object) -> {
    return new ElementTag(object.getUUID().toString().toUpperCase());
});

Вставьте это в метод registerTags, соберите Denizen и положите jar-файл в папку plugins. Запустите сервер и выполните в игре /ex narrate <player.uuid_uppercase> — в чате отобразится ваш UUID в верхнем регистре.

../../_images/uuid_uppercase.png

Получение ввода в теге

Пойдём дальше и ещё немного «поиздеваемся» над UUID игрока. Подправим наш тег uuid_uppercase так, чтобы он принимал булевый ввод: нужно ли повторить UUID ещё раз через пробел.

Через параметр attribute можно проверить, есть ли у тега ввод, вызвав hasParam(). Если он есть — получить его как элемент через getParamElement(). У класса ElementTag есть несколько методов, возвращающих разные примитивы в зависимости от внутреннего значения; булевый ввод мы получим, вызвав asBoolean().

Эту логику можно уложить в один if:

if (attribute.hasParam() && attribute.getParamElement().asBoolean()) {
    // ...
}

Затем вынесем строку UUID в переменную и, если условие сработает, модифицируем её (конкатенации достаточно). После этого просто вернём её как элемент, как и раньше.

tagProcessor.registerTag(ElementTag.class, "uuid_uppercase", (attribute, object) -> {
    String uuid = object.getUUID().toString().toUpperCase();
    if (attribute.hasParam() && attribute.getParamElement().asBoolean()) {
        uuid = uuid + " " + uuid;
    }
    return new ElementTag(uuid);
});

В таком виде ввод у тега необязательный — [] можно даже не писать. Но если передать true, тег вернёт два UUID в верхнем регистре через пробел.

Документирование тегов: мета-комментарии

Тег готов и работает, но не хватает ещё одной вещи — документации! Если вам когда-нибудь было интересно, как работает сайт мета-документации: он просматривает весь код Denizen и находит особые комментарии, которые затем разбирает. Мы называем это мета-документацией, потому что она существует рядом с фичей, но не как её непосредственная часть. Для тегов она выглядит так:

// <--[tag]
// @attribute <ObjectTag.tag_name>
// @returns ObjectTag
// @mechanism ObjectTag.mech_name
// @description
// This is the description of the tag.
// -->

Заметьте, что «attribute» — это просто другое название для тега. Имейте в виду, что в этом блоке нужно отразить ровно тот вид, в котором тег должен использоваться, поэтому если тег принимает ввод — обязательно укажите это. Ключ @mechanism необязателен; если он есть, значит указанный механизм — прямой аналог этого тега.

Ключ @returns указывает тип возвращаемого значения. В нашем случае это просто ElementTag, потому что мы возвращаем строку текста. Но иногда нужно уточнить: ElementTag может содержать логическое значение, целое число или десятичную дробь. В таких случаях конкретный тип указывается в скобках: ElementTag(Boolean), ElementTag(Number) и ElementTag(Decimal) соответственно. То же правило действует и для ListTag: например, для списка локаций тип будет записан как ListTag(LocationTag).

Мета-комментарий располагается прямо над кодом тега. Вот как это выглядит для нашего тега:

// <--[tag]
// @attribute <PlayerTag.uuid_uppercase[(<repeat>)]>
// @returns ElementTag
// @description
// Returns the UUID of the player in uppercase.
// Optionally specify whether the UUID should be repeated once.
// -->
tagProcessor.registerTag(ElementTag.class, "uuid_uppercase", (attribute, object) -> {
    String uuid = object.getUUID().toString().toUpperCase();
    if (attribute.hasParam() && attribute.getParamElement().asBoolean()) {
        uuid = uuid + " " + uuid;
    }
    return new ElementTag(uuid);
});

Имейте в виду: для вашей IDE мета-документация — это просто текстовый комментарий, но на деле это очень строгий формат, и его нужно соблюдать аккуратно. Дело в том, что несколько сервисов автоматически разбирают эти метаданные и используют их. Например, если в примере выше забыть () вокруг <repeat>, редактор скриптов покажет ошибку для любого скрипта, который попробует использовать тег без параметра.

Создание механизмов

ПРИМЕЧАНИЕ: раздел может измениться в будущем, когда механизмы переведут на систему регистрации, аналогичную системе регистрации тегов.

В отличие от современных тегов, механизмы проходят серию проверок, чтобы «совпасть» с обрабатываемым механизмом. Это происходит в методе adjust определения класса, который принимает параметр mechanism. У механизмов есть набор методов для сопоставления и проверки ввода — мы их скоро увидим.

Чтобы сопоставить механизм, вызовите метод matches(String) и передайте имя механизма. Чтобы потребовать ввод определённого типа, используйте методы, начинающиеся с require: например, requireBoolean() или requireObject(Object). Оберните их в if, чтобы открыть блок механизма.

if (mechanism.matches("some_mechanism") && mechanism.requireBoolean()) {
    // do stuff
}

Сделаем механизм для ItemTag под названием wrap_brackets, принимающий целое число. Он будет оборачивать отображаемое имя предмета в скобки, между которыми вставлено указанное количество пробелов. Для ввода возьмём метод requireInteger().

У класса ItemTag есть поле-экземпляр Item с именем item. Поскольку adjust не статический, внутри кода механизма можно напрямую использовать поля экземпляра. Отображаемое имя, описание, зачарования и т. п. относятся к «item meta»: проверить её наличие можно через hasItemMeta(), а получить — через getItemMeta(). Если мета ещё нет — выбросим ошибку.

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

Метод echoError("...") есть и у attribute в тегах, и у mechanism в механизмах. Сам по себе он выполнение кода не останавливает, поэтому в обоих случаях нужно ещё явно вызвать return. Поскольку тег обязан что-то вернуть, в нём возвращают null — система тегов понимает это как «тег оказался невалидным».

if (mechanism.matches("wrap_brackets") && mechanism.requireInteger()) {
    if (getItemMeta() == null || !getItemMeta().hasDisplayName()) {
        mechanism.echoError("This item doesn't have a display name!");
        return;
    }
}

Когда мы избавились от ошибок, можно вызвать метод getValue(), который возвращает ввод как ElementTag. Опираясь на раздел «Получение ввода в теге», для количества скобок используем asInt(). Затем соберём нужное число пробелов с помощью несложной логики на StringBuilder.

if (mechanism.matches("wrap_brackets") && mechanism.requireInteger()) {
    // ...
    int amount = mechanism.getValue().asInt();
    StringBuilder spaces = new StringBuilder(amount);
    for (int i = 0; i < amount; i++) {
        spaces.append(" ");
    }
}

Пора поговорить про NMS (сокращение от net.minecraft.server — базовый пакет серверного Minecraft). Код «NMS» в Denizen обычно используется там, где Spigot API не поддерживает нужную возможность или где поведение зависит от версии Minecraft. В случае с отображаемым именем предмета Spigot API содержит метод, который неправильно обрабатывает расширенные текстовые возможности (например, альтернативные шрифты), поэтому в Denizen используется специальная реализация поверх NMS.

Доступ к NMS-инструментам Denizen даёт через класс NMSHandler и его подклассы. Конкретно нам нужен ItemHelper, который можно получить через NMSHandler.getItemHelper(). Затем вызовем getDisplayName(ItemTag) и setDisplayName(ItemTag, String) — и получим то, что нам нужно.

Возвращаясь к нашему механизму — всё сводится к паре вызовов этих методов и конкатенации строк. Вот итоговая реализация:

if (mechanism.matches("wrap_brackets") && mechanism.requireInteger()) {
    if (getItemMeta() == null || !getItemMeta().hasDisplayName()) {
        mechanism.echoError("This item doesn't have a display name!");
        return;
    }
    int amount = mechanism.getValue().asInt();
    StringBuilder spaces = new StringBuilder(amount);
    for (int i = 0; i < amount; i++) {
        spaces.append(" ");
    }
    String newName = "[" + spaces + NMSHandler.getItemHelper().getDisplayName(item) + spaces + "]";
    NMSHandler.getItemHelper().setDisplayName(this, newName);
}

Не забывайте, что для применения изменений к предмету в инвентаре нужно использовать команду inventory. У предмета также должно быть заранее задано свойство display (для этого и нужна проверка на ошибку выше).

У механизмов тоже есть мета-комментарии! Попробуйте заполнить его самостоятельно по этому шаблону:

// <--[mechanism]
// @object ObjectTag
// @name mech_name
// @input ObjectTag
// @description
// This is the description of the mechanism.
// @tags
// <ObjectTag.tag_name>
// -->

А свойства?

Многие объекты в Denizen можно описать базовым типом плюс набором конкретных деталей этого объекта. Каждая такая деталь, без которой нельзя точно определить «кто это», называется свойством (Property).

Например, у MaterialTag, когда он описывает тип блока, есть базовый тип — значение enum Material — и набор конкретных параметров данных блока. Скажем, MaterialTag.half — это значение «half» материала наподобие кровати: «головная» половина или «ножная». Верхняя половина кровати и нижняя половина кровати — это разные точные типы блоков. В Denizen это выглядит как red_bed[half=head]. Когда Denizen читает этот материал, создаётся экземпляр MaterialTag с типом материала red_bed, после чего к нему применяется механизм half со значением head, и в итоге получается валидный объект. При вызове identify() на этом экземпляре система проходит по всем свойствам и включает их в результат.

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

Теги регистрируются в статическом методе registerTags, а механизмы применяются в методе adjust. Возможно, вы заметите, что это ровно та же схема, что и в классах типов тегов!

Вот методы, которые нужно реализовать свойству:

  • getPropertyString — текущее значение свойства в виде строки, отформатированное так, чтобы его можно было напрямую передать в механизм. Для простых свойств вроде логических значений можно вернуть "true" или "false". Для более сложных выводов тегов можно использовать метод identify() соответствующего типа объекта. Метод может возвращать null, если значение свойства — значение по умолчанию или «не задано». Для расширяющих свойств он тоже возвращает null (подробнее об этом ниже).

  • getPropertyId — имя свойства. Должно совпадать с именем механизма. Например, half в MaterialTag.half.

  • describes — статический метод, который вызывается через рефлексию движком свойств и методом getFrom. Принимает ObjectTag и решает, может ли свойство с ним работать. Нужно проверить как тип тега, так и то, что конкретный подтип объекта описывается этим свойством. Например, leaf_size имеет смысл только для блоков bamboo, поэтому MaterialLeafSize проверяет, что переданный материал — бамбук.

  • getFrom — статический метод, вызываемый через рефлексию движком свойств. Должен вернуть экземпляр свойства, если это уместно, или null, если нет. Тело этого метода на практике почти всегда просто копируется из эталонных реализаций.

Сейчас при создании механизма нужно добавлять его имя в массив handledMechs. Когда-то то же самое требовалось для тегов, но от этого отказались в пользу системы регистрации. Если ваш механизм не распознаётся — скорее всего, вы просто забыли добавить его в массив.

Чтобы свойство распознавалось, его тоже нужно зарегистрировать. В пакете properties лежат пакеты для разных типов свойств, а также класс PropertyRegistry. Чтобы зарегистрировать свойство, вызовите PropertyParser.registerProperty() и передайте ему класс свойства и тип тега, к которому оно относится. Поставьте этот вызов в алфавитном порядке относительно других свойств того же типа.

Загляните в уже существующие классы свойств — там есть примеры!

Расширяющие свойства

Ещё один сценарий использования системы свойств — расширение существующих типов объектов. Например, в Spigot-реализации Denizen есть несколько расширений базовых типов, таких как BukkitElementProperties. А Depenizen активно использует расширяющие свойства, чтобы добавлять в Spigot-типы вроде PlayerTag возможности, завязанные на сторонние плагины.

На техническом уровне расширяющее свойство:

  • реализует Property, как и любое обычное свойство

  • обычно не делает в методе describes никаких проверок, кроме проверки типа объекта

  • всегда возвращает null из getPropertyString

  • возвращает из getPropertyId общее имя (как правило, совпадающее с именем класса)

  • не обязано иметь какой-либо механизм

  • живёт в отдельном проекте, отделённом от того, где определён сам объект