Event system
The @Pulse annotation, the PulseListener interface, handler priorities, cancelling and changing immutable events
Events are the main way into the work of FlectonePulse. They fire at every stage, from a player joining to the assembly of each message
| Component | What it is for |
|---|---|
Event | The base interface of every event, it can be cancelled |
EventDispatcher | Runs an event through the listeners by priority |
ListenerRegistry | Registers and removes listeners |
@Pulse | Marks a method as a handler |
PulseListener | Marks a class that holds handlers |
Events are immutable
Every event is a record. You cannot change a field in place, the with* methods give you a new copy, and the handler has to return that copy through return
// does not work, the copy is created but goes nowhere
@Pulse
public void onFormatting(MessageFormattingEvent event) {
event.withContext(event.context().withMessage("new text"));
}
// works, the copy reaches the dispatcher
@Pulse
public Event onFormatting(MessageFormattingEvent event) {
return event.withContext(event.context().withMessage("new text"));
}If your handler changes nothing, declare it as void. If it changes something, the return type has to be Event or the concrete event type. The dispatcher picks up the result only when it is an event
Your own listener
Implement the PulseListener interface
Declare public methods with the @Pulse annotation and one parameter, the event type you need
Register the listener through 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 {
// we change the message during formatting, so return is required
@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));
}
// with ignoreCancelled the method runs even when the event is already cancelled
@Pulse(priority = Event.Priority.LOWEST, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
String playerIp = event.player().ip();
}
}The method has to be public and take exactly one parameter that extends Event. Otherwise registration throws ListenerRegistrationException
Registration
ListenerRegistry registers listeners
import net.flectone.pulse.platform.registry.ListenerRegistry;
ListenerRegistry listenerRegistry = flectonePulse.get(ListenerRegistry.class);
// for your own plugins, survives /flectonepulse reload
listenerRegistry.registerPermanent(new MyCustomListener());| Method | What it does |
|---|---|
registerPermanent(PulseListener) | Registers a listener and restores it after a reload |
register(PulseListener) | Registers a ready instance until the next reload |
register(Class<?>) | Builds a listener through Guice and registers it |
register(Class<? extends Event>, Priority, UnaryOperator<Event>) | Registers a single handler without a separate class |
unregisterAll() | Removes every listener, permanent ones included |
getPulseListeners(Class<? extends Event>) | Gives the handlers of an event, grouped by priority |
/flectonepulse reload calls unregisterAll() and then registers the default and permanent listeners again. If you used register(), your listener disappears after a reload
A handler without a class
For one simple handler a separate class is not needed
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() + " left");
return event;
});Priorities
EventDispatcher calls handlers in rising order of priority
| Priority | Order | What it fits |
|---|---|---|
LOWEST | First | Early cancelling or data preparation |
LOW | Second | Data checks |
NORMAL | Third | Ordinary work, set by default |
HIGH | Fourth | Work after the main changes |
HIGHEST | Fifth | Final touches |
MONITOR | Last | Watching and logging only, no changes |
Inside one priority handlers run in registration order
Cancelling an event
A cancelled event stops the action it was fired for
@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("Local network login is not allowed"));
}
return event;
}You can find out whether someone else cancelled the event through cancelled()
@Pulse(priority = Event.Priority.MONITOR, ignoreCancelled = true)
public void onSend(MessageSendEvent event) {
if (event.cancelled()) {
getLogger().info("Another listener cancelled the message");
}
}ignoreCancelled
| Value | Behaviour |
|---|---|
false | The handler does not run when the event is already cancelled. Set by default |
true | The handler runs in any case |
Firing by hand
You can run an event through the listeners yourself with EventDispatcher
import net.flectone.pulse.dispatcher.EventDispatcher;
EventDispatcher eventDispatcher = flectonePulse.get(EventDispatcher.class);
MessageReceiveEvent result = eventDispatcher.dispatch(
new MessageReceiveEvent(fPlayer, Component.text("Hello"), false)
);
if (!result.cancelled()) {
// nobody cancelled it, you can send
}dispatch() gives back the event in the shape the listeners left it
Your own events
Your own event is a record that implements Event. The Lombok @With annotation generates the copy methods for you
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);
}
}You fire it through the same EventDispatcher, and listeners look exactly like the ones for built in events
Events run synchronously and in priority order, so move long work into the scheduler. A failure inside a handler does not break the chain, FlectonePulse writes a warning to the log and carries on with the original event