336 lines
8.5 KiB
Markdown
336 lines
8.5 KiB
Markdown
# 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
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
getEventRegistry().register(PlayerInteractEvent.class, event -> {
|
|
if (shouldBlock(event)) {
|
|
event.setCancelled(true);
|
|
}
|
|
});
|
|
```
|
|
|
|
When checking if an event was cancelled:
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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:
|
|
|
|
```java
|
|
EventRegistration registration = getEventRegistry().register(
|
|
PlayerConnectEvent.class,
|
|
this::onConnect
|
|
);
|
|
|
|
// Later, to unregister:
|
|
registration.unregister();
|
|
```
|
|
|
|
### Combining Registrations
|
|
|
|
Multiple registrations can be combined for bulk management:
|
|
|
|
```java
|
|
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:
|
|
|
|
```java
|
|
// 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
|