362 lines
9.9 KiB
Markdown
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;
|
|
});
|
|
});
|
|
```
|