Добавление новых свойств и инструментов
Предполагается, что вы уже умеете пользоваться тегами и механизмами в скриптах. Эта страница проведёт вас через создание собственных кастомных тегов, механизмов и свойств на Java.
Получение ввода в теге
Пойдём дальше и ещё немного «поиздеваемся» над 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 в верхнем регистре через пробел.
Создание механизмов
ПРИМЕЧАНИЕ: раздел может измениться в будущем, когда механизмы переведут на систему регистрации, аналогичную системе регистрации тегов.
В отличие от современных тегов, механизмы проходят серию проверок, чтобы «совпасть» с обрабатываемым механизмом. Это происходит в методе 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общее имя (как правило, совпадающее с именем класса)не обязано иметь какой-либо механизм
живёт в отдельном проекте, отделённом от того, где определён сам объект
