296 lines
8.1 KiB
Markdown
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 >}}
|