Skip to content

Module EventBus

Since v6.2.2

The Module EventBus is available starting from UltiTools-API v6.2.2.

UltiTools provides a decoupled publish/subscribe event system for inter-module communication. Modules can publish events and subscribe to events from other modules without any direct dependency between them.

Defining Events

Create a custom event by extending ModuleEvent:

java
package com.ultikits.docs.eventbus;

import com.ultikits.ultitools.events.ModuleEvent;
import lombok.Getter;

import java.util.UUID;

public class BalanceChangeEvent extends ModuleEvent {
    @Getter private final UUID player;
    @Getter private final double amount;

    public BalanceChangeEvent(UUID player, double amount) {
        this.player = player;
        this.amount = amount;
    }
}

Every ModuleEvent automatically carries:

  • sourceModule — the name of the module that published the event (set via setSourceModule())
  • timestamp — the time the event was created (System.currentTimeMillis())

Annotation-Based Handlers

Mark a method with @ModuleEventHandler in any managed bean. The framework discovers and registers it automatically:

java
package com.ultikits.docs.eventbus;

import com.ultikits.ultitools.annotations.ModuleEventHandler;
import com.ultikits.ultitools.annotations.Service;
import org.bukkit.Bukkit;

@Service
public class AuditService {

    @ModuleEventHandler
    public void onBalanceChange(BalanceChangeEvent event) {
        Bukkit.getLogger().info(
            event.getSourceModule() + " changed balance for "
            + event.getPlayer() + " by " + event.getAmount()
        );
    }
}

The method must have exactly one parameter that extends ModuleEvent.

Annotation Attributes

AttributeTypeDefaultDescription
priorityEventPriorityNORMALExecution order — LOWEST runs first, MONITOR runs last
ignoreCancelledbooleanfalseIf true, skip this handler when the event has been cancelled
java
@ModuleEventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onHighPriority(BalanceChangeEvent event) {
    // Runs after NORMAL handlers, skips if event was cancelled
}

Priority Order

Handlers execute in priority order from lowest to highest:

LOWESTLOWNORMALHIGHHIGHESTMONITOR

MONITOR Priority

Use MONITOR for read-only observation (logging, analytics). Avoid modifying event state at this priority.

Programmatic Subscriptions

For dynamic subscriptions (e.g., temporary listeners), use the EventBus API directly:

java
EventBus eventBus = UltiTools.getInstance().getEventBus();

// Simple subscription
Subscription sub = eventBus.subscribe(BalanceChangeEvent.class, event -> {
    player.sendMessage("Balance changed by " + event.getAmount());
});

// Later, unsubscribe when no longer needed
sub.unsubscribe();

Full options are also available:

java
Subscription sub = eventBus.subscribe(
    BalanceChangeEvent.class,
    EventPriority.HIGH,
    true,       // ignoreCancelled
    "MyModule", // owner module name
    event -> { /* handler */ }
);

The returned Subscription provides:

  • unsubscribe() — stop receiving events (safe to call multiple times)
  • isActive() — check if the subscription is still active

Publishing Events

Synchronous

Handlers run on the calling thread in priority order:

java
EventBus eventBus = UltiTools.getInstance().getEventBus();

BalanceChangeEvent event = new BalanceChangeEvent(player, 100.0);
event.setSourceModule("UltiEconomy");
eventBus.publish(event);

Asynchronous

Handlers run on a background thread pool:

java
eventBus.publishAsync(event);

Async Restrictions

  • Cancellable events cannot be published asynchronously — the framework throws IllegalArgumentException
  • Async handlers must not call most Bukkit API methods (use Bukkit.getScheduler().runTask() to dispatch back)

Cancellable Events

Implement Cancellable to allow handlers to prevent further processing:

java
package com.ultikits.docs.eventbus;

import com.ultikits.ultitools.events.Cancellable;
import com.ultikits.ultitools.events.ModuleEvent;

public class PlayerTradeEvent extends ModuleEvent implements Cancellable {
    private boolean cancelled;

    @Override
    public boolean isCancelled() { return cancelled; }

    @Override
    public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }

    // ... your event data
}

A handler can cancel the event:

java
@ModuleEventHandler(priority = EventPriority.LOW)
public void onTrade(PlayerTradeEvent event) {
    if (isBanned(event.getTrader())) {
        event.setCancelled(true); // Stops subsequent handlers with ignoreCancelled=true
    }
}

Handlers with ignoreCancelled = true will be skipped once the event is cancelled.

Superclass Matching

Handlers registered for a parent event type also receive child events:

java
// Catches ALL ModuleEvents
@ModuleEventHandler
public void onAnyEvent(ModuleEvent event) {
    Bukkit.getLogger().info("Event from " + event.getSourceModule());
}

A handler for BalanceChangeEvent will not receive other ModuleEvent subtypes.

Error Isolation

If a handler throws an exception, it is logged and other handlers continue to execute normally. One buggy handler cannot break the entire event chain.

Automatic Lifecycle

  • Registration: @ModuleEventHandler methods in @Service beans are discovered and registered when the plugin module loads
  • Cleanup: All handlers owned by a module are automatically unregistered when the module is unloaded

You do not need to manage handler lifecycle manually for annotation-based handlers.

Complete Example

Economy module publishes an event when balance changes:

java
// In UltiEconomy module
public class BalanceChangeEvent extends ModuleEvent {
    @Getter private final UUID player;
    @Getter private final double oldBalance;
    @Getter private final double newBalance;

    public BalanceChangeEvent(UUID player, double oldBalance, double newBalance) {
        this.player = player;
        this.oldBalance = oldBalance;
        this.newBalance = newBalance;
    }
}

@Service
public class EconomyService {
    @Autowired
    private UltiToolsPlugin plugin;

    public void setBalance(UUID player, double amount) {
        double old = getBalance(player);
        // ... update database ...

        BalanceChangeEvent event = new BalanceChangeEvent(player, old, amount);
        event.setSourceModule(plugin.getPluginName());
        UltiTools.getInstance().getEventBus().publish(event);
    }
}

Separate audit module receives it without depending on the economy module:

java
package com.ultikits.docs.eventbus.complete;

import com.ultikits.ultitools.annotations.ModuleEventHandler;
import com.ultikits.ultitools.annotations.Service;
import org.bukkit.Bukkit;

// In a separate audit module
@Service
public class AuditLogService {

    @ModuleEventHandler
    public void onBalanceChange(BalanceChangeEvent event) {
        double delta = event.getNewBalance() - event.getOldBalance();
        Bukkit.getLogger().info(String.format(
            "[Audit] %s balance: %.2f -> %.2f (delta: %+.2f) from %s",
            event.getPlayer(), event.getOldBalance(),
            event.getNewBalance(), delta, event.getSourceModule()
        ));
    }
}

Cross-Module Communication

The EventBus is ideal for scenarios where modules need to react to each other's actions without tight coupling. Examples:

  • Economy changes triggering audit logs or notifications
  • Player teleport events triggering region checks
  • Custom game events coordinating between multiple modules

Contributors

The avatar of contributor named as Ling Bao Ling Bao

Changelog

Released under the MIT License.