--- 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` support asynchronous handling: ```java public interface IAsyncEvent extends IBaseEvent { // 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` - 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 spamCheckFuture = CompletableFuture.supplyAsync(() -> checkForSpam(event.getContent())); CompletableFuture 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` and `ICancellable`: ```java public class PlayerChatEvent implements IAsyncEvent, ICancellable { // sender: PlayerRef // targets: List // 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 filterContent( CompletableFuture 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 processChat( CompletableFuture 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 logChat( CompletableFuture 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; }); }); ```