---
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;
    });
});
```
