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
- Use appropriate priorities - Don't use
FIRSTunless you need to monitor/log before modifications - Check cancellation state - If using
LATEorLAST, check if event was cancelled - Don't block in sync handlers - Use async events for long-running operations
- Clean up registrations - Unregister listeners in
shutdown()if needed - Use keyed events - When possible, listen for specific keys to reduce unnecessary handler calls
- Handle exceptions - Wrap handler code in try-catch to prevent crashing other listeners
- Document custom events - When creating custom events, document their key type and when they're dispatched