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

9.9 KiB

title, type, weight
title type weight
Async Events docs 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:

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 >}}

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

getEventRegistry().registerAsyncGlobal(PlayerChatEvent.class, future -> {
    return future.thenApply(event -> {
        // Async processing
        return event;
    });
});

With Priority

getEventRegistry().registerAsync(
    EventPriority.EARLY,
    PlayerChatEvent.class,
    future -> future.thenApply(event -> {
        // Runs early in async chain
        return event;
    })
);

With Key

getEventRegistry().registerAsync(
    KeyedAsyncEvent.class,
    myKey,
    future -> future.thenApply(event -> {
        // Only for events matching myKey
        return event;
    })
);

Global Async

getEventRegistry().registerAsyncGlobal(
    KeyedAsyncEvent.class,
    future -> future.thenApply(event -> {
        // All events of this type
        return event;
    })
);

Unhandled Async

getEventRegistry().registerAsyncUnhandled(
    KeyedAsyncEvent.class,
    future -> future.thenApply(event -> {
        // Events not handled by keyed handlers
        return event;
    })
);

Working with CompletableFuture

Sequential Operations

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

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

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:

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:

public class PlayerChatEvent implements IAsyncEvent<String>, ICancellable {
    // sender: PlayerRef
    // targets: List<PlayerRef>
    // content: String
    // formatter: Formatter
    // cancelled: boolean
}

Complete Chat Handler Example

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

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