Skip to main content

Event Node

Event Node is a powerful event handling system that provides a graph-based approach to event filtering and processing. Unlike traditional Bukkit listeners, Event Nodes allow you to create hierarchical event structures with conditional filtering, priority ordering, and dynamic management.


Overview

The Event Node system provides:

  • Hierarchical Structure: Create parent-child relationships between event handlers
  • Conditional Filtering: Filter events based on event data or handler targets
  • Priority Ordering: Control the execution order of event handlers
  • Dynamic Management: Add, remove, or replace event handlers at runtime
  • Type Safety: Strong typing for event types and handlers

Basic Usage

Injecting BukkitEventNode

The BukkitEventNode is the global event node for all Bukkit events. Inject it into your component:

import io.fairyproject.bukkit.events.BukkitEventNode;
import io.fairyproject.container.InjectableComponent;
import org.bukkit.event.player.PlayerJoinEvent;

@InjectableComponent
public class MyEventHandler {
private final BukkitEventNode eventNode;

public MyEventHandler(BukkitEventNode eventNode) {
this.eventNode = eventNode;

// Add a simple listener
eventNode.addListener(PlayerJoinEvent.class, event -> {
event.getPlayer().sendMessage("Welcome to the server!");
});
}
}

Creating Event Nodes

You can create standalone event nodes for more organized event handling:

import io.fairyproject.event.EventNode;
import io.fairyproject.event.EventFilter;
import org.bukkit.event.Event;

// Create a node that accepts all events
EventNode<Event> allEventsNode = EventNode.all("my-node");

// Create a node with a specific event filter
EventNode<PlayerEvent> playerNode = EventNode.type("player-events", BukkitEventFilter.PLAYER);

Event Filters

Event filters define what type of events a node accepts and provide access to the event's handler (target).

Available Bukkit Filters

FilterEvent TypeHandler TypeDescription
BukkitEventFilter.ALLEvent-Accepts all Bukkit events
BukkitEventFilter.PLAYERPlayerEventPlayerPlayer-related events
BukkitEventFilter.ENTITYEntityEventEntityEntity-related events
BukkitEventFilter.BLOCKBlockEventBlockBlock-related events
BukkitEventFilter.WORLDWorldEventWorldWorld-related events
BukkitEventFilter.INVENTORYInventoryEventInventoryInventory-related events
BukkitEventFilter.PLUGINPluginEventPluginPlugin-related events

Using Filters with Conditions

import io.fairyproject.bukkit.events.BukkitEventFilter;
import io.fairyproject.event.EventNode;
import org.bukkit.GameMode;
import org.bukkit.event.player.PlayerEvent;

// Create a node that only accepts events from players in creative mode
EventNode<PlayerEvent> creativePlayerNode = EventNode.value(
"creative-players",
BukkitEventFilter.PLAYER,
player -> player.getGameMode() == GameMode.CREATIVE
);

// Create a node with event-based condition
EventNode<PlayerEvent> specificWorldNode = EventNode.event(
"specific-world",
BukkitEventFilter.PLAYER,
event -> event.getPlayer().getWorld().getName().equals("world")
);

// Create a node with both event and handler condition
EventNode<PlayerEvent> customNode = EventNode.type(
"custom-filter",
BukkitEventFilter.PLAYER,
(event, player) -> player.hasPermission("my.permission")
);

Event Listeners

Simple Listener

eventNode.addListener(PlayerJoinEvent.class, event -> {
event.getPlayer().sendMessage("Hello!");
});

Builder Pattern

For more control, use the EventListener.builder():

import io.fairyproject.event.EventListener;
import org.bukkit.event.player.PlayerMoveEvent;

EventListener<PlayerMoveEvent> listener = EventListener.builder(PlayerMoveEvent.class)
.filter(event -> event.getTo() != null) // Only process if destination exists
.ignoreCancelled(true) // Skip cancelled events (default)
.expireCount(10) // Remove after 10 executions
.expireWhen(event -> event.getPlayer().isDead()) // Remove when player dies
.handler(event -> {
// Handle the event
event.getPlayer().sendMessage("You moved!");
})
.build();

eventNode.addListener(listener);

Listener Options

OptionDescriptionDefault
filter(Predicate)Additional condition for the event to passNone
ignoreCancelled(boolean)Whether to skip cancelled eventstrue
expireCount(int)Remove listener after N executions0 (never)
expireWhen(Predicate)Remove listener when condition is metNone
handler(Consumer)The event handler functionRequired

Node Hierarchy

Parent-Child Relationships

Event nodes can have child nodes, creating a hierarchical structure:

import io.fairyproject.bukkit.events.BukkitEventFilter;
import io.fairyproject.bukkit.events.BukkitEventNode;
import io.fairyproject.event.EventNode;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;

@InjectableComponent
public class PlayerEventManager {
public PlayerEventManager(BukkitEventNode globalNode) {
// Create a child node for player events
EventNode<PlayerEvent> playerNode = EventNode.type("player-events", BukkitEventFilter.PLAYER);

// Add listeners to the child node
playerNode.addListener(PlayerJoinEvent.class, event -> {
System.out.println(event.getPlayer().getName() + " joined!");
});

playerNode.addListener(PlayerQuitEvent.class, event -> {
System.out.println(event.getPlayer().getName() + " left!");
});

// Attach to global node
globalNode.addChild(playerNode);
}
}

