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

296 lines
8.1 KiB
Markdown

---
title: Async Operations
type: docs
weight: 2
---
Asynchronous operations allow plugins to perform heavy tasks without blocking the world's ticking thread.
## Why Async?
{{< callout type="info" >}}
Each World has a dedicated ticking thread that handles game logic, player interactions, and tick updates. Blocking it causes lag and poor player experience. Use async for:
- Database queries
- File I/O
- Network requests
- Complex calculations
{{< /callout >}}
## Basic Async Pattern
Use `CompletableFuture.runAsync()` and `world.execute()`:
```java
PlayerRef playerRef = player.getPlayerRef();
World world = player.getWorld();
CompletableFuture.runAsync(() -> {
// This runs on a background thread
// Do heavy work here
Data result = computeData();
// Return to world thread for game state changes
world.execute(() -> {
playerRef.sendMessage(Message.raw("Result: " + result));
});
});
```
## Thread Safety
### Using PlayerRef
```java
getEventRegistry().register(PlayerConnectEvent.class, event -> {
PlayerRef playerRef = event.getPlayerRef();
World world = event.getWorld();
CompletableFuture.runAsync(() -> {
// Load data asynchronously
PlayerData data = loadFromDatabase(playerRef.getUuid());
// Return to world thread
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
applyData(ref, data);
}
});
});
});
```
### Thread-Safe Collections
```java
// Use concurrent collections for shared data
private final Map<UUID, PlayerData> playerData = new ConcurrentHashMap<>();
private final Set<UUID> processing = ConcurrentHashMap.newKeySet();
public void processPlayer(PlayerRef playerRef, World world) {
UUID uuid = playerRef.getUuid();
// Prevent duplicate processing
if (!processing.add(uuid)) {
return; // Already processing
}
CompletableFuture.runAsync(() -> {
try {
PlayerData data = compute(uuid);
playerData.put(uuid, data);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Processing complete!"));
});
} finally {
processing.remove(uuid);
}
});
}
```
## Async Events
Handle async events with CompletableFuture:
```java
getEventRegistry().registerAsync(AsyncEvent.class, event -> {
return CompletableFuture.supplyAsync(() -> {
// Async processing
return processEvent(event);
});
});
```
## Common Patterns
### Database Operations
```java
public void savePlayerData(PlayerRef playerRef, World world) {
// Capture data on world thread before going async
PlayerData data = captureData(playerRef);
CompletableFuture.runAsync(() -> {
try {
database.save(playerRef.getUuid(), data);
getLogger().at(Level.INFO).log("Saved data for " + playerRef.getUsername());
} catch (Exception e) {
getLogger().error("Failed to save data", e);
}
});
}
public void loadPlayerData(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
PlayerData data = database.load(playerRef.getUuid());
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null && data != null) {
applyData(ref, data);
playerRef.sendMessage(Message.raw("Data loaded!"));
}
});
});
}
```
### HTTP Requests
```java
public void fetchPlayerStats(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
String response = httpClient.get("https://api.example.com/stats/" + playerRef.getUuid());
Stats stats = parseStats(response);
world.execute(() -> {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
displayStats(ref, stats);
}
});
} catch (Exception e) {
world.execute(() -> {
playerRef.sendMessage(Message.raw("Failed to fetch stats."));
});
}
});
}
```
### Batch Processing
```java
public void processAllPlayers(World world) {
// Capture player refs on world thread
List<PlayerRef> players = new ArrayList<>();
world.getEntityStore().forEach((ref, store) -> {
PlayerRef playerRef = store.getComponent(ref, PlayerRef.getComponentType());
if (playerRef != null) {
players.add(playerRef);
}
});
CompletableFuture.runAsync(() -> {
Map<UUID, Result> results = new HashMap<>();
for (PlayerRef playerRef : players) {
results.put(playerRef.getUuid(), computeResult(playerRef));
}
world.execute(() -> {
for (PlayerRef playerRef : players) {
Ref<EntityStore> ref = playerRef.getReference();
if (ref != null) {
Result result = results.get(playerRef.getUuid());
applyResult(ref, result);
}
}
});
});
}
```
## Error Handling
```java
public void safeAsyncOperation(PlayerRef playerRef, World world) {
CompletableFuture.runAsync(() -> {
try {
riskyOperation(playerRef.getUuid());
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation successful!"));
});
} catch (Exception e) {
getLogger().error("Async operation failed", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation failed. Please try again."));
});
}
});
}
```
## Chaining Async Operations
Use `CompletableFuture` chaining for sequential async operations:
```java
public void chainedOperations(PlayerRef playerRef, World world) {
CompletableFuture
.supplyAsync(() -> {
// First async operation
return fetchFromDatabase(playerRef.getUuid());
})
.thenApplyAsync(data -> {
// Second async operation
return processData(data);
})
.thenAccept(result -> {
// Return to world thread with final result
world.execute(() -> {
playerRef.sendMessage(Message.raw("Final result: " + result));
});
})
.exceptionally(e -> {
getLogger().error("Chain failed", e);
world.execute(() -> {
playerRef.sendMessage(Message.raw("Operation failed."));
});
return null;
});
}
```
## Best Practices
{{< callout type="warning" >}}
**Async Rules:**
1. Never access game state directly from async threads
2. Always use `PlayerRef`, never store `Player` or `Ref<EntityStore>`
3. Capture needed data before going async
4. Use `world.execute()` to return to world thread
5. Handle exceptions properly
6. Use concurrent collections for shared data
{{< /callout >}}
```java
// CORRECT: Capture data, process async, apply on world thread
public void correctPattern(PlayerRef playerRef, World world) {
String username = playerRef.getUsername(); // Capture on world thread
CompletableFuture.runAsync(() -> {
String result = process(username);
world.execute(() -> {
playerRef.sendMessage(Message.raw(result));
});
});
}
// WRONG: Using world objects directly in async
// public void wrongPattern(Player player, Ref<EntityStore> ref) {
// CompletableFuture.runAsync(() -> {
// // DON'T DO THIS - these may be invalid from async context
// String name = player.getDisplayName();
// player.sendMessage("Hello");
// });
// }
```
## Comparison with Other Platforms
{{< callout type="info" >}}
**Note for developers from other platforms:**
- Unlike Bukkit/Spigot, Hytale does NOT have `runSync()` or `runLater()` methods
- Use `CompletableFuture.runAsync()` instead of platform-specific async methods
- Use `world.execute()` instead of `runSync()` to return to the world thread
- Use `CompletableFuture.delayedExecutor()` for delayed tasks
- Use `ScheduledExecutorService` for repeating tasks
{{< /callout >}}