Кастомные типы
В AbstractMenus пять реестров типов, у всех одинаковая сигнатура register(key, class, serializer, owner):
api.actions().register("...", MyAction.class, new MyAction.Serializer(), this);api.rules().register("...", MyRule.class, new MyRule.Serializer(), this);api.itemProperties().register("...", MyProperty.class, new MyProperty.Serializer(), this);api.activators().register("...", MyActivator.class, new MyActivator.Serializer(), this);api.catalogs().register("...", MyCatalog.class, new MyCatalog.Serializer(), this);Ключи нечувствительны к регистру. owner - твой экземпляр MenuExtension; по нему AbstractMenus снимает твои регистрации при выключении аддона.
Для своих ключей бери вендорный префикс (myaddon_action, playerpoints_take) - чтобы будущая встроенная сущность с таким же именем не вступила в конфликт.
Регистрируй всё в MenuExtension.onEnable(api). Из onLoad не регистрируй: аддоны, на которые ты опираешься, могут быть ещё не включены.
Действие
Заголовок раздела «Действие»Действие - то, что плагин делает: отправляет сообщение, выдаёт предмет, выполняет команду, открывает другое меню. Класс действия реализует Action:
public class MessageAction implements Action {
private final String text;
private MessageAction(String text) { this.text = text; }
@Override public void activate(Player player, Menu menu, Item clickedItem) { player.sendMessage(text); }
public static class Serializer implements NodeSerializer<MessageAction> { @Override public MessageAction deserialize(Class<MessageAction> type, ConfigNode node) { return new MessageAction(node.getString()); } }}clickedItem может быть null - зависит от того, что запустило цепочку действий: клик или, например, цепочка deny-действий, сработавшая ещё до выбора предмета.
Регистрация:
api.actions().register("myMessage", MessageAction.class, new MessageAction.Serializer(), this);В файле меню:
items: [ { slot: 1 material: STONE name: "Мой предмет" click { myMessage: "Привет! Это моё действие!" } }]Правило
Заголовок раздела «Правило»Правило - булева проверка по игроку. Реализует Rule:
public class IsBobRule implements Rule {
@Override public boolean check(Player player, Menu menu, Item clickedItem) { return "Bob".equals(player.getName()); }
public static class Serializer implements NodeSerializer<IsBobRule> { @Override public IsBobRule deserialize(Class<IsBobRule> type, ConfigNode node) { return new IsBobRule(); } }}Регистрация и использование:
api.rules().register("isBob", IsBobRule.class, new IsBobRule.Serializer(), this);rules { isBob: true}Правило без параметров принимает в HOCON true - это сокращение для “правило активно”. Правила с параметрами принимают ту HOCON-форму, которую парсит их сериализатор.
Экстрактор значений
Заголовок раздела «Экстрактор значений»Экстрактор значений достаёт именованные значения из объекта контекста. Активаторы и каталоги используют его, чтобы пробрасывать данные контекста через плейсхолдеры.
Реализует ValueExtractor:
public class UserExtractor implements ValueExtractor {
@Override public String extract(Object obj, String placeholder) { if (!(obj instanceof User)) return null; User user = (User) obj; return switch (placeholder) { case "user_name" -> user.name; case "user_age" -> String.valueOf(user.age); case "user_friends" -> String.valueOf(user.friends); default -> null; }; }}Сами экстракторы напрямую не регистрируются. Их экземпляр возвращает активатор или каталог из своего getValueExtractor() / extractor().
По форме похоже на PlaceholderAPI, только тип контекста любой, не обязательно Player.
Активатор
Заголовок раздела «Активатор»Активатор - слушатель событий, который открывает меню. Наследуется от абстрактного Activator, а тот - от Bukkit-овского Listener. Внутри слушай любое нужное Bukkit-событие:
public class SneakActivator extends Activator {
@EventHandler public void onSneak(PlayerToggleSneakEvent event) { if (event.isSneaking()) { openMenu(null, event.getPlayer()); } }
public static class Serializer implements NodeSerializer<SneakActivator> { @Override public SneakActivator deserialize(Class<SneakActivator> type, ConfigNode node) { return new SneakActivator(); } }}openMenu(ctx, player) открывает меню, к которому привязан активатор. ctx - контекст открытия, или null, если контекста нет.
Регистрация активатора:
api.activators().register("onSneak", SneakActivator.class, new SneakActivator.Serializer(), this);Активатор с контекстом
Заголовок раздела «Активатор с контекстом»В ctx можно положить любой объект, а его поля прокинуть через ValueExtractor. В примере ниже контекстом идёт точка респауна, LocationExtractor делает её координаты доступными как %activator_loc_x% и т.д.:
public class RespawnActivator extends Activator {
@EventHandler public void onRespawn(PlayerRespawnEvent event) { openMenu(event.getRespawnLocation(), event.getPlayer()); }
@Override public ValueExtractor getValueExtractor() { return new LocationExtractor(); }
public static class Serializer implements NodeSerializer<RespawnActivator> { @Override public RespawnActivator deserialize(Class<RespawnActivator> type, ConfigNode node) { return new RespawnActivator(); } }}public class LocationExtractor implements ValueExtractor {
@Override public String extract(Object obj, String placeholder) { if (!(obj instanceof Location)) return null; Location loc = (Location) obj; return switch (placeholder) { case "loc_x" -> String.valueOf(loc.getX()); case "loc_y" -> String.valueOf(loc.getY()); case "loc_z" -> String.valueOf(loc.getZ()); default -> null; }; }}title: "Тест"size: 1activators { onRespawn: true}items: [ { slot: 4 material: CAKE name: "Тестовый предмет" lore: [ "Loc x: %activator_loc_x%", "Loc y: %activator_loc_y%", "Loc z: %activator_loc_z%" ] }]Свойство предмета
Заголовок раздела «Свойство предмета»Свойство предмета меняет его внешний вид - имя, лор, материал, custom model data и т.д.
Реализуй ItemProperty:
canReplaceMaterial- возвращаетtrue, если свойство меняет материал. Такие свойства выполняются первыми, чтобы дальше остальные работали уже с валиднойItemMeta.isApplyMeta- возвращаетtrue, если послеapplyAbstractMenus должен сам вызватьsetItemMetaна стеке. Возвращайfalse, еслиapplyуже разбирается с meta самостоятельно.apply- модифицируетItemStackи/илиItemMeta.
Свойство для имени:
public class DisplayNameProperty implements ItemProperty {
private final String name;
private DisplayNameProperty(String name) { this.name = name; }
@Override public boolean canReplaceMaterial() { return false; }
@Override public boolean isApplyMeta() { return true; }
@Override public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) { meta.setDisplayName(name); }
public static class Serializer implements NodeSerializer<DisplayNameProperty> { @Override public DisplayNameProperty deserialize(Class<DisplayNameProperty> type, ConfigNode node) { return new DisplayNameProperty(node.getString()); } }}Подмена материала (параметров нет, просто проставляет тип):
public class CreeperHeadProperty implements ItemProperty {
@Override public boolean canReplaceMaterial() { return true; }
@Override public boolean isApplyMeta() { return false; }
@Override public void apply(ItemStack item, ItemMeta meta, Player player, Menu menu) { item.setType(Material.CREEPER_HEAD); }
public static class Serializer implements NodeSerializer<CreeperHeadProperty> { @Override public CreeperHeadProperty deserialize(Class<CreeperHeadProperty> type, ConfigNode node) { return new CreeperHeadProperty(); } }}Регистрация:
api.itemProperties().register("creeperHead", CreeperHeadProperty.class, new CreeperHeadProperty.Serializer(), this);Каталог
Заголовок раздела «Каталог»Каталог отдаёт генерируемому меню динамическую коллекцию объектов. Каждая запись становится своим предметом в меню, а ValueExtractor даёт доступ к её полям через плейсхолдеры.
Класс реализует Catalog<T>:
public class UserCatalog implements Catalog<User> {
@Override public Collection<User> snapshot(Player player, Menu menu) { return List.of( new User("User 1", 17), new User("User 2", 18), new User("User 3", 19) ); }
@Override public ValueExtractor extractor() { return new UserExtractor(); }
public static class Serializer implements NodeSerializer<UserCatalog> { @Override public UserCatalog deserialize(Class<UserCatalog> type, ConfigNode node) throws NodeSerializeException { return new UserCatalog(); } }}snapshot дёргается один раз на открытие меню (или на refresh). Может вернуть пустую коллекцию, но не null.
Регистрация и использование:
api.catalogs().register("users", UserCatalog.class, new UserCatalog.Serializer(), this);title: "Пользователи"size: 4
catalog { type: users}
matrix { cells: [ "_x_x_x_x_", "_x_x_x_x_", "_x_x_x_x_" ] templates { "x" { material: CAKE name: "%activator_user_name%" lore: "&7Возраст: &e%activator_user_age%" } }}Если каталогу нужна конфигурация (фильтр и т.п.), парси её из ConfigNode в сериализаторе.
Регистрации типов снимаются автоматически при выключении аддона. AbstractMenus запоминает владельца из register(...) и сносит всё, что помечено этим владельцем. Самому unregister дёргать нельзя.