Priority System

Child nodes and listeners are executed based on their priority:

EventNode<Event> highPriority = EventNode.all("high-priority").setPriority(1);
EventNode<Event> lowPriority = EventNode.all("low-priority").setPriority(10);

// Lower number = higher priority (executed first)
globalNode.addChild(highPriority); // Executed first
globalNode.addChild(lowPriority); // Executed second

Dynamic Node Management

Adding and Removing Children

// Add a child node
EventNode<Event> childNode = EventNode.all("child");
parentNode.addChild(childNode);

// Remove a child node
parentNode.removeChild(childNode);

// Remove all children with a specific name
parentNode.removeChildren("child");

Finding Children

// Find all children with a specific name
List<EventNode<Event>> found = parentNode.findChildren("my-node");

// Find children with a specific name and type
List<EventNode<PlayerEvent>> playerNodes = parentNode.findChildren("player-node", PlayerEvent.class);

Replacing Children

EventNode<Event> newNode = EventNode.all("replacement");
parentNode.replaceChildren("old-node", newNode);

Cancellable Events

Handling Cancellation

import io.fairyproject.event.Cancellable;

eventNode.addListener(PlayerMoveEvent.class, event -> {
if (shouldCancel(event)) {
event.setCancelled(true);
}
});

// Later listeners can check and skip cancelled events
EventListener<PlayerMoveEvent> listener = EventListener.builder(PlayerMoveEvent.class)
.ignoreCancelled(true) // This listener won't run if event is cancelled
.handler(event -> {
// This only runs if event wasn't cancelled
})
.build();

Cancellable Callback

// Execute callback only if event is not cancelled
eventNode.callCancellable(new MyEvent(), () -> {
// This runs only if the event wasn't cancelled
System.out.println("Event was successful!");
});

Cleanup and Lifecycle

Using Terminable

Event nodes implement Terminable, making cleanup easy:

import io.fairyproject.util.terminable.TerminableConsumer;
import io.fairyproject.util.terminable.composite.CompositeTerminable;

@InjectableComponent
public class MyComponent {
private final CompositeTerminable terminable = CompositeTerminable.create();

public MyComponent(BukkitEventNode globalNode) {
EventNode<Event> myNode = EventNode.all("my-node");
globalNode.addChild(myNode);

// Register for automatic cleanup
myNode.bindWith(terminable);
}

// Called when component is destroyed
public void onDestroy() {
terminable.closeAndReportException();
}
}

Complete Example

Here's a complete example demonstrating various Event Node features:

import io.fairyproject.bukkit.events.BukkitEventFilter;
import io.fairyproject.bukkit.events.BukkitEventNode;
import io.fairyproject.container.InjectableComponent;
import io.fairyproject.container.PreDestroy;
import io.fairyproject.event.EventListener;
import io.fairyproject.event.EventNode;
import org.bukkit.GameMode;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;

@InjectableComponent
public class AdvancedEventHandler {
private final EventNode<PlayerEvent> playerNode;
private final BukkitEventNode globalNode;

public AdvancedEventHandler(BukkitEventNode globalNode) {
this.globalNode = globalNode;

// Create a filtered player event node
this.playerNode = EventNode.value(
"survival-players",
BukkitEventFilter.PLAYER,
player -> player.getGameMode() == GameMode.SURVIVAL
);

// Set priority (lower = executed earlier)
playerNode.setPriority(5);

// Add welcome message for survival players
playerNode.addListener(PlayerJoinEvent.class, event -> {
event.getPlayer().sendMessage("Welcome, survivor!");
});

// Add goodbye message
playerNode.addListener(PlayerQuitEvent.class, event -> {
System.out.println("Survivor " + event.getPlayer().getName() + " left");
});

// Add a listener that expires after 100 moves
EventListener<PlayerMoveEvent> limitedListener = EventListener.builder(PlayerMoveEvent.class)
.filter(event -> event.getTo() != null)
.expireCount(100)
.handler(event -> {
// Track first 100 movements per server restart
})
.build();
playerNode.addListener(limitedListener);

// Attach to global node
globalNode.addChild(playerNode);
}

@PreDestroy
public void cleanup() {
// Remove our node when component is destroyed
globalNode.removeChild(playerNode);
}
}

Event Node vs Traditional Listeners

FeatureEvent NodeTraditional Listener
Hierarchical filteringYesNo
Dynamic add/removeEasyManual
Conditional executionBuilt-inManual checks
Priority controlPer-nodePer-handler
Type safetyStrongBasic
Memory managementAutomatic with TerminableManual
When to Use Event Node

Use Event Node when you need:

  • Complex event filtering logic
  • Dynamic event handler management
  • Organized hierarchical event handling
  • Automatic cleanup with component lifecycle

For simple cases, the traditional @RegisterAsListener approach may be simpler.


Best Practices

  1. Name your nodes descriptively - Makes debugging and finding nodes easier
  2. Use appropriate filters - Filter at the node level rather than in every listener
  3. Clean up properly - Use Terminable or @PreDestroy to remove nodes
  4. Set priorities carefully - Lower numbers execute first
  5. Prefer child nodes over many listeners - Better organization and easier management