Files
hytale-server/docs/03-event-system.md

9.3 KiB

Event System

The Hytale event system provides a powerful mechanism for plugins to react to game events. It supports both synchronous and asynchronous events with priority-based dispatching.

Event Architecture

Event Interface Hierarchy

IBaseEvent<KeyType>
├── IEvent<KeyType>        // Synchronous events
└── IAsyncEvent<KeyType>   // Asynchronous events (CompletableFuture)

ICancellable               // Mixin interface for cancellable events

Key Classes

Class Description
IEvent<K> Synchronous event interface
IAsyncEvent<K> Asynchronous event interface
ICancellable Interface for events that can be cancelled
EventRegistry Plugin-specific event registration
SyncEventBusRegistry Global synchronous event bus
EventPriority Event listener priority levels

Event Priorities

Events are dispatched to listeners in priority order:

Priority Value Description
FIRST -21844 Runs first, before all others
EARLY -10922 Runs early, after FIRST
NORMAL 0 Default priority
LATE 10922 Runs late, after NORMAL
LAST 21844 Runs last, after all others

Lower values run first. Use FIRST sparingly - typically for monitoring/logging. Use LAST for final processing after other plugins have had a chance to modify the event.

Registering Event Listeners

Basic Registration

@Override
protected void setup() {
    // Register with default NORMAL priority
    getEventRegistry().register(PlayerConnectEvent.class, this::onPlayerConnect);
}

private void onPlayerConnect(PlayerConnectEvent event) {
    getLogger().info("Player connected: " + event.getPlayer().getName());
}

With Priority

// Run before other listeners
getEventRegistry().register(EventPriority.FIRST, PlayerConnectEvent.class, event -> {
    getLogger().debug("Logging player connect (first)");
});

// Run after other listeners
getEventRegistry().register(EventPriority.LAST, PlayerConnectEvent.class, event -> {
    getLogger().debug("Final processing (last)");
});

Global Listeners

Global listeners receive all events of a type, regardless of the event's key:

// Receive all entity events
getEventRegistry().registerGlobal(EntityEvent.class, event -> {
    // Handle any entity event
});

Keyed Events

Some events are keyed (e.g., by world name). You can listen for specific keys:

// Listen for events in a specific world
getEventRegistry().register(AddPlayerToWorldEvent.class, "worldName", event -> {
    // Only triggered for "worldName"
});

Unhandled Events

Register for events that no other listener handled:

getEventRegistry().registerUnhandled(CustomEvent.class, event -> {
    // Only called if no other listener handled this event
});

Asynchronous Events

Async events return CompletableFuture and are handled on separate threads:

// Register async event listener
getEventRegistry().registerAsync(AsyncDataLoadEvent.class, event -> {
    return CompletableFuture.supplyAsync(() -> {
        // Perform async operation
        return loadData(event.getDataId());
    });
});

// Global async listener
getEventRegistry().registerAsyncGlobal(AsyncEvent.class, this::handleAsync);

// Unhandled async listener
getEventRegistry().registerAsyncUnhandled(AsyncEvent.class, this::handleUnhandledAsync);

Cancellable Events

Events implementing ICancellable can be cancelled by listeners:

getEventRegistry().register(PlayerInteractEvent.class, event -> {
    if (shouldBlock(event)) {
        event.setCancelled(true);
    }
});

When checking if an event was cancelled:

getEventRegistry().register(EventPriority.LAST, PlayerInteractEvent.class, event -> {
    if (!event.isCancelled()) {
        // Event was not cancelled by earlier listeners
        processInteraction(event);
    }
});

Common Event Types

Server Lifecycle Events

Event Description
BootEvent Server boot complete
ShutdownEvent Server shutting down
PrepareUniverseEvent Universe initialization

Player Events

Event Description
PlayerConnectEvent Player connected to server
PlayerDisconnectEvent Player disconnected
AddPlayerToWorldEvent Player added to a world
DrainPlayerFromWorldEvent Player removed from a world
PlayerInteractEvent Player interaction (cancellable)
PlayerMouseButtonEvent Mouse button input (cancellable)

Entity Events

Event Description
EntityRemoveEvent Entity removed from world
LivingEntityUseBlockEvent Living entity using a block

World Events

Event Description
AddWorldEvent World added to universe
RemoveWorldEvent World removed
StartWorldEvent World started
AllWorldsLoadedEvent All worlds finished loading

ECS Events

Event Description
UseBlockEvent Block usage event
DiscoverZoneEvent Zone discovery

Creating Custom Events

Synchronous Event

public class MyCustomEvent implements IEvent<String> {
    private final String key;
    private final Player player;
    private final String data;
    
    public MyCustomEvent(String key, Player player, String data) {
        this.key = key;
        this.player = player;
        this.data = data;
    }
    
    @Override
    public String getKey() {
        return key;
    }
    
    public Player getPlayer() {
        return player;
    }
    
    public String getData() {
        return data;
    }
}

Cancellable Event

public class MyCancellableEvent implements IEvent<String>, ICancellable {
    private final String key;
    private boolean cancelled = false;
    
    public MyCancellableEvent(String key) {
        this.key = key;
    }
    
    @Override
    public String getKey() {
        return key;
    }
    
    @Override
    public boolean isCancelled() {
        return cancelled;
    }
    
    @Override
    public void setCancelled(boolean cancelled) {
        this.cancelled = cancelled;
    }
}

Async Event

public class MyAsyncEvent implements IAsyncEvent<String> {
    private final String key;
    
    public MyAsyncEvent(String key) {
        this.key = key;
    }
    
    @Override
    public String getKey() {
        return key;
    }
}

Dispatching Events

To dispatch custom events:

// Get the event bus
SyncEventBusRegistry<String, MyCustomEvent> eventBus = getEventBus(MyCustomEvent.class);

// Dispatch event
MyCustomEvent event = new MyCustomEvent("key", player, "data");
eventBus.dispatch(event);

// For cancellable events, check result
MyCancellableEvent cancellable = new MyCancellableEvent("key");
eventBus.dispatch(cancellable);
if (!cancellable.isCancelled()) {
    // Proceed with action
}

Event Registration Management

Unregistering Listeners

Event registration returns an EventRegistration that can be used to unregister:

EventRegistration registration = getEventRegistry().register(
    PlayerConnectEvent.class, 
    this::onConnect
);

// Later, to unregister:
registration.unregister();

Combining Registrations

Multiple registrations can be combined for bulk management:

EventRegistration reg1 = getEventRegistry().register(Event1.class, this::handle1);
EventRegistration reg2 = getEventRegistry().register(Event2.class, this::handle2);

EventRegistration combined = reg1.combine(reg2);

// Unregister both at once
combined.unregister();

Conditional Listeners

Registrations can be conditionally enabled:

// Listener only active while condition is true
Supplier<Boolean> isEnabled = () -> config.get().featureEnabled;
// The registration checks isEnabled before calling the listener

Best Practices

  1. Use appropriate priorities - Don't use FIRST unless you need to monitor/log before modifications
  2. Check cancellation state - If using LATE or LAST, check if event was cancelled
  3. Don't block in sync handlers - Use async events for long-running operations
  4. Clean up registrations - Unregister listeners in shutdown() if needed
  5. Use keyed events - When possible, listen for specific keys to reduce unnecessary handler calls
  6. Handle exceptions - Wrap handler code in try-catch to prevent crashing other listeners
  7. Document custom events - When creating custom events, document their key type and when they're dispatched