Система событий
Аннотация @Pulse, интерфейс PulseListener, приоритеты обработки, отмена и изменение неизменяемых событий
События это главный способ вмешаться в работу FlectonePulse. Они срабатывают на всех этапах, от входа игрока до сборки каждого сообщения
| Компонент | Зачем нужен |
|---|---|
Event | Базовый интерфейс всех событий, умеет отменяться |
EventDispatcher | Прогоняет событие через слушателей по приоритету |
ListenerRegistry | Регистрирует и удаляет слушателей |
@Pulse | Помечает метод обработчиком |
PulseListener | Помечает класс, в котором лежат обработчики |
События неизменяемы
Каждое событие это record. Поменять поле на месте нельзя, методы with* отдают новую копию, и эту копию обработчик обязан вернуть через return
// не работает, копия создана, но никуда не передана
@Pulse
public void onFormatting(MessageFormattingEvent event) {
event.withContext(event.context().withMessage("новый текст"));
}
// работает, копия уходит диспетчеру
@Pulse
public Event onFormatting(MessageFormattingEvent event) {
return event.withContext(event.context().withMessage("новый текст"));
}Если обработчик ничего не меняет, объявляй его как void. Если меняет, тип возвращаемого значения должен быть Event или конкретный тип события. Диспетчер подхватит результат, только когда это событие
Свой слушатель
Реализуй интерфейс PulseListener
Объяви публичные методы с аннотацией @Pulse и одним параметром, типом нужного события
Зарегистрируй слушателя через ListenerRegistry
import net.flectone.pulse.annotation.Pulse;
import net.flectone.pulse.listener.PulseListener;
import net.flectone.pulse.model.event.Event;
import net.flectone.pulse.model.event.message.MessageFormattingEvent;
import net.flectone.pulse.model.event.message.context.MessageContext;
import net.flectone.pulse.model.event.player.PlayerJoinEvent;
public class MyCustomListener implements PulseListener {
// меняем сообщение во время форматирования, поэтому нужен return
@Pulse(priority = Event.Priority.NORMAL)
public Event onMessageFormatting(MessageFormattingEvent event) {
MessageContext messageContext = event.context();
String modifiedMessage = messageContext.message() + " [Modified]";
return event.withContext(messageContext.withMessage(modifiedMessage));
}
// с ignoreCancelled метод вызовется, даже если событие уже отменено
@Pulse(priority = Event.Priority.LOWEST, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
String playerIp = event.player().ip();
}
}Метод должен быть публичным и принимать ровно один параметр, наследующий Event. Иначе при регистрации прилетит ListenerRegistrationException
Регистрация
Слушателей регистрирует ListenerRegistry
import net.flectone.pulse.platform.registry.ListenerRegistry;
ListenerRegistry listenerRegistry = flectonePulse.get(ListenerRegistry.class);
// для своих плагинов, переживёт /flectonepulse reload
listenerRegistry.registerPermanent(new MyCustomListener());| Метод | Что делает |
|---|---|
registerPermanent(PulseListener) | Регистрирует слушателя и восстанавливает его после перезагрузки |
register(PulseListener) | Регистрирует готовый экземпляр до ближайшей перезагрузки |
register(Class<?>) | Создаёт слушателя через Guice и регистрирует его |
register(Class<? extends Event>, Priority, UnaryOperator<Event>) | Регистрирует одиночный обработчик без отдельного класса |
unregisterAll() | Удаляет всех слушателей, включая постоянных |
getPulseListeners(Class<? extends Event>) | Отдаёт обработчики события, разложенные по приоритетам |
При /flectonepulse reload вызывается unregisterAll(), после чего заново регистрируются стандартные и постоянные слушатели. Если ты использовал register(), твой слушатель после перезагрузки пропадёт
Обработчик без класса
Ради одного простого обработчика отдельный класс заводить не обязательно
import net.flectone.pulse.model.event.Event;
import net.flectone.pulse.model.event.player.PlayerQuitEvent;
listenerRegistry.register(PlayerQuitEvent.class, Event.Priority.MONITOR, event -> {
PlayerQuitEvent quitEvent = (PlayerQuitEvent) event;
getLogger().info(quitEvent.player().name() + " вышел");
return event;
});Приоритеты
EventDispatcher вызывает обработчики по возрастанию приоритета
| Приоритет | Порядок | Для чего подходит |
|---|---|---|
LOWEST | Первый | Ранняя отмена события или подготовка данных |
LOW | Второй | Проверка данных |
NORMAL | Третий | Обычная обработка, стоит по умолчанию |
HIGH | Четвёртый | Обработка после основных изменений |
HIGHEST | Пятый | Финальные правки |
MONITOR | Последний | Только наблюдение и логирование, без изменений |
Внутри одного приоритета обработчики идут в порядке регистрации
Отмена события
Отменённое событие останавливает действие, ради которого его вызвали
@Pulse(priority = Event.Priority.LOWEST)
public Event onPreLogin(PlayerPreLoginEvent event) {
if (event.player().ip() != null && event.player().ip().startsWith("10.")) {
return event.withAllowed(false)
.withKickReason(Component.text("Вход из локальной сети запрещён"));
}
return event;
}Узнать, отменил ли событие кто то другой, можно через cancelled()
@Pulse(priority = Event.Priority.MONITOR, ignoreCancelled = true)
public void onSend(MessageSendEvent event) {
if (event.cancelled()) {
getLogger().info("Сообщение отменил другой слушатель");
}
}ignoreCancelled
| Значение | Поведение |
|---|---|
false | Обработчик не вызовется, если событие уже отменено. Стоит по умолчанию |
true | Обработчик вызовется в любом случае |
Ручная отправка
Прогнать событие через слушателей можно самому, для этого есть EventDispatcher
import net.flectone.pulse.dispatcher.EventDispatcher;
EventDispatcher eventDispatcher = flectonePulse.get(EventDispatcher.class);
MessageReceiveEvent result = eventDispatcher.dispatch(
new MessageReceiveEvent(fPlayer, Component.text("Привет"), false)
);
if (!result.cancelled()) {
// никто не отменил, можно отправлять
}dispatch() отдаёт событие в том виде, в котором его оставили слушатели
Свои события
Своё событие пишется как record, реализующий Event. Аннотация Lombok @With сама сгенерирует методы копирования
import lombok.With;
import net.flectone.pulse.model.entity.FPlayer;
import net.flectone.pulse.model.event.Event;
@With
public record MyCustomEvent(
boolean cancelled,
FPlayer player,
String data
) implements Event {
public MyCustomEvent(FPlayer player, String data) {
this(false, player, data);
}
}Отправляется оно тем же EventDispatcher, а слушатели пишутся точно так же, как для встроенных событий
События обрабатываются синхронно и по порядку приоритета, поэтому долгие операции выноси в планировщик. Ошибка внутри обработчика не роняет цепочку, FlectonePulse запишет предупреждение в лог и продолжит с исходным событием