Flectone LogoFlectonePulse

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

ComponentWhat it is for
EventThe base interface of every event, it can be cancelled
EventDispatcherRuns an event through the listeners by priority
ListenerRegistryRegisters and removes listeners
@PulseMarks a method as a handler
PulseListenerMarks 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"));
}
Attention

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();
    }

}
Warning

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());
MethodWhat 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
Attention

/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

PriorityOrderWhat it fits
LOWESTFirstEarly cancelling or data preparation
LOWSecondData checks
NORMALThirdOrdinary work, set by default
HIGHFourthWork after the main changes
HIGHESTFifthFinal touches
MONITORLastWatching 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

ValueBehaviour
falseThe handler does not run when the event is already cancelled. Set by default
trueThe 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

Note

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

Last update August 11, 2026
Edit on Github

On this page