Files
Documentation/content/core-concepts/events/async-events.en.md
2026-01-20 20:33:59 +01:00

362 lines
9.9 KiB
Markdown

---
title: Async Events
type: docs
weight: 4
---
Async events allow you to perform asynchronous operations during event handling. They use `CompletableFuture` for non-blocking execution.
## IAsyncEvent Interface
Events implementing `IAsyncEvent<K>` support asynchronous handling:
```java
public interface IAsyncEvent<K> extends IBaseEvent<K> {
// Async events use CompletableFuture in handlers
}
```
## Registering Async Handlers
Use `registerAsync()` for async events with `Void` key, or `registerAsyncGlobal()` for keyed events:
{{< callout type="warning" >}}
`PlayerChatEvent` implements `IAsyncEvent<String>` - it has a `String` key type. Therefore, you must use `registerAsyncGlobal()` instead of `registerAsync()`. The simple `registerAsync(Class, handler)` method only works for events with `Void` key type.
{{< /callout >}}
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// This runs asynchronously
String filtered = filterContent(event.getContent());
event.setContent(filtered);
return event;
});
});
```
## Async Registration Methods
### Basic Async Registration
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async processing
return event;
});
});
```
### With Priority
```java
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Runs early in async chain
return event;
})
);
```
### With Key
```java
getEventRegistry().registerAsync(
KeyedAsyncEvent.class,
myKey,
future -> future.thenApply(event -> {
// Only for events matching myKey
return event;
})
);
```
### Global Async
```java
getEventRegistry().registerAsyncGlobal(
KeyedAsyncEvent.class,
future -> future.thenApply(event -> {
// All events of this type
return event;
})
);
```
### Unhandled Async
```java
getEventRegistry().registerAsyncUnhandled(
KeyedAsyncEvent.class,
future -> future.thenApply(event -> {
// Events not handled by keyed handlers
return event;
})
);
```
## Working with CompletableFuture
### Sequential Operations
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future
.thenApply(event -> {
// Step 1: Filter content
event.setContent(filterProfanity(event.getContent()));
return event;
})
.thenApply(event -> {
// Step 2: Add formatting
event.setContent(addChatFormatting(event.getContent()));
return event;
})
.thenApply(event -> {
// Step 3: Log
logChatMessage(event.getSender(), event.getContent());
return event;
});
});
```
### Parallel Operations
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenCompose(event -> {
// Start multiple async operations in parallel
CompletableFuture<Boolean> spamCheckFuture =
CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent()));
CompletableFuture<Boolean> linkCheckFuture =
CompletableFuture.supplyAsync(() -> checkForLinks(event.getContent()));
// Combine results
return spamCheckFuture.thenCombine(linkCheckFuture, (isSpam, hasLinks) -> {
if (isSpam || hasLinks) {
event.setCancelled(true);
}
return event;
});
});
});
```
### Error Handling
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future
.thenApply(event -> {
// May throw exception
riskyOperation(event);
return event;
})
.exceptionally(throwable -> {
// Handle error
getLogger().severe("Async event failed: " + throwable.getMessage());
return null; // Event will be skipped
});
});
```
## Switching to Main Thread
If you need to perform game operations after async work:
```java
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async: Check external database
boolean isMuted = checkMuteStatus(event.getSender().getUuid());
if (isMuted) {
event.setCancelled(true);
// Schedule world thread work for player notification
World world = Universe.get().getWorld(event.getSender().getWorldUuid());
if (world != null) {
world.execute(() -> {
event.getSender().sendMessage(Message.raw("You are muted!"));
});
}
}
return event;
});
});
```
## PlayerChatEvent - The Main Async Event
`PlayerChatEvent` is the primary async event in Hytale. It implements both `IAsyncEvent<String>` and `ICancellable`:
```java
public class PlayerChatEvent implements IAsyncEvent<String>, ICancellable {
// sender: PlayerRef
// targets: List<PlayerRef>
// content: String
// formatter: Formatter
// cancelled: boolean
}
```
### Complete Chat Handler Example
```java
public class ChatPlugin extends JavaPlugin {
@Override
public void start() {
// Early priority: Content filtering
getEventRegistry().registerAsync(
EventPriority.EARLY,
PlayerChatEvent.class,
this::filterContent
);
// Normal priority: Standard processing
getEventRegistry().registerAsync(
PlayerChatEvent.class,
this::processChat
);
// Late priority: Logging
getEventRegistry().registerAsync(
EventPriority.LATE,
PlayerChatEvent.class,
this::logChat
);
}
private CompletableFuture<PlayerChatEvent> filterContent(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
String content = event.getContent();
// Filter profanity
String filtered = filterProfanity(content);
if (!filtered.equals(content)) {
event.setContent(filtered);
}
return event;
});
}
private CompletableFuture<PlayerChatEvent> processChat(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
if (event.isCancelled()) {
return event;
}
PlayerRef sender = event.getSender();
// Custom formatter with rank prefix
event.setFormatter((playerRef, msg) -> {
String prefix = getRankPrefix(playerRef);
return Message.raw(prefix + playerRef.getUsername() + ": " + msg);
});
return event;
});
}
private CompletableFuture<PlayerChatEvent> logChat(
CompletableFuture<PlayerChatEvent> future) {
return future.thenApply(event -> {
if (!event.isCancelled()) {
logToDatabase(
event.getSender().getUuid(),
event.getContent(),
System.currentTimeMillis()
);
}
return event;
});
}
}
```
## Database Integration Example
```java
public class ChatDatabasePlugin extends JavaPlugin {
private final ExecutorService dbExecutor = Executors.newFixedThreadPool(4);
@Override
public void start() {
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenCompose(event -> {
// Run database check on dedicated thread pool
return CompletableFuture.supplyAsync(() -> {
try {
// Check if player is muted in database
boolean muted = database.isPlayerMuted(event.getSender().getUuid());
if (muted) {
event.setCancelled(true);
}
return event;
} catch (Exception e) {
getLogger().severe("Database error: " + e.getMessage());
return event; // Allow message on DB error
}
}, dbExecutor);
});
});
}
@Override
public void shutdown() {
dbExecutor.shutdown();
}
}
```
## Best Practices
{{< callout type="tip" >}}
**Async Event Tips:**
- Never block the async chain with `.get()` or `.join()` - use `.thenApply()` or `.thenCompose()`
- Use `world.execute()` to return to world thread for game operations
- Handle exceptions with `.exceptionally()` or `.handle()`
- Keep async handlers lightweight for better performance
- Check `isCancelled()` before doing expensive operations
{{< /callout >}}
{{< callout type="warning" >}}
**Thread Safety:**
- PlayerRef's `getReference()` may return null or invalid reference if player disconnected
- Always check `ref != null && ref.isValid()` before accessing ECS data
- Avoid modifying game state directly in async handlers
- Use `world.execute()` to safely access game state from the world thread
{{< /callout >}}
```java
// Safe pattern for async player operations
getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
return future.thenApply(event -> {
// Async work here...
String processed = processMessage(event.getContent());
event.setContent(processed);
// Safe player notification via PlayerRef
PlayerRef sender = event.getSender();
World world = Universe.get().getWorld(sender.getWorldUuid());
if (world != null) {
world.execute(() -> {
sender.sendMessage(Message.raw("Message processed"));
});
}
return event;
});
});
```